1#
2# TCL library for Netmagis
3#
4#
5# History
6#   2002/03/27 : pda/jean : design
7#   2002/05/23 : pda/jean : add info-groupe
8#   2004/01/14 : pda/jean : add IPv6
9#   2004/08/04 : pda/jean : aadd MAC
10#   2004/08/06 : pda/jean : extension of network access rights
11#   2006/01/26 : jean     : bug fix in check-authorized-host (case ip EXIST)
12#   2006/01/30 : jean     : alias message in check-authorized-host
13#   2010/11/29 : pda      : i18n
14#   2010/12/17 : pda      : reworked installation and parameters
15#   2011/01/02 : pda      : integration of libauth in libdns
16#   2011/07/29 : pda      : renamed to libnetmagis
17#
18
19##############################################################################
20# Configuration file processing
21##############################################################################
22
23#
24# Read configuration file
25#
26# Input:
27#   - parameters:
28#	- file : configuration file
29# Output:
30#   - none (program ends if an error is encountered)
31#
32# History
33#   2010/12/17 : pda      : design
34#   2013/08/29 : pda/jean : reset the internal representation before file read
35#   2014/02/26 : pda/jean : add the pseudo-parameter _conffile
36#   2014/02/26 : pda/jean : add the pseudo-parameter _version
37#
38
39proc read-local-conf-file {file} {
40    global netmagisconf
41
42    if {[catch {set fd [open "$file" "r"]} msg]} then {
43	puts stderr "Cannot open configuration file '$file'"
44	exit 1
45    }
46    set lineno 1
47    set errors false
48    array unset netmagisconf
49    while {[gets $fd line] >= 0} {
50	regsub {#.*} $line {} line
51	regsub {\s*$} $line {} line
52	if {$line ne ""} then {
53	    if {[regexp {(\S+)\s+"(.*)"} $line m key val]} then {
54		set netmagisconf($key) $val
55	    } elseif {[regexp {(\S+)\s+(.*)} $line m key val]} then {
56		set netmagisconf($key) $val
57	    } else {
58		puts stderr "$file($lineno): unrecognized line $line"
59		set errors true
60	    }
61	}
62	incr lineno
63    }
64    close $fd
65    if {$errors} then {
66	exit 1
67    }
68    set netmagisconf(_conffile) $file
69    set netmagisconf(_version) "%NMVERSION%"
70}
71
72#
73# Get configuration key
74#
75# Input:
76#   - parameters:
77#	- key : configuration key
78# Output:
79#   - return value: configuration value or empty string
80#
81# History
82#   2010/12/17 : pda      : design
83#   2010/12/19 : pda      : empty string if key is not found
84#
85
86proc get-local-conf {key} {
87    global netmagisconf
88
89    if {[info exists netmagisconf($key)]} then {
90	set v $netmagisconf($key)
91    } else {
92	set v ""
93    }
94    return $v
95}
96
97#
98# Get database handle
99#
100# Input:
101#   - parameters:
102#	- prefix : prefix for configuration keys (e.g. db for dbhost/dbname/...)
103# Output:
104#   - return value: conninfo script for pg_connect
105#
106# History
107#   2010/12/17 : pda      : design
108#   2011/01/21 : pda      : add port specification
109#   2013/02/08 : pda/jean : fix bug in values containing special characters
110#
111
112proc get-conninfo {prefix} {
113    set conninfo {}
114    foreach f {{host host} {port port} {dbname name}
115			{user user} {password password}} {
116	lassign $f connkey suffix
117	set v [get-local-conf "$prefix$suffix"]
118	regsub {['\\]} $v {\\&} v
119	lappend conninfo "$connkey='$v'"
120    }
121    return [join $conninfo " "]
122}
123
124##############################################################################
125# Library initialization
126##############################################################################
127
128read-local-conf-file %CONFFILE%
129
130lappend auto_path [get-local-conf "pkgtcl"]
131set debug [get-local-conf "debug"]
132
133package require msgcat			;# tcl
134namespace import ::msgcat::*
135
136package require snit			;# tcllib
137package require ip			;# tcllib
138package require md5			;# tcllib
139package require md5crypt		;# tcllib
140package require uuid			;# tcllib
141
142package require webapp
143package require pgsql
144package require arrgen
145
146##############################################################################
147# Library parameters
148##############################################################################
149
150#
151# Authentication pages
152#
153
154set libconf(page-login)		"login.html"
155set libconf(next-login)		"login"
156
157set libconf(token-length)	64
158
159#
160# Various table specifications
161#
162
163set libconf(tabperm) {
164    global {
165	chars {10 normal}
166	align {left}
167	botbar {yes}
168	columns {75 25}
169    }
170    pattern Normal {
171	vbar {yes}
172	column { }
173	vbar {yes}
174	column { }
175	vbar {yes}
176    }
177}
178
179set libconf(tabdreq) {
180    global {
181	chars {10 normal}
182	align {left}
183	botbar {yes}
184	columns {20 80}
185    }
186    pattern PermEq {
187	vbar {yes}
188	column { }
189	vbar {yes}
190	column {
191	    chars {bold}
192	    format {lines}
193	}
194	vbar {yes}
195    }
196}
197
198set libconf(tabnetworks) {
199    global {
200	chars {10 normal}
201	align {left}
202	botbar {yes}
203	columns {15 35 15 35}
204    }
205    pattern Network {
206	vbar {yes}
207	column {
208	    align {center}
209	    chars {14 bold}
210	    multicolumn {4}
211	}
212	vbar {yes}
213    }
214    pattern Normal4 {
215	vbar {yes}
216	column { }
217	vbar {yes}
218	column {
219	    chars {bold}
220	}
221	vbar {yes}
222	column { }
223	vbar {yes}
224	column {
225	    chars {bold}
226	}
227	vbar {yes}
228    }
229    pattern Perm {
230	vbar {yes}
231	column { }
232	vbar {yes}
233	column {
234	    multicolumn {3}
235	    chars {bold}
236	    format {lines}
237	}
238	vbar {yes}
239    }
240}
241
242set libconf(tabdomains) {
243    global {
244	chars {10 normal}
245	align {left}
246	botbar {yes}
247	columns {50 50}
248    }
249    pattern Title {
250	chars {gras}
251	vbar {yes}
252	column { }
253	vbar {yes}
254	column { }
255	vbar {yes}
256    }
257    pattern Normal {
258	vbar {yes}
259	column { }
260	vbar {yes}
261	column { }
262	vbar {yes}
263    }
264}
265
266set libconf(tabviews) $libconf(tabdomains)
267
268set libconf(tabdhcpprofile) {
269    global {
270	chars {10 normal}
271	align {left}
272	botbar {yes}
273	columns {25 75}
274    }
275    pattern DHCP {
276	vbar {yes}
277	column { }
278	vbar {no}
279	column {
280	    format {lines}
281	}
282	vbar {yes}
283    }
284}
285
286set libconf(tabl2only) {
287    global {
288	chars {10 normal}
289	align {left}
290	botbar {yes}
291	columns {100}
292    }
293    pattern Normal {
294	vbar {yes}
295	column {
296	    format {lines}
297	}
298	vbar {yes}
299    }
300}
301
302set libconf(tabmachine) {
303    global {
304	chars {10 normal}
305	align {left}
306	botbar {yes}
307	columns {20 80}
308    }
309    pattern Normal {
310	vbar {yes}
311	column { }
312	vbar {yes}
313	column {
314	    format {raw}
315	}
316	vbar {yes}
317    }
318}
319
320set libconf(tabwtmp) {
321    global {
322	chars {10 normal}
323	align {left}
324	botbar {yes}
325	columns {15 25 25 25 25}
326    }
327    pattern Title {
328	vbar {yes}
329	column { chars {bold} }
330	vbar {yes}
331	column { chars {bold} }
332	vbar {yes}
333	column { chars {bold} }
334	vbar {yes}
335	column { chars {bold} }
336	vbar {yes}
337	column { chars {bold} }
338	vbar {yes}
339    }
340    pattern Normal {
341	vbar {yes}
342	column { }
343	vbar {yes}
344	column { }
345	vbar {yes}
346	column { }
347	vbar {yes}
348	column { }
349	vbar {yes}
350	column { }
351	vbar {yes}
352    }
353}
354
355set libconf(tabuser) {
356    global {
357	chars {10 normal}
358	align {left}
359	botbar {yes}
360	columns {20 80}
361    }
362    pattern Normal {
363	vbar {yes}
364	column { }
365	vbar {yes}
366	column {
367	    chars {gras}
368	}
369	vbar {yes}
370    }
371}
372
373set libconf(tabeqstatus) {
374    global {
375	chars {10 normal}
376	align {left}
377	botbar {yes}
378	columns {20 10 20 50}
379    }
380    pattern Title4 {
381	chars {gras}
382	vbar {yes}
383	column { }
384	vbar {yes}
385	column { }
386	vbar {yes}
387	column { }
388	vbar {yes}
389	column { }
390	vbar {yes}
391    }
392    pattern Normal4 {
393	vbar {yes}
394	column { }
395	vbar {yes}
396	column { }
397	vbar {yes}
398	column { }
399	vbar {yes}
400	column { }
401	vbar {yes}
402    }
403}
404
405
406set libconf(extractcoll)	"extractcoll %s"
407set libconf(extracteq)		"extracteq %s %s"
408
409# Cisco aironet frequency conversion table
410array set libconf {
411    freq:2412	1
412    freq:2417	2
413    freq:2422	3
414    freq:2427	4
415    freq:2432	5
416    freq:2437	6
417    freq:2442	7
418    freq:2447	8
419    freq:2452	9
420    freq:2457	10
421    freq:2462	11
422}
423
424# Authorised characters for vlan name
425set libconf(vlan-chars)                {/+. a-zA-Z0-9()<>_-}
426
427##############################################################################
428# Netmagis application framework
429##############################################################################
430
431#
432# Netmagis access class
433#
434# This class is a simple way to initialize the whole context of all
435# Netmagis programs (CGI scripts, daemons, command line utilities).
436#
437# Methods:
438#   cgi-register
439#	register a CGI script and conditions to execute it
440#   cgi-dispatch
441#	dispatch execution to a registered CGI script
442#   init-script
443#	initialize context for an autonomous program (not CGI)
444#   locale
445#	set current locale
446#   end
447#	properly close access to application and to database
448#   nextprog, nextargs
449#	return next action (prog and args), i.e. page to come back when
450#	current action (travel in the application) is finished
451#   euid
452#	returns the effective login and id of user
453#   urlset
454#	register a named URL as a path and arguments. These components
455#	will be used in the output page, or with the urlget method
456#   urladd
457#	adds an argument to a registered named URL
458#   urlsetnext
459#	adds a specified next action (see nextprog/nextargs) to a
460#	registered named URL
461#   urladdnext
462#	adds the current next action (see nextprog/nextargs) to a
463#	registered named URL
464#   urlsubst
465#	returns a substitution list (see ::webapp::file-subst) with all
466#	registered URLs
467#   urlget
468#	returns (and de-register) a named URL
469#   module
470#	sets the current module, used for the links menu
471#   error
472#	returns an error page and close access to application
473#   errimg
474#	returns an error image and close access to application
475#   result
476#	returns a page and close access to application
477#   writelog
478#	write a log message in the log system
479#   dblock, dbabort, dbcommit
480#	database locking/unlocking operations
481#
482# History
483#   2001/06/18 : pda      : design
484#   2002/12/26 : pda      : update and usage
485#   2003/05/13 : pda/jean : integration in netmagis and auth class usage
486#   2007/10/05 : pda/jean : adaptation to "authuser" and "authbase" objects
487#   2007/10/26 : jean     : add log
488#   2010/10/25 : pda      : add dnsconfig
489#   2010/11/05 : pda      : use a snit object
490#   2010/11/09 : pda      : add init-script
491#   2010/11/29 : pda      : i18n
492#   2010/12/21 : pda/jean : add version in class
493#   2011/02/18 : pda      : add scriptmode
494#   2012/01/02 : pda      : add errimg
495#
496
497snit::type ::netmagis {
498    # Netmagis version
499    variable version "%NMVERSION%"
500
501    # cgi script dispatching (see cgi-register)
502    # critform : list of field names
503    # critscript : list {{crit form script} {crit form script} ...}
504    variable critform {}
505    variable critscript {}
506
507    # database handle
508    variable db ""
509
510    # mode : script, cgi, daemon
511    variable scriptmode ""
512
513    # in script or daemon mode, name of executing program
514    variable scriptargv0
515
516    # locale in use : either specified by browser, or specified by user
517    variable locale "C"
518    # locale specified by browser
519    variable blocale "C"
520    # all available locales. Order is not important.
521    variable avlocale {fr en}
522
523    # log access
524    variable log
525
526    # uid, and effective uid
527    variable uid ""
528    variable euid ""
529    variable eidcor -1
530
531    # HTML error page
532    variable errorpage "error.html"
533
534    # HTML home page
535    variable homepage -array {
536	:anon		index
537	:dns		start
538	:admin		admindex
539	:pgauth		pgauth
540	:mac		macindex
541	:topo		eq
542    }
543
544    # in order to come back from a travel in the Netmagis application
545    variable dnextprog ""
546    variable dnextargs ""
547
548    # URL declared in the scripts
549    # urltab(<name>) = {path {key val} {key val} {key val...}}
550    # <name> = %[A-Z0-9]+% or "" for a temporary URL
551    # urltab(<name>:nextprog) = <nextprog> or empty string
552    # urltab(<name>:nextargs) = <nextargs> (if <nextprog> != empty string)
553    variable urltab -array {}
554
555    # where are we in the application?
556    # authorized values: dns topo admin
557    variable curmodule	""
558
559    # current capacities (depending on user access rights or application
560    # installation/parametrization)
561    # possible values: admin dns topo
562    variable curcap	{}
563
564    # Links menu
565    # This array has a tree structure:
566    #	tab(:<module>)	{{<element>|:<module> <cap>}..{<element>|:<module> <cap>}}
567    #   tab(<element>)	{<url> <desc>}
568    #
569    # The first type gives display order for a module
570    #	- a module is one of the values of the "curmodule" variable,
571    #		or a reference from another module (in this array)
572    #	- each element or module is displayed only if the condition
573    #		"cap" (capacity) is true for this user. Special "always"
574    #		capacity means that this element or module is always
575    #		displayed.
576    #	- if a module is mentionned in the list, this module is
577    #		recursively searched (which gives the tree structure,
578    #		elements are the terminal nodes)
579    # The second type gives the display of a particular element.
580    variable links -array {
581	:anon		{
582			}
583	:dns		{
584			    {start always}
585			    {net always}
586			    {add always}
587			    {del always}
588			    {mod always}
589			    {mail always}
590			    {dhcprange always}
591			    {search always}
592			    {whereami always}
593			    {topotitle topo}
594			    {passwd pgauth}
595			    {mactitle mac}
596			    {admtitle admin}
597			}
598	start		{start Welcome}
599	net		{net Consult}
600	add		{add Add}
601	del		{del Delete}
602	mod		{mod Modify}
603	mail		{mail {Mail roles}}
604	dhcprange	{dhcp {DHCP ranges}}
605	passwd		{pgapasswd Password}
606	search		{search Search}
607	whereami	{search?q=_ {Where am I?}}
608	topotitle	{eq Topology}
609	mactitle	{macindex Mac}
610	admtitle	{admindex Admin}
611	:topo		{
612			    {eq always}
613			    {l2 always}
614			    {l3 always}
615			    {genl topogenl}
616			    {topotop admin}
617			    {dnstitle dns}
618			    {mactitle mac}
619			    {admtitle admin}
620			}
621	eq		{eq Equipments}
622	l2		{l2 Vlans}
623	l3		{l3 Networks}
624	dnstitle	{start DNS/DHCP}
625	genl		{genl {Link number}}
626	:admin		{
627			    {admtitle always}
628			    {pgatitle authadmin}
629			    {admlmx always}
630			    {lnet always}
631			    {lusers always}
632			    {search always}
633			    {whonow always}
634			    {wholast always}
635			    {modorg always}
636			    {modcomm always}
637			    {modhinfo always}
638			    {modnetwork always}
639			    {moddomain always}
640			    {admmrel always}
641			    {admmx always}
642			    {modview always}
643			    {modzone always}
644			    {modzone4 always}
645			    {modzone6 always}
646			    {moddhcpprof always}
647			    {modvlan always}
648			    {modeqtype always}
649			    {modeq always}
650			    {modconfcmd always}
651			    {moddotattr always}
652			    {admgrp always}
653			    {admzgen always}
654			    {admpar always}
655			    {statuser always}
656			    {statorg always}
657			    {topotop topo}
658			    {dnstitle dns}
659			    {topotitle topo}
660			    {mactitle mac}
661			}
662	pgatitle	{pgaindex {Internal Auth}}
663	admlmx		{admlmx {List MX}}
664	lnet		{lnet {List networks}}
665	lusers		{lusers {List users}}
666	whonow		{who?action=now {Connected users}}
667	wholast		{who?action=last {Last connections}}
668	modorg		{admref?type=org {Modify organizations}}
669	modcomm		{admref?type=comm {Modify communities}}
670	modhinfo	{admref?type=hinfo {Modify machine types}}
671	modnetwork	{admref?type=net {Modify networks}}
672	moddomain	{admref?type=domain {Modify domains}}
673	admmrel		{admmrel {Modify mailhost}}
674	admmx		{admmx {Modify MX}}
675	modview		{admref?type=view {Modify views}}
676	modzone		{admref?type=zone {Modify zones}}
677	modzone4	{admref?type=zone4 {Modify reverse IPv4 zones}}
678	modzone6	{admref?type=zone6 {Modify reverse IPv6 zones}}
679	moddhcpprof	{admref?type=dhcpprof {Modify DHCP profiles}}
680	modvlan		{admref?type=vlan {Modify Vlans}}
681	modeqtype	{admref?type=eqtype {Modify equipment types}}
682	modeq		{admref?type=eq {Modify equipments}}
683	modconfcmd	{admref?type=confcmd {Modify configuration commands}}
684	moddotattr	{admref?type=dotattr {Modify Graphviz attributes}}
685	admgrp		{admgrp {Modify users and groups}}
686	admzgen		{admzgen {Force zone generation}}
687	admpar		{admpar {Application parameters}}
688	statuser	{statuser {Statistics by user}}
689	statorg		{statorg {Statistics by organization}}
690	topotop		{topotop {Topod status}}
691	:mac		{
692			    {macindex always}
693			    {mac always}
694			    {ipinact always}
695			    {macstat always}
696			    {dnstitle dns}
697			    {topotitle topo}
698			    {admtitle admin}
699			}
700	macindex	{macindex {MAC index}}
701	mac		{mac {MAC search}}
702	ipinact		{ipinact {Inactive addresses}}
703	macstat		{macstat {MAC stats}}
704	:pgauth	{
705			    {admtitle always}
706			    {pgatitle authadmin}
707			    {pgaalst authadmin}
708			    {pgaaprn authadmin}
709			    {pgaaadd authadmin}
710			    {pgaamod authadmin}
711			    {pgaadel authadmin}
712			    {pgaapasswd authadmin}
713			    {pgarlst authadmin}
714			    {pgaradd authadmin}
715			    {pgarmod authadmin}
716			    {pgardel authadmin}
717			    {dnstitle dns}
718			    {topotitle topo}
719			    {mactitle mac}
720			}
721	pgaalst		{pgaacc?action=list {List accounts}}
722	pgaaprn		{pgaacc?action=print {Print accounts}}
723	pgaaadd		{pgaacc?action=add {Add account}}
724	pgaamod		{pgaacc?action=mod {Modify account}}
725	pgaadel		{pgaacc?action=del {Remove account}}
726	pgaapasswd	{pgaacc?action=passwd {Change account password}}
727	pgarlst		{pgarealm?action=list {List realms}}
728	pgaradd		{pgarealm?action=add {Add realm}}
729	pgarmod		{pgarealm?action=mod {Modify realm}}
730	pgardel		{pgarealm?action=del {Remove realm}}
731    }
732
733    #
734    # Links for the session menu
735    #
736
737    variable sessionlinks -array {
738	login		login
739	logout		login?logout=yes
740	profile		profile
741    }
742
743
744    ###########################################################################
745    # Internal procedures
746    ###########################################################################
747
748    #
749    # Database initialization
750    #
751    # Input:
752    #	- selfs : current object
753    #	- _dbfd : database handle, in return
754    #
755    # Output:
756    #	- return value: empty string or error message
757    #
758
759    proc init-database {selfns _dbfd} {
760	upvar $_dbfd dbfd
761
762	#
763	# Access to Netmagis database
764	#
765
766	set conninfo [get-conninfo "dnsdb"]
767	if {[catch {set dbfd [pg_connect -conninfo $conninfo]} msg]} then {
768	    return [mc "Error accessing database: %s" $msg]
769	}
770
771	#
772	# Access to configuration parameters (stored in the database)
773	#
774
775	config ::dnsconfig
776	dnsconfig setdb $dbfd
777
778	#
779	# Check compatibility with database schema version
780	# - empty string : pre-2.2 schema
781	# - non empty string : integer containing schema version
782	# Netmagis version (x.y.... => xy) must match schema version.
783	#
784
785	# get code version (from top-level Makefile)
786	if {! [regsub {^(\d+)\.(\d+).*} $version {\1\2} nver]} then {
787	    return [mc "Internal error: Netmagis version number '%s' unrecognized" $version]
788	}
789
790	# get schema version (from database)
791	if {[catch {dnsconfig get "schemaversion"} sver]} then {
792	    set sver ""
793	}
794
795	if {$sver eq ""} then {
796	    return [mc "Database schema is too old. See http://netmagis.org/upgrade.html"]
797	} elseif {$sver < $nver} then {
798	    return [mc "Database schema is too old. See http://netmagis.org/upgrade.html"]
799	} elseif {$sver > $nver} then {
800	    return [mc {Database schema '%1$s' is not yet recognized by Netmagis %2$s} $sver $version]
801	}
802
803	#
804	# Log initialization
805	#
806
807	set log [::webapp::log create %AUTO% \
808				    -subsys netmagis \
809				    -method opened-postgresql \
810				    -medium [list "db" $dbfd table global.log] \
811			]
812
813	#
814	# Access to database is initialized
815	#
816
817	set db $dbfd
818
819	return ""
820    }
821
822    #
823    # Common initialization work
824    #
825    # Input:
826    #	- selfs : current object
827    #	- dbfd : database handle
828    #   - login : user's login
829    #   - anon : "anon" (don't fetch identity in auth database) or "id" (fetch)
830    #	- usedefuser : use default user name if login is not found
831    #   - _tabuid : array containing, in return, user's characteristics
832    #		(login, password, lastname, firstname, mail, phone, fax,
833    #			mobile, addr, idcor, idgrp, present)
834    #
835    # Output:
836    #	- return value: empty string or error message
837    #
838
839    proc init-common {selfns dbfd login anon usedefuser _tabuid} {
840	global ah
841	upvar $_tabuid tabuid
842
843	set uid $login
844	set euid $login
845
846	#
847	# Access to authentification mechanism (database or LDAP)
848	#
849
850	set am [dnsconfig get "authmethod"]
851	switch $am {
852	    pgsql {
853		set m {-method opened-postgresql}
854		lappend m "-db" $dbfd
855	    }
856	    casldap -
857	    ldap {
858		foreach v {ldapurl ldapbinddn ldapbindpw ldapbasedn
859				ldapsearchlogin ldapattrlogin
860				ldapattrname ldapattrgivenname ldapattrmail
861				ldapattrphone ldapattrmobile ldapattrfax
862				ldapattraddr} {
863		    set $v [dnsconfig get $v]
864		}
865		set m {-method ldap}
866		lappend m "-db" [list \
867				    "url" $ldapurl \
868				    "binddn" $ldapbinddn \
869				    "bindpw" $ldapbindpw \
870				    "base" $ldapbasedn \
871				    "searchuid" $ldapsearchlogin \
872				    ]
873		lappend m "-attrmap" [list \
874					"login" $ldapattrlogin \
875					"lastname" $ldapattrname \
876					"firstname" $ldapattrgivenname \
877					"maill" $ldapattrmail \
878					"phone" $ldapattrphone \
879					"mobile" $ldapattrmobile \
880					"fax" $ldapattrfax \
881					"addr" $ldapattraddr \
882					]
883	    }
884	    default {
885		return [mc "Unrecognized authentication method '%s'" $am]
886	    }
887	}
888
889	switch $anon {
890	    id {
891		set ah [::webapp::authbase create %AUTO%]
892		$ah configurelist $m
893	    }
894	    anon {
895		set ah ""
896	    }
897	}
898
899	#
900	# Reads all user's characteristics. If this user is not
901	# marked "present" in the database, get him out!
902	#
903
904	set n [read-user $dbfd $login tabuid msg]
905	switch $n {
906	    0 {
907		if {$usedefuser} then {
908		    set login [dnsconfig get "defuser"]
909
910		    set uid $login
911		    set euid $login
912		    set n [read-user $dbfd $login tabuid msg]
913		}
914		# IF user is not found
915		#    OR (able to use default user AND default user is not found)
916		if {$n != 1} then {
917		    return $msg
918		}
919	    }
920	    1 {
921	    	# Set at least the login
922	    	set tabuid(login) $login
923	    }
924	    default {
925		return $msg
926	    }
927	}
928	if {! $tabuid(present)} then {
929	    return [mc "User '%s' not authorized" $login]
930	}
931	set eidcor $tabuid(idcor)
932
933	#
934	# Initializes user object
935	#
936	::nmuser create ::u
937	u setdb $dbfd
938	u setlogin $login
939
940	#
941	# Access to Netmagis is now initialized
942	#
943
944	return ""
945    }
946
947    #
948    # Builds up an URL
949    #
950    # Input:
951    #   - _urltab : name of an array containing :
952    #		urltab($name): the list {path {key val} {key val} ...}
953    #		urltab($name:nextprog) program
954    #		urltab($name:nextargs) arguments
955    #   - name : index in urltab
956    #	- u, eu : uid and effective uid
957    #	- l, bl : locale and browser locale
958    # Output:
959    #   - return value: URL
960    #
961    # Each element {key val} may optionnally be a single string "key=val",
962    #	in which case it must be post-string encoded)
963    #
964
965    proc make-url {_urltab name u eu l bl} {
966	upvar $_urltab urltab
967
968	set path [lindex $urltab($name) 0]
969	set largs [lreplace $urltab($name) 0 0]
970
971	#
972	# Two possible cases:
973	# - URL is a local one (does not begin with "http://")
974	# - URL is external (begins with "http://")
975	# In the last case, don't add default arguments which are
976	# specific to Netmagis application.
977	#
978
979	if {! [regexp {^https?://} $path]} then {
980	    #
981	    # Add default arguments
982	    #
983
984	    # user susbtitution
985	    if {$u ne $eu} then {
986		lappend largs [list "uid" $u]
987	    }
988
989	    # default locale
990	    if {$l ne $bl} then {
991		lappend largs [list "l" $l]
992	    }
993
994	    # travel in the application
995	    if {$urltab($name:nextprog) ne ""} then {
996		lappend largs [list "nextprog" $urltab($name:nextprog)]
997		lappend largs [list "nextargs" $urltab($name:nextargs)]
998	    }
999
1000	    #
1001	    # Build-up the argument list
1002	    #
1003
1004	    set l {}
1005	    foreach keyval $largs {
1006		if {[llength $keyval] == 1} then {
1007		    lappend l $keyval
1008		} else {
1009		    lassign $keyval k v
1010		    set v [::webapp::post-string $v]
1011		    lappend l "$k=$v"
1012		}
1013	    }
1014
1015	    #
1016	    # Build-up URL from path and arguments
1017	    #
1018
1019	    if {[llength $l] == 0} then {
1020		# no argument: simple case
1021		set url $path
1022	    } else {
1023		if {[string match {*\?*} $path]} then {
1024		    # already an argument in the path
1025		    set url [format "%s&%s" $path [join $l "&"]]
1026		} else {
1027		    # not yet an argument in the path
1028		    set url [format "%s?%s" $path [join $l "&"]]
1029		}
1030	    }
1031	} else {
1032	    set url $path
1033	}
1034
1035	unset urltab($name)
1036	return $url
1037    }
1038
1039    #
1040    # Recursive internal method to get links menu
1041    #
1042    # Input:
1043    #	- eorm = element (without ":") or module (with ":")
1044    # Output:
1045    #	- HTML code for the menu
1046    #
1047
1048    method Get-links {eorm} {
1049	set h ""
1050	if {[info exists links($eorm)]} then {
1051	    set lks $links($eorm)
1052
1053	    if {[string match ":*" $eorm]} then {
1054		foreach couple $lks {
1055		    lassign $couple neorm cond
1056		    if {$cond eq "always" || $cond in $curcap} then {
1057			append h [$self Get-links $neorm]
1058			append h "\n"
1059		    }
1060		}
1061	    } else {
1062		lassign $lks path msg
1063		$self urlset "" $path {}
1064		set url [make-url urltab "" $uid $euid $locale $blocale]
1065
1066		append h [::webapp::helem "li" \
1067				[::webapp::helem "a" [mc $msg] "href" $url]]
1068		append h "\n"
1069	    }
1070
1071	} else {
1072	    append h [::webapp::helem "li" [mc "Unknown module '%s'" $eorm] ]
1073	    append h "\n"
1074	}
1075	return $h
1076    }
1077
1078    ###########################################################################
1079    # Register a CGI script
1080    #
1081    # Input:
1082    #	- crit : criterion list {field regexp field regexp ...}
1083    #	- form : form field specification (see webapp::get-data)
1084    #   - script : script to execute if criterion matches.
1085    #		Variables defined in script:
1086    #		- dbfd : database descriptor
1087    #		- ftab : field array (see webapp::get-data)
1088    #		- tabuid : user's characteristics
1089    #		(login, password, lastname, firstname, mail, phone, fax,
1090    #			mobile, addr, idcor, idgrp, present)
1091    # Output: (none)
1092    #
1093
1094    method cgi-register {crit form script} {
1095	#
1096	# Memorize field name from criterion
1097	#
1098	foreach {f re} $crit {
1099	    lappend critform $f
1100	}
1101
1102	#
1103	# Memorize criterion, form and script
1104	#
1105	lappend critscript [list $crit $form $script]
1106    }
1107
1108    ###########################################################################
1109    # Dispatch to CGI actions
1110    #
1111    # Input:
1112    #   - module : current module we are in ("dns", "admin" or "topo")
1113    #		or "anon" to access unauthentified pages
1114    #   - attr : needed attribute to execute the script
1115    # Output:
1116    #   - return value: none
1117    #   - object d : Netmagis context
1118    #   - object $ah : access to authentication base
1119    #
1120
1121    method cgi-dispatch {module attr} {
1122	global libconf
1123
1124	#
1125	# Builds-up a fictive context to easily return error messages
1126	#
1127
1128	set curmodule "anon"
1129	set curcap {dns}
1130	set locale "C"
1131	set blocale "C"
1132	set scriptmode "cgi"
1133
1134	set debug [get-local-conf "debug"]
1135
1136	#
1137	# Language negociation
1138	#
1139
1140	set blocale [::webapp::locale $avlocale]
1141	$self locale $blocale
1142
1143	#
1144	# Database initialization
1145	#
1146
1147	set msg [init-database $selfns dbfd]
1148	if {$msg ne ""} then {
1149	    $self error $msg
1150	}
1151
1152	#
1153	# Keep track of authentication status
1154	#
1155
1156	set authtoken [::webapp::get-cookie "session"]
1157	set authenticated [check-authtoken $dbfd $authtoken login]
1158
1159	if {$attr ne "anon"} then {
1160	    #
1161	    # Attempt to access a page restricted to valid users
1162	    #
1163
1164	    if {! $authenticated} then {
1165
1166		#
1167		# Send login page
1168		#
1169
1170		set am [dnsconfig get "authmethod"]
1171		if {$am eq "casldap"} then {
1172		    #
1173		    # Check for CAS auth first
1174		    #
1175
1176		    set casurl [dnsconfig get "casurl"]
1177		    if {$casurl eq ""} then {
1178			d error [mc "Invalid CAS URL"]
1179		    }
1180		    set home [::webapp::myurl 1]
1181		    if {$home eq ""} then {
1182			d error [mc "Cannot get my own URL"]
1183		    }
1184		    set url "$casurl/login?service=$home/$libconf(next-login)"
1185		    ::webapp::redirect $url
1186		} else {
1187		    #
1188		    # Normal login page for other authentication methods
1189		    #
1190
1191		    # For the "logged as" message
1192		    set euid "-"
1193		    set uid  $euid
1194
1195		    #
1196		    # Send resulting page
1197		    #
1198
1199		    d urlset "%URLFORM%" $libconf(next-login) {}
1200		    d result $libconf(page-login) [list \
1201							[list %MESSAGE% ""] \
1202							[list %LOGIN%  ""] \
1203						    ]
1204		}
1205		exit 0
1206	    }
1207
1208	    #
1209	    # If we get there, page is restricted to authenticated users,
1210	    # and our user is authenticated
1211	    #
1212	}
1213
1214	#
1215	# If user is authenticated, check maintenance mode and
1216	# get user attributes
1217	#
1218
1219	if {$authenticated} then {
1220	    set uid $login
1221	    set euid $login
1222
1223	    #
1224	    # Maintenance mode : access is forbidden to all, except
1225	    # for users specified in ROOT pattern.
1226	    #
1227
1228	    set ftest [get-local-conf "nologinfile"]
1229	    set rootusers [get-local-conf "rootusers"]
1230	    if {! [catch [lindex $rootusers 0]]} then {
1231		$self error "Invalid 'rootusers' configuration parameter"
1232	    }
1233
1234	    if {[file exists $ftest]} then {
1235		if {$uid eq "" || ! ($uid in $rootusers)} then {
1236		    set fd [open $ftest "r"]
1237		    set msg [read $fd]
1238		    close $fd
1239		    $self error $msg
1240		}
1241	    }
1242
1243	    #
1244	    # Common initialization work
1245	    #
1246
1247	    set msg [init-common $selfns $dbfd $login "id" false tabuid]
1248	    if {$msg ne ""} then {
1249		$self error $msg
1250	    }
1251
1252	    set curmodule $module
1253
1254	} else {
1255	    set uid "-"
1256	    set euid "-"
1257	}
1258
1259	#
1260	# To help write HTML code
1261	#
1262
1263	::html create ::h
1264
1265	#
1266	# Add default parameters in form analysis
1267	# Default parameters are:
1268	#   l : language
1269	#   uid : login to be substituted
1270	#   nextprog : next action, after current travel
1271	#   nextargs : arguments of next action, after current travel
1272	#
1273
1274	lappend form {l 0 1}
1275	lappend form {uid 0 1}
1276	lappend form {nextprog 0 1}
1277	lappend form {nextargs 0 1}
1278
1279	#
1280	# Add dispatch criterions
1281	#
1282
1283	foreach f [lsort -unique $critform] {
1284	    lappend form [list $f 0 1]
1285	}
1286
1287	#
1288	# Get variables
1289	#
1290
1291	if {[llength [::webapp::get-data ftab $form]] == 0} then {
1292	    set msg [mc "Invalid input"]
1293	    if {$debug} then {
1294		append msg "\n$ftab(_error)"
1295	    }
1296	    $self error $msg
1297	}
1298
1299	#
1300	# Is a specific language required ?
1301	#
1302
1303	set l [string trim [lindex $ftab(l) 0]]
1304	if {$l ne ""} then {
1305	    $self locale $l
1306	}
1307
1308	#
1309	# Get next action
1310	#
1311
1312	set dnextprog [string trim [lindex $ftab(nextprog) 0]]
1313	set dnextargs [string trim [lindex $ftab(nextargs) 0]]
1314
1315	#
1316	# Set user capabilities
1317	#
1318
1319	set curcap	{anon}
1320	if {$authenticated} then {
1321	    #
1322	    # Perform user substitution (through the uid parameter)
1323	    #
1324
1325	    set nuid [string trim [lindex $ftab(uid) 0]]
1326	    if {$nuid ne "" && $tabuid(p_admin)} then {
1327		array set tabouid [array get tabuid]
1328		array unset tabuid
1329
1330		set uid $nuid
1331		set login $nuid
1332
1333		set n [read-user $dbfd $login tabuid msg]
1334		if {$n != 1} then {
1335		    $self error $msg
1336		}
1337		if {! $tabuid(present)} then {
1338		    $self error [mc "User '%s' not authorized" $login]
1339		}
1340
1341		u setlogin $login
1342	    }
1343
1344	    #
1345	    # Computes capabilites, given local installation and/or user rights
1346	    #
1347
1348	    lappend curcap "dns"
1349	    if {[dnsconfig get "topoactive"]} then {
1350		lappend curcap "topo"
1351	    }
1352	    if {[dnsconfig get "macactive"] && $tabuid(p_mac)} then {
1353		lappend curcap "mac"
1354	    }
1355	    if {$tabuid(p_genl)} then {
1356		lappend curcap "topogenl"
1357	    }
1358	    if {$tabuid(p_admin)} then {
1359		lappend curcap "admin"
1360	    }
1361	    if {[dnsconfig get "authmethod"] eq "pgsql"} then {
1362		lappend curcap "pgauth"
1363		set qlogin [::pgsql::quote $login]
1364		set sql "SELECT r.admin
1365				FROM pgauth.realm r, pgauth.member m
1366				WHERE r.realm = m.realm
1367				    AND login = '$qlogin'"
1368		pg_select $dbfd $sql tab {
1369		    if {$tab(admin)} then {
1370			lappend curcap "authadmin"
1371		    }
1372		}
1373	    }
1374	}
1375
1376	#
1377	# Remove additionnal default parameters
1378	# If they were staying in ftab, they could be caught by a
1379	# "hide all ftab paramaters" in a CGI script.
1380	#
1381
1382	foreach p {l uid nextprog nextargs} {
1383	    unset ftab($p)
1384	}
1385
1386
1387	#
1388	# Is this page an "admin" only page ?
1389	#
1390
1391	if {[llength $attr] > 0} then {
1392	    # XXX : for now, test only one attribute
1393	    if {! ($attr in $curcap)} then {
1394		$self error [mc "User '%s' not authorized" $login]
1395	    }
1396	}
1397
1398	#
1399	# Find script according to criterion
1400	#
1401
1402	set ok 0
1403	foreach cfs $critscript {
1404	    lassign $cfs crit form script
1405	    set ok 1
1406	    foreach {f re} $crit {
1407		set v [string trim [lindex $ftab($f) 0]]
1408		if {! [regexp "^$re$" $v]} then {
1409		    set ok 0
1410		    break
1411		}
1412	    }
1413	    if {$ok} {
1414		break
1415	    }
1416	}
1417
1418	if {! $ok} then {
1419	    $self error [mc "Cannot find registered CGI action"]
1420	}
1421
1422	#
1423	# Criterion ok
1424	# Get additional form variables and import them into current context
1425	#
1426
1427	if {[llength $form] > 0} then {
1428	    if {[llength [::webapp::get-data ftab $form]] == 0} then {
1429		set msg [mc "Invalid input"]
1430		if {$debug} then {
1431		    append msg "\n$ftab(_error)"
1432		}
1433		$self error $msg
1434	    }
1435	}
1436
1437	#
1438	# Prepare variable import
1439	#
1440
1441	foreach f [lsort -unique $critform] {
1442	    lappend form [list $f 0 1]
1443	}
1444	set script "::webapp::import-vars ftab \$form ; $script"
1445
1446	#
1447	# Execute script
1448	#
1449
1450	set r [catch $script msg]
1451	# r=0 (OK), 1 (ERROR), 2 (RETURN), 3 (BREAK) or 4 (CONTINUE)
1452	if {$r == 1} then {
1453	    global errorInfo
1454
1455	    ::webapp::cgi-err $errorInfo $debug
1456	}
1457
1458	return 0
1459    }
1460
1461    ###########################################################################
1462    # Initialize access to Netmagis, for an autonomous program (command
1463    # line utility, daemon, etc.)
1464    #
1465    # Input:
1466    #   - _dbfd : database handle, in return
1467    #   - argv0 : script argv0
1468    #   - usedefuser : use default user name if login is not found
1469    #   - _tabuid : array containing, in return, user's characteristics
1470    #		(login, password, lastname, firstname, mail, phone, fax,
1471    #			mobile, addr, idcor, idgrp, present)
1472    # Output:
1473    #   - return value: error message or empty string
1474    #   - object d : Netmagis context
1475    #   - object $ah : access to authentication base
1476    #
1477
1478    method init-script {_dbfd argv0 usedefuser _tabuid} {
1479	upvar $_dbfd dbfd
1480	upvar $_tabuid tabuid
1481
1482	set scriptmode "script"
1483	regsub {.*/} $argv0 {} argv0
1484	set scriptargv0 $argv0
1485
1486	#
1487	# Locale
1488	#
1489
1490	uplevel #0 mclocale
1491	uplevel #0 mcload [get-local-conf "msgsdir"]
1492
1493	#
1494	# Look for user's login
1495	#
1496
1497	set cmd [get-local-conf "whoami"]
1498	if {[catch {exec sh -c $cmd} msg]} then {
1499	    return "Cannot get login name ($msg)"
1500	}
1501	set login $msg
1502
1503	#
1504	# Database initialization
1505	#
1506
1507	set msg [init-database $selfns dbfd]
1508	if {$msg ne ""} then {
1509	    $self error $msg
1510	}
1511
1512	#
1513	# Common initialization work
1514	#
1515
1516	set msg [init-common $selfns $dbfd $login "anon" $usedefuser tabuid]
1517	if {$msg ne ""} then {
1518	    return $msg
1519	}
1520
1521	return ""
1522    }
1523
1524    ###########################################################################
1525    # Ends access to Netmagis (CGI script or autonomous program)
1526    #
1527    # Input:
1528    #   - none
1529    # Output:
1530    #   - return value: none
1531    #
1532
1533    method end {} {
1534	if {$db ne ""} then {
1535	    pg_disconnect $db
1536	}
1537    }
1538
1539
1540    method locale {{l {}}} {
1541	set locale "C"
1542	if {$l in $avlocale} then {
1543	    set locale $l
1544	}
1545
1546	uplevel #0 mclocale $locale
1547	uplevel #0 mcload [get-local-conf "msgsdir"]
1548
1549	return $locale
1550    }
1551
1552    ###########################################################################
1553    # Returns an error and properly close access to application (and database)
1554    #
1555    # Input:
1556    #   - msg : (translated) error message
1557    # Output:
1558    #   - return value: none (this method don't return)
1559    #
1560
1561    method error {msg} {
1562	switch $scriptmode {
1563	    cgi {
1564		set msg [::webapp::html-string $msg]
1565		regsub -all "\n" $msg "<br>" msg
1566		$self result $errorpage [list [list %MESSAGE% $msg]]
1567		exit 0
1568	    }
1569	    daemon -
1570	    script {
1571		puts stderr "$scriptargv0: $msg"
1572		$self end
1573		exit 1
1574	    }
1575	}
1576    }
1577
1578    ###########################################################################
1579    # Returns an error as an image and properly close access to application
1580    # (and database)
1581    #
1582    # Input:
1583    #   - msg : (translated) error message
1584    # Output:
1585    #   - return value: none (this method don't return)
1586    #
1587
1588    method errimg {msg} {
1589	switch $scriptmode {
1590	    cgi {
1591		::webapp::send png [errimg $msg]
1592		$self end
1593		exit 1
1594	    }
1595	    daemon -
1596	    default {
1597		# should not occur
1598		puts stderr "$scriptargv0: $msg"
1599		$self end
1600		exit 1
1601	    }
1602	}
1603    }
1604
1605    ###########################################################################
1606    # Sends a page and properly close access to application (and database)
1607    #
1608    # Input:
1609    #   - page : HTML or LaTeX page containing templates
1610    #   - lsubst : substitution list for template values
1611    # Output:
1612    #   - return value: none
1613    #
1614
1615    method result {page lsubst} {
1616	#
1617	# Define the output format from file extension
1618	#
1619
1620	switch -glob $page {
1621	    *.html { set fmt html }
1622	    *.tex { set fmt pdf }
1623	    default { set fmt "unknown" }
1624	}
1625
1626	#
1627	# Handle internationalized template files
1628	#
1629
1630	set found 0
1631	foreach l [concat [mcpreferences] "C"] {
1632	    set tdir [get-local-conf "templatedir"]
1633	    set file "$tdir/$l/$page"
1634	    if {[file exists $file]} then {
1635		set found 1
1636		break
1637	    }
1638	}
1639	if {! $found} then {
1640	    error "Template file '$page' not found in locale: [mcpreferences]"
1641	}
1642
1643	#
1644	# Add the "logged as" information
1645	#
1646
1647	set session {}
1648	if {$euid eq "-"} then {
1649	    # Not logged in
1650	    set m [mc "Log in"]
1651	    set url $sessionlinks(login)
1652	    append session [::webapp::helem "li" \
1653				[::webapp::helem "a" "$m" "href" $url] \
1654			    ]
1655	} else {
1656	    # Currently logged in
1657	    set m [mc "Logged as %s" $euid]
1658	    set url $sessionlinks(profile)
1659	    append session [::webapp::helem "li" \
1660				[::webapp::helem "a" "$m" "href" $url] \
1661			    ]
1662	    set m [mc "Log out"]
1663	    set url $sessionlinks(logout)
1664	    append session [::webapp::helem "li" \
1665				[::webapp::helem "a" "$m" "href" $url] \
1666			    ]
1667	}
1668	lappend lsubst [list %SESSION% $session]
1669
1670	#
1671	# Constitute the links menu if the database access is initialized
1672	#
1673
1674	if {$fmt eq "html"} then {
1675	    if {$db eq ""} then {
1676		set linksmenu ""
1677	    } else {
1678		set linksmenu [$self Get-links ":$curmodule"]
1679
1680		foreach l $avlocale {
1681		    if {$l ne $locale} then {
1682			set utab(L) [list $homepage(:$curmodule)]
1683			set utab(L:nextprog) ""
1684			set url [make-url utab "L" $uid $euid $l $blocale]
1685			append linksmenu [::webapp::helem "li" \
1686				    [::webapp::helem "a" "\[$l\]" "href" $url] \
1687				]
1688		    }
1689		}
1690	    }
1691
1692	    lappend lsubst [list %LINKS% $linksmenu]
1693
1694	    foreach s [$self urlsubst] {
1695		lappend lsubst $s
1696	    }
1697
1698	    lappend lsubst [list %VERSION% $version]
1699	}
1700
1701	#
1702	# Path to pdflatex
1703	#
1704
1705	if {$fmt eq "pdf"} then {
1706	    set path [get-local-conf "pdflatex"]
1707	    if {$path ne ""} then {
1708		::webapp::cmdpath "pdflatex" $path
1709	    }
1710
1711	    set pageformat [string tolower [::dnsconfig get "pageformat"]]
1712	    switch -- $pageformat {
1713		letter { set pageformat "letterpaper" }
1714		a4 -
1715		default { set pageformat "a4paper" }
1716	    }
1717	    lappend lsubst [list %PAGEFORMAT% $pageformat]
1718	}
1719
1720	#
1721	# Send resulting page
1722	#
1723
1724	::webapp::send $fmt [::webapp::file-subst $file $lsubst]
1725	$self end
1726    }
1727
1728    ###########################################################################
1729    # Get the next action (i.e. where we must come back after the current
1730    # travel)
1731    #
1732    # Input: none
1733    # Output:
1734    #   - return value: <nextprog> or <nextargs>, depending on method
1735    #
1736
1737    method nextprog {} {
1738	return $dnextprog
1739    }
1740
1741    method nextargs {} {
1742	return $dnextargs
1743    }
1744
1745    ###########################################################################
1746    # Get or set the effective login and idcor of the user
1747    #
1748    # Input:
1749    #   - if supplied: list {new effective login, new effective idcor} to set
1750    #		(use {- -1} for anonymous user)
1751    #   - if not supplied: just get effective login and idcor
1752    # Output:
1753    #   - return value: list {login idcor}
1754    #
1755
1756    method euid {{neweuid {}}} {
1757	if {$neweuid ne {}} then {
1758	    lassign $neweuid euid eidcor
1759	}
1760	return [list $euid $eidcor]
1761    }
1762
1763    ###########################################################################
1764    # Get or set the real login and idcor of the user
1765    #
1766    # Input:
1767    #   - if supplied: real login to set (use "-" for anonymous user)
1768    #   - if not supplied: just get login
1769    # Output:
1770    #   - return value: login
1771    #
1772
1773    method uid {{newuid {}}} {
1774	if {$newuid ne {}} then {
1775	    set uid $newuid
1776	}
1777	return $uid
1778    }
1779
1780
1781    ###########################################################################
1782    # URL framework
1783    #
1784
1785    method urlset {name path {largs {}}} {
1786	set urltab($name) [linsert $largs 0 $path]
1787	set urltab($name:nextprog) ""
1788    }
1789
1790    method urladd {name largs} {
1791	set url($name) [concat $url($name) $largs]
1792    }
1793
1794    method urlsetnext {name nextprog nextargs} {
1795	set urltab($name:nextprog) $nextprog
1796	set urltab($name:nextargs) $nextargs
1797    }
1798
1799    method urladdnext {name} {
1800	if {$dnextprog eq ""} then {
1801	    set urltab($name:nextprog) ""
1802	} else {
1803	    set urltab($name:nextprog) $dnextprog
1804	    set urltab($name:nextargs) $dnextargs
1805	}
1806    }
1807
1808    method urlsubst {} {
1809	set lsubst {}
1810	foreach name [array names urltab] {
1811	    if {! [string match "*:*" $name]} then {
1812		set url [$self urlget $name]
1813		lappend lsubst [list $name $url]
1814	    }
1815	}
1816	return $lsubst
1817    }
1818
1819    method urlget {name} {
1820	set path [lindex $urltab($name) 0]
1821	set largs [lreplace $urltab($name) 0 0]
1822	set url [make-url urltab $name $uid $euid $locale $blocale]
1823	return $url
1824    }
1825
1826
1827    ###########################################################################
1828    # Sets the context used for the links menu
1829    #
1830    # Input:
1831    #   - module : module name (see curmodule and links variables)
1832    # Output: none
1833    #
1834
1835    method module {module} {
1836	set idx ":$module"
1837	if {! [info exists links($idx)]} then {
1838	    # This is an internal error
1839	    error "'$module' is not a valid module"
1840	}
1841	set curmodule $module
1842    }
1843
1844    ###########################################################################
1845    # Write a line in the log system
1846    #
1847    # Input:
1848    #	- event : event name (examples : supprhost, suppralias etc.)
1849    #	- message : log message (example: parameters of the event)
1850    #	- date (optional) : event date (in seconds since epoch)
1851    #   - leuid (optional) : event owner
1852    #	- ip (optional) : IP address
1853    #
1854    # Output: none
1855    #
1856    # History :
1857    #   2007/10/?? : jean     : design
1858    #   2010/11/09 : pda      : dnscontext object and no more login parameter
1859    #   2015/01/14 : pda/jean : add optional parameters date, leuid, ip
1860    #
1861
1862    method writelog {event msg {date {}} {leuid {}} {ip {}}} {
1863	global env
1864
1865	if {$ip eq {}} then {
1866	    if {[info exists env(REMOTE_ADDR)]} then {
1867		set ip $env(REMOTE_ADDR)
1868	    } else {
1869		set ip ""
1870	    }
1871	}
1872
1873	if {$leuid eq {}} then {
1874	    set leuid $euid
1875	}
1876
1877	$log log $date $event $leuid $ip $msg
1878    }
1879
1880    #
1881    # Transaction processing
1882    #
1883
1884    method dblock {tablelist} {
1885	set msg ""
1886	if {! [::pgsql::lock $db $tablelist msg]} then {
1887	    if {[llength $tablelist] == 0} then {
1888		set tl [join $tablelist ", "]
1889		set msg [mc {Cannot lock table(s) %1$s: %2$s} $tl $msg]
1890	    } else {
1891		set msg [mc "Cannot lock database: %s" $msg]
1892	    }
1893	    if {$scriptmode eq "cgi"} then {
1894		$self error $msg
1895	    }
1896	}
1897	return $msg
1898    }
1899
1900    method dbcommit {op} {
1901	set msg ""
1902	if {! [::pgsql::unlock $db "commit" msg]} then {
1903	    set msg [$self dbabort $op $msg]
1904	}
1905	return $msg
1906    }
1907
1908    method dbabort {op msg} {
1909	::pgsql::unlock $db "abort" m
1910	set msg [mc {Cannot perform operation "%1$s": %2$s} $op $msg]
1911	if {$scriptmode eq "cgi"} then {
1912	    $self error $msg
1913	}
1914	return $msg
1915    }
1916
1917    method version {} {
1918	return $version
1919    }
1920}
1921
1922::netmagis create d
1923
1924##############################################################################
1925# Configuration parameters
1926##############################################################################
1927
1928#
1929# Configuration parameters class
1930#
1931# This class is a simple way to access to configuration parameters
1932# of the Netmagis application.
1933#
1934# Methods:
1935# - setdb dbfd
1936#	set the database handle used to access parameters
1937# - class
1938#	returns all known classes
1939# - desc class-or-key
1940#	returns the description associated with class or key
1941# - keys [ class ]
1942#	returns all keys associed with the class, or all known keys
1943# - keytype key
1944#	returns type of a given key, under the format {string|bool|text|menu x}
1945#	X is present only for the "menu" type.
1946# - keyhelp key
1947#	returns the help message associated with a key
1948# - get key
1949#	returns the value associated with a key
1950# - set key val
1951#	set the value associated with a key and returns an empty string or
1952#	an error message.
1953#
1954# History
1955#   2001/03/21 : pda      : design getconfig/setconfig
1956#   2010/10/25 : pda      : transform into a class
1957#   2010/12/04 : pda      : i18n
1958#   2012/10/27 : pda      : add read-only mode
1959#
1960
1961snit::type ::config {
1962    # database handle
1963    variable db ""
1964
1965    # configuration parameter specification
1966    # {{class class-spec} {class class-spec} ...}
1967    # class = class name
1968    # class-spec = {{key ro/rw type} {key ro/rw type} ...}
1969    variable configspec {
1970	{general
1971	    {datefmt rw {string}}
1972	    {dayfmt rw {string}}
1973	    {authmethod rw {menu {{pgsql Internal} {ldap {LDAP}} {casldap CAS}}}}
1974	    {authexpire rw {string}}
1975	    {authtoklen rw {string}}
1976	    {wtmpexpire rw {string}}
1977	    {failloginthreshold1 rw {string}}
1978	    {faillogindelay1 rw {string}}
1979	    {failloginthreshold2 rw {string}}
1980	    {faillogindelay2 rw {string}}
1981	    {failipthreshold1 rw {string}}
1982	    {failipdelay1 rw {string}}
1983	    {failipthreshold2 rw {string}}
1984	    {failipdelay2 rw {string}}
1985	    {pageformat rw {menu {{a4 A4} {letter Letter}}} }
1986	    {schemaversion ro {string}}
1987	}
1988	{dns
1989	    {defuser rw {string}}
1990	}
1991	{dhcp
1992	    {dhcpdefdomain rw {string}}
1993	    {dhcpdefdnslist rw {string}}
1994	    {default_lease_time rw {string}}
1995	    {max_lease_time rw {string}}
1996	    {min_lease_time rw {string}}
1997	}
1998	{topo
1999	    {topoactive rw {bool}}
2000	    {defdomain rw {string}}
2001	    {topofrom rw {string}}
2002	    {topoto rw {string}}
2003	    {topographddelay rw {string}}
2004	    {toposendddelay rw {string}}
2005	    {topomaxstatus rw {string}}
2006	    {sensorexpire rw {string}}
2007	    {modeqexpire rw {string}}
2008	    {ifchangeexpire rw {string}}
2009	    {fullrancidmin rw {string}}
2010	    {fullrancidmax rw {string}}
2011	}
2012	{mac
2013	    {macactive rw {bool}}
2014	}
2015	{authcas
2016	    {casurl rw {string}}
2017	}
2018	{authldap
2019	    {ldapurl rw {string}}
2020	    {ldapbinddn rw {string}}
2021	    {ldapbindpw rw {string}}
2022	    {ldapbasedn rw {string}}
2023	    {ldapsearchlogin rw {string}}
2024	    {ldapattrlogin rw {string}}
2025	    {ldapattrname rw {string}}
2026	    {ldapattrgivenname rw {string}}
2027	    {ldapattrmail rw {string}}
2028	    {ldapattrphone rw {string}}
2029	    {ldapattrmobile rw {string}}
2030	    {ldapattrfax rw {string}}
2031	    {ldapattraddr rw {string}}
2032	}
2033	{authpgsql
2034	    {authpgminpwlen rw {string}}
2035	    {authpgmaxpwlen rw {string}}
2036	    {authpgmailfrom rw {string}}
2037	    {authpgmailreplyto rw {string}}
2038	    {authpgmailcc rw {string}}
2039	    {authpgmailbcc rw {string}}
2040	    {authpgmailsubject rw {string}}
2041	    {authpgmailbody rw {text}}
2042	    {authpggroupes rw {string}}
2043	}
2044    }
2045
2046    #
2047    # Internal representation of parameter specification
2048    #
2049    # (class)			{<cl1> ... <cln>}
2050    # (class:<cl1>)		{<k1> ... <kn>}
2051    # (key:<k1>:type)		{string|bool|text|menu ...}
2052    # (key:<k1>:rw)		ro|rw
2053    #
2054
2055    variable internal -array {}
2056
2057    constructor {} {
2058	set internal(class) {}
2059	foreach class $configspec {
2060
2061	    set classname [lindex $class 0]
2062	    lappend internal(class) $classname
2063	    set internal(class:$classname) {}
2064
2065	    foreach key [lreplace $class 0 0] {
2066		lassign $key keyname keyrw keytype
2067
2068		lappend internal(class:$classname) $keyname
2069		set internal(key:$keyname:type) $keytype
2070		set internal(key:$keyname:rw) $keyrw
2071	    }
2072	}
2073    }
2074
2075    method setdb {dbfd} {
2076	set db $dbfd
2077    }
2078
2079    # returns all classes
2080    method class {} {
2081	return $internal(class)
2082    }
2083
2084    # returns textual description of the given class or key
2085    method desc {cork} {
2086	set r $cork
2087	if {[info exists internal(class:$cork)]} then {
2088	    set r [mc "cfg:$cork"]
2089	} elseif {[info exists internal(key:$cork:type)]} {
2090	    set r [mc "cfg:$cork:desc"]
2091	}
2092	return $r
2093    }
2094
2095    # returns all keys associated with a class (default  : all classes)
2096    method keys {{class {}}} {
2097	if {[llength $class] == 0} then {
2098	    set class $internal(class)
2099	}
2100	set lk {}
2101	foreach c $class {
2102	    set lk [concat $lk $internal(class:$c)]
2103	}
2104	return $lk
2105    }
2106
2107    # returns key rw/ro
2108    method keyrw {key} {
2109	set r ""
2110	if {[info exists internal(key:$key:rw)]} then {
2111	    set r $internal(key:$key:rw)
2112	}
2113	return $r
2114    }
2115
2116    # returns key type
2117    method keytype {key} {
2118	set r ""
2119	if {[info exists internal(key:$key:type)]} then {
2120	    set r $internal(key:$key:type)
2121	}
2122	return $r
2123    }
2124
2125    # returns key help
2126    method keyhelp {key} {
2127	set r $key
2128	if {[info exists internal(key:$key:type)]} then {
2129	    set r [mc "cfg:$key:help"]
2130	}
2131	return $r
2132    }
2133
2134    # returns key value
2135    method get {key} {
2136	if {[info exists internal(key:$key:type)]} then {
2137	    set found 0
2138	    pg_select $db "SELECT * FROM global.config WHERE key = '$key'" tab {
2139		set val $tab(value)
2140		set found 1
2141	    }
2142	    if {! $found} then {
2143		switch $internal(key:$key:type) {
2144		    string	{ set val "" }
2145		    bool	{ set val 0 }
2146		    set text	{ set val "" }
2147		    set menu	{ set val "" }
2148		    default	{ set val "type unknown" }
2149		}
2150	    }
2151	} else {
2152	    error [mc "Unknown configuration key '%s'" $key]
2153	}
2154	return $val
2155    }
2156
2157    # set key value
2158    # returns empty string if ok, or an error message
2159    method set {key val} {
2160	if {[info exists internal(key:$key:rw)]} then {
2161	    if {$internal(key:$key:rw) eq "rw"} then {
2162		set r ""
2163		set k [::pgsql::quote $key]
2164		set sql "DELETE FROM global.config WHERE key = '$k'"
2165		if {[::pgsql::execsql $db $sql msg]} then {
2166		    set v [::pgsql::quote $val]
2167		    set sql "INSERT INTO global.config (key, value)
2168		    				VALUES ('$k', '$v')"
2169		    if {! [::pgsql::execsql $db $sql msg]} then {
2170			set r [mc {Cannot set key '%1$s' to '%2$s': %3$s} $key $val $msg]
2171		    }
2172		} else {
2173		    set r [mc {Cannot fetch key '%1$s': %2$s} $key $msg]
2174		}
2175	    } else {
2176		set r [mc {Cannot modify read-only key '%s'} $key]
2177	    }
2178	} else {
2179	    error [mc "Unknown configuration key '%s'" $key]
2180	}
2181
2182	return $r
2183    }
2184}
2185
2186##############################################################################
2187# User characteristics
2188##############################################################################
2189
2190#
2191# Netmagis user characteristics class
2192#
2193# This class stores all informations related to current Netmagis user
2194#
2195# Methods:
2196# - setdb dbfd
2197#	set the database handle used to access parameters
2198# - setlogin login
2199#	set the login name
2200#
2201# ....
2202#
2203# - viewname id
2204#	returns view name associated to view id (or empty string if error)
2205# - viewid name
2206#	returns view id associated to view name (or -1 if error)
2207# - myviewids
2208#	get all authorized view ids
2209# - isallowedview id
2210#	check if a view is authorized (1 if ok, 0 if not)
2211#
2212# - domainname id
2213#	returns domain name associated to domain id (or empty string if error)
2214# - domainid name
2215#	returns domain id associated to domain name (or -1 if error)
2216# - myiddom
2217#	get all authorized domain ids
2218# - isalloweddom id
2219#	check if a domain is authorized (1 if ok, 0 if not)
2220#
2221# History
2222#   2012/10/31 : pda/jean : design
2223#
2224
2225snit::type ::nmuser {
2226    # database handle
2227    variable db ""
2228    # login of user
2229    variable login ""
2230
2231    # Group management
2232    # Group information is loaded
2233    variable groupsloaded 0
2234    # allgroups(id:<id>)=name
2235    # allgroups(name:<name>)=id
2236    variable allgroups -array {}
2237
2238    # View management
2239    # view information is loaded
2240    variable viewsloaded 0
2241    # allviews(id:<id>)=name
2242    # allviews(name:<name>)=id
2243    variable allviews -array {}
2244    # authviews(<id>)=1
2245    variable authviews -array {}
2246    # myviewids : sorted list of views
2247    variable myviewids {}
2248
2249    # Domain management
2250    # domain information is loaded
2251    variable domainloaded 0
2252    # alldom(id:<id>)=name
2253    # alldom(name:<name>)=id
2254    variable alldom -array {}
2255    # authdom(<id>)=1
2256    variable authdom -array {}
2257    # myiddoms : sorted list of domains
2258    variable myiddom {}
2259
2260    method setdb {dbfd} {
2261	set db $dbfd
2262    }
2263
2264    method setlogin {newlogin} {
2265	if {$login ne $newlogin} then {
2266	    set viewsisloaded 0
2267	}
2268	set login $newlogin
2269    }
2270
2271
2272    #######################################################################
2273    # Group management
2274    #######################################################################
2275
2276    proc load-groups {selfns} {
2277	array unset allgroups
2278
2279	set sql "SELECT * FROM global.nmgroup"
2280	pg_select $db $sql tab {
2281	    set idgrp $tab(idgrp)
2282	    set name  $tab(name)
2283	    set allgroups(id:$idgrp) $name
2284	    set allgroups(name:$name) $idgrp
2285	}
2286	set groupsloaded 1
2287    }
2288
2289    method groupname {id} {
2290	if {! $groupsloaded} then {
2291	    load-groups $selfns
2292	}
2293	set r -1
2294	if {[info exists allgroups(id:$id)]} then {
2295	    set r $allgroups(id:$id)
2296	}
2297	return $r
2298    }
2299
2300    method groupid {name} {
2301	if {! $groupsloaded} then {
2302	    load-groups $selfns
2303	}
2304	set r ""
2305	if {[info exists allgroups(name:$name)]} then {
2306	    set r $allgroups(name:$name)
2307	}
2308	return $r
2309    }
2310
2311    #######################################################################
2312    # View management
2313    #######################################################################
2314
2315    proc load-views {selfns} {
2316	array unset allviews
2317	array unset authviews
2318	set myviewids {}
2319
2320	set sql "SELECT * FROM dns.view"
2321	pg_select $db $sql tab {
2322	    set idview $tab(idview)
2323	    set name   $tab(name)
2324	    set allviews(id:$idview) $name
2325	    set allviews(name:$name) $idview
2326	}
2327
2328	set qlogin [::pgsql::quote $login]
2329	set sql "SELECT p.idview
2330			FROM dns.p_view p, dns.view v, global.nmuser u
2331			WHERE p.idgrp = u.idgrp
2332			    AND p.idview = v.idview
2333			    AND u.login = '$qlogin'
2334			ORDER BY p.sort ASC, v.name ASC"
2335	pg_select $db $sql tab {
2336	    set idview $tab(idview)
2337	    set authviews($idview) 1
2338	    lappend myviewids $tab(idview)
2339	}
2340
2341	set viewsloaded 1
2342    }
2343
2344    method viewname {id} {
2345	if {! $viewsloaded} then {
2346	    load-views $selfns
2347	}
2348	set r -1
2349	if {[info exists allviews(id:$id)]} then {
2350	    set r $allviews(id:$id)
2351	}
2352	return $r
2353    }
2354
2355    method viewid {name} {
2356	if {! $viewsloaded} then {
2357	    load-views $selfns
2358	}
2359	set r ""
2360	if {[info exists allviews(name:$name)]} then {
2361	    set r $allviews(name:$name)
2362	}
2363	return $r
2364    }
2365
2366    method myviewids {} {
2367	if {! $viewsloaded} then {
2368	    load-views $selfns
2369	}
2370	return $myviewids
2371    }
2372
2373    method isallowedview {id} {
2374	if {! $viewsloaded} then {
2375	    load-views $selfns
2376	}
2377	return [info exists authviews($id)]
2378    }
2379
2380    #######################################################################
2381    # Domain management
2382    #######################################################################
2383
2384    proc load-domains {selfns} {
2385	array unset alldom
2386	array unset authdom
2387	set myiddom {}
2388
2389	set sql "SELECT * FROM dns.domain"
2390	pg_select $db $sql tab {
2391	    set iddom $tab(iddom)
2392	    set name   $tab(name)
2393	    set alldom(id:$iddom) $name
2394	    set alldom(name:$name) $iddom
2395	}
2396
2397	set qlogin [::pgsql::quote $login]
2398	set sql "SELECT p.iddom
2399			FROM dns.p_dom p, dns.domain d, global.nmuser u
2400			WHERE p.idgrp = u.idgrp
2401			    AND p.iddom = d.iddom
2402			    AND u.login = '$qlogin'
2403			ORDER BY p.sort ASC, d.name ASC"
2404	pg_select $db $sql tab {
2405	    set iddom $tab(iddom)
2406	    set authdom($iddom) 1
2407	    lappend myiddom $tab(iddom)
2408	}
2409
2410	set domainloaded 1
2411    }
2412
2413    method domainname {id} {
2414	if {! $domainloaded} then {
2415	    load-domains $selfns
2416	}
2417	set r -1
2418	if {[info exists alldom(id:$id)]} then {
2419	    set r $alldom(id:$id)
2420	}
2421	return $r
2422    }
2423
2424    method domainid {name} {
2425	if {! $domainloaded} then {
2426	    load-domains $selfns
2427	}
2428	set r ""
2429	if {[info exists alldom(name:$name)]} then {
2430	    set r $alldom(name:$name)
2431	}
2432	return $r
2433    }
2434
2435    method myiddom {} {
2436	if {! $domainloaded} then {
2437	    load-domains $selfns
2438	}
2439	return $myiddom
2440    }
2441
2442    method isalloweddom {id} {
2443	if {! $domainloaded} then {
2444	    load-domains $selfns
2445	}
2446	return [info exists authdom($id)]
2447    }
2448
2449}
2450
2451##############################################################################
2452# File installation class
2453##############################################################################
2454
2455#
2456# File installation class
2457#
2458# This class is meant to simplify installation of new files in tree
2459# hierarchy.
2460#
2461# When a file is added, its contents are written in a ".new" file and
2462# the name is queued in internal instance variable fileq.
2463# When a commit is requested, all original files are renamed into ".old"
2464# files and ".new" file replace original files.
2465# When an abort is requested, all ".new" files are removed.
2466#
2467# Methods:
2468# - init
2469#	reset a new file list
2470# - add filename filecontent
2471#	add a new file based on its contents (as a textual value)
2472#	returns empty string if succeeds
2473# - abort
2474#	reset new files
2475# - commit
2476#	apply modifications
2477#	returns empty string if succeeds
2478# - uncommit
2479#	undo previous commit
2480#	returns empty string if succeeds
2481#
2482# History
2483#   2011/06/05 : pda      : design
2484#
2485
2486snit::type ::fileinst {
2487    # file queue
2488    variable fileq {}
2489
2490    # state
2491    variable state "init"
2492
2493    # reset queue to empty state
2494    method init {} {
2495	set fileq {}
2496    }
2497
2498    # add a file contents into the queue
2499    method add {name contents} {
2500	if {$state eq "init" || $state eq "nonempty"} then {
2501	    set nf "$name.new"
2502	    catch {file delete -force $nf}
2503	    if {! [catch {set fd [open "$nf" "w"]} err]} then {
2504		puts -nonewline $fd $contents
2505		if {! [catch {close $fd} err]} then {
2506		    lappend fileq $name
2507		    set err ""
2508		}
2509	    }
2510	    set state "nonempty"
2511	} else {
2512	    set err "cannot add file: state != 'init' && state != 'nonempty'"
2513	}
2514	return $err
2515    }
2516
2517    # commit new files
2518    method commit {} {
2519	set err ""
2520	if {$state eq "init" || $state eq "nonempty"} then {
2521
2522	    # we use a "for" loop instead of a "foreach" since the index i
2523	    # will be used if anything goes wrong
2524	    set n [llength $fileq]
2525	    for {set i 0} {$i < $n} {incr i} {
2526		set f [lindex $fileq $i]
2527		set nf "$f.new"
2528		set of "$f.old"
2529
2530		# make a backup of original file if it exists
2531		catch {file delete -force $of}
2532		if {[file exists $f]} then {
2533		    if {[catch {file rename -force $f $of} msg]} then {
2534			set err "cannot rename $f to $of\n$msg"
2535			break
2536		    }
2537		}
2538
2539		# install new file
2540		if {[catch {file rename $nf $f} msg]} then {
2541		    set err "cannot rename $nf to $f\n$msg"
2542		    break
2543		}
2544	    }
2545
2546	    if {$err eq ""} then {
2547		set state "commit"
2548	    } else {
2549		for {set j 0} {$j <= $i} {incr j} {
2550		    set f [lindex $fileq $j]
2551		    set nf "$f.new"
2552		    set of "$f.old"
2553
2554		    if {! [file exists $nf]} then {
2555			catch {file rename -force $f $nf}
2556		    }
2557
2558		    if {[file exists $of]} then {
2559			catch {file rename -force $of $f}
2560		    }
2561		}
2562	    }
2563	} else {
2564	    set err "cannot add file: state != 'init' && state != 'nonempty'"
2565	}
2566
2567	return $err
2568    }
2569
2570    # undo previous commit
2571    method uncommit {} {
2572	if {$state eq "commit"} then {
2573	    set err ""
2574	    set n [llength $fileq]
2575	    for {set i 0} {$i < $n} {incr i} {
2576		set f [lindex $fileq $i]
2577		set nf "$f.new"
2578		set of "$f.old"
2579
2580		if {[catch {file rename -force $f $nf} msg]} then {
2581		    append err "cannot rename $f to $nf\n$msg\n"
2582		} else {
2583		    if {[file exists $of]} then {
2584			if {[catch {file rename -force $of $f} msg]} then {
2585			    append err "cannot rename $of to $f\n$msg\n"
2586			}
2587		    }
2588		}
2589	    }
2590	} else {
2591	    set err "cannot commit: state != 'commit'"
2592	}
2593	return $err
2594    }
2595
2596    # abort new files
2597    method abort {} {
2598	foreach f $fileq {
2599	    catch {file delete -force "$f.new"}
2600	}
2601	set fileq {}
2602    }
2603}
2604
2605#
2606# Compare old file contents with new contents as a variable
2607#
2608# Input:
2609#   - parameters
2610#	- file: name of file
2611#	- text: new file content
2612#	- _errmsg: variable containing error message in return
2613# Output:
2614#   - return value: -1 (error), 0 (no change), or 1 (change)
2615#   - variable _errmsg: error message, if return value = -1
2616#
2617# History
2618#   2004/03/09 : pda/jean : design
2619#   2011/05/14 : pda      : use configuration variables
2620#   2011/05/22 : pda      : make it simpler
2621#
2622
2623proc compare-file-with-text {file text _errmsg} {
2624    upvar $_errmsg errmsg
2625
2626    set r 1
2627    if {[file exists $file]} then {
2628	if {[catch {set fd [open $file "r"]} errmsg]} then {
2629	    set r -1
2630	} else {
2631	    set old [read $fd]
2632	    close $fd
2633
2634	    if {$old eq $text} then {
2635		set r 0
2636	    }
2637	}
2638    }
2639
2640    return $r
2641}
2642
2643#
2644# Show difference between old file and new contents
2645#
2646# Input:
2647#   - parameters
2648#	- fd : file descriptor
2649#	- cmd: diff command
2650#	- file: name of file
2651#	- text: new file content
2652#	- _errmsg: variable containing error message in return
2653# Output:
2654#   - return value: 1 (ok) or 0 (error)
2655#   - variable _errmsg: error message, if return value = 0
2656#
2657# History
2658#   2011/05/22 : pda      : specification
2659#   2011/06/10 : pda      : add fd parameter
2660#   2011/06/10 : pda      : add special case for non-existant file
2661#
2662
2663proc show-diff-file-text {fd cmd file text} {
2664    if {! [file exists $file]} then {
2665	set file "/dev/null"
2666    }
2667    set c [format $cmd $file]
2668    append c "|| exit 0"
2669    catch {exec sh -c $c << $text} r
2670    puts $fd $r
2671}
2672
2673##############################################################################
2674# Graphviz graphs
2675##############################################################################
2676
2677#
2678# Graph generation class with Graphviz
2679#
2680# This class is a simple way to generate a Netmagis graph.
2681#
2682# Methods:
2683# - reset
2684#	reset graph parameters
2685#	set the output format for the graph
2686# - title <string>
2687#	set graph title (default: empty string, hence no title)
2688# - node <nodename> { <attr> ... } (with <attr> ::= "name=value")
2689#	set a node
2690# - link <nodename> <nodename> { <attr> ... }
2691#	mark a link between nodes
2692# - graphviz <png|pdf> <engine> <dot path> <ps2pdf path>
2693#	calls graphviz on the current graph and returns 1 if success
2694#	and 0 if error.
2695# - error
2696#	returns error message from graphviz call (if graphviz method returned 0)
2697# - output
2698#	returns generated graph (if graphviz method returned 1)
2699#
2700# History
2701#   2011/12/29 : pda      : design
2702#   2012/01/18 : pda      : only one dot command for all layout engines
2703#
2704
2705snit::type ::gvgraph {
2706
2707    variable title  ""
2708    variable nnodes 0
2709    variable nodesandlinks {}
2710    variable error ""
2711    variable output ""
2712
2713    # graph skeleton
2714    #	%1$s : nodes and links
2715    #	%2$s : graph title
2716    #	%3$s : layout engine (dot or neato)
2717    #	%4$s : page & size attributes (meaningful only for PDF graphs)
2718    variable skeleton -array {
2719	map {
2720	    graph g {
2721		layout = %3$s;
2722		charset = "UTF-8";
2723		fontsize = 14;
2724		fontname = Helvetica;
2725		margin = .3;
2726		center = true;
2727		orientation = portrait;
2728		maxiter = 1000 ;
2729		node [fontname=Helvetica,fontsize=10, color=grey];
2730		edge [fontname=Helvetica,fontsize=8, len=1.4, labelfontname=Helvetica, labelfontsize=6, color=grey];
2731		overlap = false;
2732		spline = true;
2733		%1$s
2734		%2$s
2735	    }
2736	}
2737	png {
2738	    graph g {
2739		layout = %3$s;
2740		charset = "UTF-8";
2741		fontsize = 14;
2742		fontname = Helvetica;
2743		margin = .3;
2744		center = true;
2745		orientation = portrait;
2746		maxiter = 1000 ;
2747		node [fontname=Helvetica,fontsize=10, color=grey];
2748		edge [fontname=Helvetica,fontsize=8, len=1.4, labelfontname=Helvetica, labelfontsize=6, color=grey];
2749		overlap = false;
2750		spline = true;
2751		%1$s
2752		%2$s
2753	    }
2754	}
2755	pdf {
2756	    graph g {
2757		layout = %3$s;
2758		charset = "UTF-8";
2759		fontsize = 14;
2760		fontname = Helvetica;
2761		margin = .3;
2762		center = true;
2763		%4$s
2764		orientation = landscape;
2765		maxiter = 1000 ;
2766		node [fontname=Helvetica,fontsize=10, color=grey];
2767		edge [fontname=Helvetica,fontsize=8, len=1.4, labelfontname=Helvetica, labelfontsize=6, color=grey];
2768		overlap = false;
2769		spline = true;
2770		%1$s
2771		%2$s
2772	    }
2773	}
2774    }
2775
2776    # %1$s : path to the dot cmd
2777    # %2$s : path to the ps2pdf cmd
2778    # %3$s : dot file name
2779    # %4$s : error file name
2780    variable gvcmd -array {
2781	map {|%1$s -Tcmapx %3$s 2>%4$s}
2782	png {|%1$s -Tpng %3$s 2>%4$s}
2783	pdf {|%1$s -Tps %3$s 2>%4$s | %2$s - -}
2784    }
2785
2786    # reset graph to initial state
2787    method reset {} {
2788	set title ""
2789	set nodesandlinks {}
2790	set nnodes 0
2791	set error ""
2792	set output ""
2793    }
2794
2795    # returns an error message if format is not valid
2796    method check-format {format} {
2797	if {! [info exists skeleton($format)]} then {
2798	    return [format [mc "Invalid format '%s'"] $format]
2799	}
2800	return ""
2801
2802    }
2803
2804    # set title of the graph (empty string means no title)
2805    method title {t} {
2806	set title $t
2807    }
2808
2809    # add a node to the graph
2810    method node {name attrlist} {
2811	set attr [join $attrlist ","]
2812	lappend nodesandlinks "\"$name\" \[$attr\];"
2813    }
2814
2815    # add a link to the graph
2816    method link {n1 n2 attrlist} {
2817	set attr [join $attrlist ","]
2818	lappend nodesandlinks "\"$n1\" -- \"$n2\" \[$attr\];"
2819    }
2820
2821    # calls graphviz and returns 1 if no error. Caller must use
2822    # error and output methods to get the result.
2823    method graphviz {format engine dotcmd ps2pdfcmd} {
2824	#
2825	# Barks if format is invalid
2826	#
2827	set error [$self check-format $format]
2828	if {$error ne ""} then {
2829	    return 0
2830	}
2831
2832	# temporary file
2833	set tmp "/tmp/gv-[pid]"
2834
2835	#
2836	# Builds the gv (dot) file for the graph
2837	#
2838
2839	# title
2840	if {$title eq ""} then {
2841	    set t ""
2842	} else {
2843	    set t "label = \"$title\";\n"
2844	}
2845
2846	# page format
2847	set pageformat [string tolower [::dnsconfig get "pageformat"]]
2848	switch -- $pageformat {
2849	    letter { set paper {page = "8.5,11"; size = "10.3,7.8";} }
2850	    a4 -
2851	    default { set paper {page = "8.26,11.69"; size = "11,7.6";} }
2852	}
2853
2854	set dot [format $skeleton($format) \
2855			[join $nodesandlinks "\n"] \
2856			$t \
2857			$engine \
2858			$paper
2859		    ]
2860
2861	set fd [open "$tmp.gv" "w"]
2862	fconfigure $fd -encoding utf-8
2863	puts $fd $dot
2864	close $fd
2865
2866	#
2867	# Calls graphviz
2868	#
2869
2870	set cmd [format $gvcmd($format) $dotcmd $ps2pdfcmd $tmp.gv $tmp.err]
2871
2872	if {[catch {open $cmd "r"} fd]} then {
2873	    set error [format [mc "Error generating graph: %s"] $fd]
2874	    set r 0
2875	} else {
2876	    fconfigure $fd -translation binary
2877	    set output [read $fd]
2878	    if {[catch {close $fd} error]} then {
2879		set r 0
2880	    } else {
2881		set r 1
2882	    }
2883	}
2884
2885	#
2886	# Has an error occurred?
2887	#
2888
2889	if {$r == 0} then {
2890	    if {! [catch {open $tmp.err "r"} fderr]} then {
2891		append error "\n"
2892		append error [read $fderr]
2893		close $fderr
2894	    }
2895	}
2896
2897	file delete -force -- $tmp.gv $tmp.err
2898
2899	#
2900	# Returns appropriate code : 1 (success) or 0 (failure)
2901	#
2902
2903	return $r
2904    }
2905
2906    # returns the error message resulting from the previous graphviz invocation
2907    method error {} {
2908	return $error
2909    }
2910
2911    # returns the output resulting from the previous graphviz invocation
2912    method output {} {
2913	return $output
2914    }
2915}
2916
2917##############################################################################
2918# Generates an error message as a bitmap image
2919##############################################################################
2920
2921proc errimg {msg} {
2922    set gv [::gvgraph %AUTO%]
2923    $gv node "ERROR $msg" {shape=rectangle color=red style=filled}
2924    if {[$gv graphviz "png" "dot" [get-local-conf "dot"] ""]} then {
2925	set img [$gv output]
2926    } else {
2927	# ouch! This is a text...
2928	set img [$gv error]
2929    }
2930    $gv destroy
2931    return $img
2932}
2933
2934##############################################################################
2935# Get graphviz node attributes from a regular expression
2936##############################################################################
2937
2938#
2939# Initialize data structure for dotattr-match-get
2940#
2941# Input:
2942#   - parameters:
2943#	- dbfd : database handle
2944#	- type : 2 or 3, depending upon the type of graph
2945#	- _tabdot : empty data structure for pattern matching
2946# Output:
2947#   - return value: none
2948#   - parameter _tabdot:
2949#	tabdot(_) {<re> ... <re>}		(in matching order)
2950#	tabdot(<re>) <attributes>
2951#
2952# History
2953#   2012/01/09 : pda      : design
2954#
2955
2956proc dotattr-match-init {dbfd type _tabdot} {
2957    upvar $_tabdot tabdot
2958
2959    catch {unset tabdot}
2960    set sql "SELECT regexp, gvattr FROM topo.dotattr
2961				WHERE type = $type ORDER BY rank"
2962    set tabdot(_) {}
2963    pg_select $dbfd $sql tab {
2964	set re $tab(regexp)
2965	set at $tab(gvattr)
2966	lappend tabdot(_) $re
2967	set tabdot($re) $at
2968    }
2969}
2970
2971#
2972# Match a string against regexp in order to find graphviz node attributes
2973#
2974# Input:
2975#   - parameters:
2976#	- string : string to match (x/y for L2 graph, x for L3 graph)
2977#	- _tabdot : array initialized by dotattr-match-init
2978# Output:
2979#   - return value: graphviz attributes
2980#
2981# History
2982#   2012/01/09 : pda      : design
2983#
2984
2985proc dotattr-match-get {str _tabdot} {
2986    upvar $_tabdot tabdot
2987
2988    set attr {}
2989    foreach re $tabdot(_) {
2990	if {[regexp $re $str]} then {
2991	    set attr $tabdot($re)
2992	    break
2993	}
2994    }
2995    return $attr
2996}
2997
2998
2999##############################################################################
3000# HTML mask/unmask class
3001##############################################################################
3002
3003#
3004# HTML class
3005#
3006# This class provides methods to simplify HTML writing
3007#
3008# Methods:
3009# - reset
3010#	reset HTML parameters
3011# - mask-next
3012#	increment mask counter
3013# - mask-link <text>
3014#	HTML code for the link to unmask/mask text
3015# - mask-text <text>
3016#	HTML code to mask the text (such as it may be unmasked by the link)
3017#
3018# Note: this class needs an "invdisp" Javascript function in the
3019#   HTML page
3020#
3021# History
3022#   2012/12/19 : pda/jean : design
3023#
3024
3025snit::type ::html {
3026
3027    variable mask_counter 0
3028
3029    # reset to initial state
3030    method reset {} {
3031	set mask_counter 0
3032    }
3033
3034    # increment mask counter
3035    method mask-next {} {
3036	incr mask_counter
3037    }
3038
3039    # HTML code for the link to unmask/mask text
3040    method mask-link {text} {
3041	return [::webapp::helem "a" $text \
3042				"href" "#" \
3043				"onclick" "invdisp('hv$mask_counter')" \
3044				]
3045    }
3046
3047    # HTML code to mask the text (such as it may be unmasked by the link)
3048    method mask-text {text} {
3049	return [::webapp::helem "div" $text \
3050				"id" "hv$mask_counter" \
3051				"style" "display:none" \
3052				]
3053    }
3054}
3055
3056##############################################################################
3057# Cosmetic
3058##############################################################################
3059
3060#
3061# Format a string such as it correctly displays in an array
3062#
3063# Input:
3064#   - parameters:
3065#	- string : string to display
3066# Output:
3067#   - return value: same string, with "&nbsp;" if empty
3068#
3069# History
3070#   2002/05/23 : pda      : design
3071#   2010/11/29 : pda      : i18n
3072#
3073
3074proc html-tab-string {string} {
3075    set v [::webapp::html-string $string]
3076    if {[string trim $v] eq ""} then {
3077	set v "&nbsp;"
3078    }
3079    return $v
3080}
3081
3082#
3083# Display user data in an HTML array
3084#
3085# Input:
3086#   - parameters:
3087#	- tabuid : array containing user's attributes
3088#   - global variables :
3089#	- libconf(tabuser) : array specification
3090# Output:
3091#   - return value: HTML code ready to use
3092#
3093# History
3094#   2002/07/25 : pda      : design
3095#   2003/05/13 : pda/jean : use tabuid
3096#   2010/11/29 : pda      : i18n
3097#
3098
3099proc display-user {_tabuid} {
3100    global libconf
3101    upvar $_tabuid tabuid
3102
3103    set lines {}
3104    lappend lines [list Normal [mc "User"] "$tabuid(lastname) $tabuid(firstname)"]
3105    foreach {txt key} {
3106			Login	login
3107			Mail	mail
3108			Phone	phone
3109			Mobile	mobile
3110			Fax	fax
3111			Address	addr
3112		    } {
3113	lappend lines [list Normal [mc $txt] $tabuid($key)]
3114    }
3115    return [::arrgen::output "html" $libconf(tabuser) $lines]
3116}
3117
3118#
3119# Display last connections from one user or from all users
3120#
3121# Input:
3122#   - parameters:
3123#	- dbfd : database handle
3124#	- idcor: id of user, or -1 for all users
3125# Output:
3126#   - return value: HTML code ready to use
3127#
3128# History
3129#   2015/06/05 : pda/jean : design
3130#
3131
3132proc display-last-connections {dbfd idcor} {
3133    global libconf
3134
3135    if {$idcor == -1} then {
3136	set where ""
3137	set limit ""
3138    } else {
3139	set where "AND w.idcor = $idcor"
3140	set limit "LIMIT 10"
3141    }
3142
3143    set sql "SELECT u.login, w.start, w.ip, w.stop, w.stopreason
3144		    FROM global.wtmp w, global.nmuser u
3145		    WHERE w.idcor = u.idcor $where
3146		    ORDER BY start DESC
3147		    $limit"
3148    set lines {}
3149    lappend lines [list "Title" [mc "Login"] \
3150    				[mc "Login time"] \
3151				[mc "IP address"] \
3152				[mc "Logout time"] \
3153				[mc "Logout reason"] \
3154			    ]
3155    pg_select $dbfd $sql tab {
3156	lappend lines [list "Normal" \
3157				$tab(login) \
3158				$tab(start) \
3159				$tab(ip) \
3160				$tab(stop) \
3161				$tab(stopreason) \
3162			    ]
3163    }
3164    return [::arrgen::output "html" $libconf(tabwtmp) $lines]
3165}
3166
3167#
3168# Display group data in an HTML array
3169#
3170# Input:
3171#   - parameters:
3172#	- dbfd : database handle
3173#	- idgrp : group id
3174#   - global variables libconf(tab*) : array specification
3175# Output:
3176#   - return value: list of 9 HTML strings
3177#
3178# History
3179#   2002/05/23 : pda/jean : specification et design
3180#   2005/04/06 : pda      : add DHCP profiles
3181#   2007/10/23 : pda/jean : add users
3182#   2008/07/23 : pda/jean : add group permissions
3183#   2010/10/31 : pda      : add ttl permission
3184#   2010/11/03 : pda/jean : add equipment permissions
3185#   2010/11/30 : pda/jean : add mac permissions
3186#   2010/12/01 : pda      : i18n
3187#   2012/01/21 : jean     : add generate link number permissions
3188#   2012/10/08 : pda/jean : add views
3189#
3190
3191proc display-group {dbfd idgrp} {
3192    global libconf
3193
3194    #
3195    # Get specific permissions: p_admin, p_smtp, p_ttl, p_mac and p_genl
3196    #
3197
3198    set lines {}
3199    set sql "SELECT p_admin, p_smtp, p_ttl, p_mac, p_genl
3200			FROM global.nmgroup
3201			WHERE idgrp = $idgrp"
3202    pg_select $dbfd $sql tab {
3203	if {$tab(p_admin)} then {
3204	    set p_admin [mc "yes"]
3205	} else {
3206	    set p_admin [mc "no"]
3207	}
3208	if {$tab(p_smtp)} then {
3209	    set p_smtp [mc "yes"]
3210	} else {
3211	    set p_smtp [mc "no"]
3212	}
3213	if {$tab(p_ttl)} then {
3214	    set p_ttl [mc "yes"]
3215	} else {
3216	    set p_ttl [mc "no"]
3217	}
3218	if {$tab(p_mac)} then {
3219	    set p_mac [mc "yes"]
3220	} else {
3221	    set p_mac [mc "no"]
3222	}
3223	if {$tab(p_genl)} then {
3224	    set p_genl [mc "yes"]
3225	} else {
3226	    set p_genl [mc "no"]
3227	}
3228	lappend lines [list Normal [mc "Netmagis administration"] $p_admin]
3229	lappend lines [list Normal [mc "SMTP authorization management"] $p_smtp]
3230	lappend lines [list Normal [mc "TTL management"] $p_ttl]
3231	lappend lines [list Normal [mc "MAC module access"] $p_mac]
3232	lappend lines [list Normal [mc "Generate link numbers"] $p_genl]
3233    }
3234    if {[llength $lines] > 0} then {
3235	set tabperm [::arrgen::output "html" $libconf(tabperm) $lines]
3236    } else {
3237	set tabperm [mc "Error on group permissions"]
3238    }
3239
3240    #
3241    # Get the list of users in this group
3242    #
3243
3244    set luser {}
3245    set sql "SELECT login FROM global.nmuser WHERE idgrp=$idgrp ORDER BY login"
3246    pg_select $dbfd $sql tab {
3247	lappend luser [::webapp::html-string $tab(login)]
3248    }
3249    set tabuser [join $luser ", "]
3250
3251    #
3252    # Get IP ranges allowed to the group
3253    #
3254
3255    set lines {}
3256    set sql "SELECT n.idnet,
3257			n.name, n.location, n.addr4, n.addr6,
3258			p.dhcp, p.acl,
3259			o.name AS org,
3260			c.name AS comm
3261		FROM dns.network n, dns.p_network p,
3262			dns.organization o, dns.community c
3263		WHERE p.idgrp = $idgrp
3264			AND p.idnet = n.idnet
3265			AND o.idorg = n.idorg
3266			AND c.idcomm = n.idcomm
3267		ORDER BY p.sort, n.addr4, n.addr6"
3268    pg_select $dbfd $sql tab {
3269	set n_name 	[::webapp::html-string $tab(name)]
3270	set n_loc	[::webapp::html-string $tab(location)]
3271	set n_org	$tab(org)
3272	set n_comm	$tab(comm)
3273	set n_dhcp	$tab(dhcp)
3274	set n_acl	$tab(acl)
3275
3276	# dispaddr : used for a pleasant address formatting
3277	set dispaddr {}
3278	# where : part of the WHERE clause for address selection
3279	set where  {}
3280	foreach a {addr4 addr6} {
3281	    if {$tab($a) ne ""} then {
3282		lappend dispaddr $tab($a)
3283		lappend where "addr <<= '$tab($a)'"
3284	    }
3285	}
3286	set dispaddr [join $dispaddr ", "]
3287	set where [join $where " OR "]
3288
3289	lappend lines [list Network $n_name]
3290	lappend lines [list Normal4 [mc "Location"] $n_loc \
3291				[mc "Organization"] $n_org]
3292	lappend lines [list Normal4 [mc "Range"] $dispaddr \
3293				[mc "Community"] $n_comm]
3294
3295	set perm {}
3296
3297	set pnet {}
3298	if {$n_dhcp} then { lappend pnet "dhcp" }
3299	if {$n_acl} then { lappend pnet "acl" }
3300	if {[llength $pnet] > 0} then {
3301	    lappend perm [join $pnet ", "]
3302	}
3303	set sql2 "SELECT addr, allow_deny
3304			FROM dns.p_ip
3305			WHERE ($where)
3306			    AND idgrp = $idgrp
3307			ORDER BY addr"
3308	pg_select $dbfd $sql2 tab2 {
3309	    if {$tab2(allow_deny)} then {
3310		set x "+"
3311	    } else {
3312		set x "-"
3313	    }
3314	    lappend perm "$x $tab2(addr)"
3315	}
3316
3317	lappend lines [list Perm [mc "Permissions"] [join $perm "\n"]]
3318    }
3319
3320    if {[llength $lines] > 0} then {
3321	set tabnetworks [::arrgen::output "html" $libconf(tabnetworks) $lines]
3322    } else {
3323	set tabnetworks [mc "No allowed network"]
3324    }
3325
3326    #
3327    # Get IP permissions out of network ranges identified above.
3328    #
3329
3330    set lines {}
3331    set found 0
3332    set sql "SELECT addr, allow_deny
3333		    FROM dns.p_ip
3334		    WHERE NOT (addr <<= ANY (
3335				SELECT n.addr4
3336					FROM dns.network n, dns.p_network p
3337					WHERE n.idnet = p.idnet
3338						AND n.addr4 IS NOT NULL
3339						AND p.idgrp = $idgrp
3340				UNION
3341				SELECT n.addr6
3342					FROM dns.network n, dns.p_network p
3343					WHERE n.idnet = p.idnet
3344						AND n.addr6 IS NOT NULL
3345						AND p.idgrp = $idgrp
3346				    ) )
3347			AND idgrp = $idgrp
3348		    ORDER BY addr"
3349    set perm {}
3350    pg_select $dbfd $sql tab {
3351	set found 1
3352	if {$tab(allow_deny)} then {
3353	    set x "+"
3354	} else {
3355	    set x "-"
3356	}
3357	lappend perm "$x $tab(addr)"
3358    }
3359    lappend lines [list Perm [mc "Permissions"] [join $perm "\n"]]
3360
3361    if {$found} then {
3362	set tabcidralone [::arrgen::output "html" $libconf(tabnetworks) $lines]
3363    } else {
3364	set tabcidralone [mc "None (it's ok)"]
3365    }
3366
3367    #
3368    # Get views
3369    #
3370
3371    set lines {}
3372    lappend lines [list Title [mc "View"] [mc "Selected by default"]]
3373    set sql "SELECT view.name AS name, p_view.selected
3374			FROM dns.p_view, dns.view
3375			WHERE p_view.idview = view.idview
3376				AND p_view.idgrp = $idgrp
3377			ORDER BY p_view.sort ASC, view.name ASC"
3378    pg_select $dbfd $sql tab {
3379	set sel ""
3380	if {$tab(selected)} then {
3381	    set sel [mc "Yes"]
3382	} else {
3383	    set sel [mc "No"]
3384	}
3385
3386	lappend lines [list Normal $tab(name) $sel]
3387    }
3388    if {[llength $lines] > 0} then {
3389	set tabviews [::arrgen::output "html" $libconf(tabviews) $lines]
3390    } else {
3391	set tabviews [mc "No allowed view"]
3392    }
3393
3394    #
3395    # Get domains
3396    #
3397
3398    set lines {}
3399    lappend lines [list Title [mc "Domain"] [mc "Mail role management"]]
3400    set sql "SELECT domain.name AS name, p_dom.mailrole
3401			FROM dns.p_dom, dns.domain
3402			WHERE p_dom.iddom = domain.iddom
3403				AND p_dom.idgrp = $idgrp
3404			ORDER BY p_dom.sort, domain.name"
3405    pg_select $dbfd $sql tab {
3406	set rm ""
3407	if {$tab(mailrole)} then {
3408	    set rm [mc "Yes"]
3409	} else {
3410	    set rm [mc "No"]
3411	}
3412	lappend lines [list Normal $tab(name) $rm]
3413    }
3414    if {[llength $lines] > 0} then {
3415	set tabdomains [::arrgen::output "html" $libconf(tabdomains) $lines]
3416    } else {
3417	set tabdomains [mc "No allowed domain"]
3418    }
3419
3420    #
3421    # Get DHCP profiles
3422    #
3423
3424    set lines {}
3425    set sql "SELECT d.name, p.sort, d.text
3426			FROM dns.dhcpprofile d, dns.p_dhcpprofile p
3427			WHERE d.iddhcpprof = p.iddhcpprof
3428				AND p.idgrp = $idgrp
3429			ORDER BY p.sort, d.name"
3430    pg_select $dbfd $sql tab {
3431	lappend lines [list DHCP $tab(name) $tab(text)]
3432    }
3433    if {[llength $lines] > 0} then {
3434	set tabdhcpprofile [::arrgen::output "html" $libconf(tabdhcpprofile) $lines]
3435    } else {
3436	set tabdhcpprofile [mc "No allowed DHCP profile"]
3437    }
3438
3439    #
3440    # Get equipment permissions
3441    #
3442
3443    set lines {}
3444    foreach {rw text} [list 0 [mc "Read"] 1 [mc "Write"]] {
3445	set sql "SELECT allow_deny, pattern
3446			    FROM topo.p_eq
3447			    WHERE idgrp = $idgrp AND rw = $rw
3448			    ORDER BY rw, allow_deny DESC, pattern"
3449	set perm ""
3450	pg_select $dbfd $sql tab {
3451	    if {$tab(allow_deny) eq "0"} then {
3452		set allow_deny "-"
3453	    } else {
3454		set allow_deny "+"
3455	    }
3456	    append perm "$allow_deny $tab(pattern)\n"
3457	}
3458	if {$perm eq ""} then {
3459	    set perm [mc "No permission"]
3460	}
3461	lappend lines [list PermEq $text $perm]
3462    }
3463    set tabdreq [::arrgen::output "html" $libconf(tabdreq) $lines]
3464
3465    #
3466    # Get VLAN-ids for L2-only networks
3467    #
3468
3469    set lines {}
3470    set sql "SELECT p.vlanid, v.descr
3471			FROM topo.p_l2only p, topo.vlan v
3472			WHERE p.idgrp = $idgrp AND v.vlanid = p.vlanid
3473			ORDER BY p.vlanid ASC"
3474    pg_select $dbfd $sql tab {
3475	lappend lines [list Normal "$tab(vlanid) - $tab(descr)"]
3476    }
3477    if {[llength $lines] > 0} then {
3478	set tabl2only [::arrgen::output "html" $libconf(tabl2only) $lines]
3479    } else {
3480	set tabl2only [mc "No allowed L2-only network"]
3481    }
3482
3483    #
3484    # Return informations
3485    #
3486
3487    return [list    $tabperm \
3488		    $tabuser \
3489		    $tabnetworks \
3490		    $tabcidralone \
3491		    $tabviews \
3492		    $tabdomains \
3493		    $tabdhcpprofile \
3494		    $tabdreq \
3495		    $tabl2only \
3496	    ]
3497}
3498
3499##############################################################################
3500# Cryptographic functions
3501##############################################################################
3502
3503#
3504# Crypt a password
3505#
3506# Input:
3507#   - parameters :
3508#	- str : string to crypt
3509# Output:
3510#   - return value : crypted string
3511#
3512# History
3513#   2003/05/13 : pda/jean : design
3514#   2005/07/22 : pda/jean : secure special characters
3515#   2010/12/29 : pda      : i18n and netmagis merge
3516#   2013/02/08 : pda/jean : apply schplurtz's patch
3517#   2014/05/09 : pda/jean : use md5crypt tcllib package
3518#
3519
3520proc pgauth-crypt {str} {
3521    return [md5crypt::md5crypt $str [::md5crypt::salt]]
3522}
3523
3524#
3525# Check a user-provided clear-text password against the crypted one in database
3526#
3527# Input:
3528#   - parameters :
3529#	- pw : clear-text password provided by the user
3530#	- refpw : encrypted password from the database
3531# Output:
3532#   - return value : true if the encrypted passwords match
3533#
3534# History
3535#   2014/05/09 : pda/jean : design
3536#
3537
3538proc pgauth-checkpw {pw pwref} {
3539    set success false
3540    if {[regexp {^\$1\$([^\$]+)\$} $pwref dummy salt]} then {
3541	set crypted [::md5crypt::md5crypt $pw $salt]
3542	if {$crypted eq $pwref} then {
3543	    set success true
3544	}
3545    }
3546    return $success
3547}
3548
3549#
3550# Generate a semi-random password
3551#
3552# Input:
3553#   - parameters : (none)
3554# Output:
3555#   - return value : generated clear-text password
3556#
3557# History
3558#   2003/06/13 : pda/jean : design
3559#   2010/12/29 : pda      : i18n and netmagis merge
3560#
3561
3562proc pgauth-genpw {} {
3563    set pwgen [get-local-conf "pwgen"]
3564    return [exec sh -c $pwgen]
3565}
3566
3567#
3568# Generate n bytes of random
3569#
3570# Input:
3571#   - parameters :
3572#	- nbytes: number of bytes
3573# Output:
3574#   - return value : random string of hex characters
3575#
3576# History
3577#   2014/05/09 : pda/jean : design
3578#
3579
3580proc get-random {nbytes} {
3581    set dev [get-local-conf "random"]
3582    if {[catch {set fd [open $dev {RDONLY BINARY}]} msg]} then {
3583	#
3584	# Silently fall-back to a non cryptographically secure random
3585	# if /dev/random is not available
3586	#
3587	expr srand([clock clicks -microseconds])
3588	set r ""
3589	for {set i 0} {$i < $nbytes} {incr i} {
3590	    append r [binary format "c" [expr int(rand()*256)]]
3591	}
3592    } else {
3593	#
3594	# Successful open: read random bytes
3595	#
3596	set r [read $fd $nbytes]
3597	close $fd
3598    }
3599
3600    binary scan $r "H*" hex
3601    return $hex
3602}
3603
3604##############################################################################
3605# Authentication
3606##############################################################################
3607
3608#
3609# Check authentication token
3610#
3611# Input:
3612#   - parameters:
3613#	- dbfd : database handle
3614#	- token : authentication token (given by the session cookie)
3615#	- _login : in return, login of user or "" if the token is not valid
3616# Output:
3617#   - return value: true if the token is a valid authentication token
3618#
3619# History
3620#   2014/04/11 : pda/jean : design
3621#   2015/02/04 : pda/jean : simplify session management with *tmp tables
3622#
3623
3624proc check-authtoken {dbfd token _login} {
3625    upvar $_login login
3626
3627    set idle       [dnsconfig get "authexpire"]
3628    set wtmpexpire [dnsconfig get "wtmpexpire"]
3629
3630    #
3631    # Expire old utmp entries
3632    #
3633
3634    d dblock {global.utmp global.wtmp}
3635
3636    # Get the list of expired sessions for the log (see below)
3637
3638    set sql "SELECT u.login, t.token, t.lastaccess
3639    			FROM global.nmuser u, global.utmp t
3640			WHERE t.lastaccess < NOW() - interval '$idle second'
3641			    AND u.idcor = t.idcor"
3642    set lexp {}
3643    pg_select $dbfd $sql tab {
3644	lappend lexp [list $tab(login) $tab(token) $tab(lastaccess)]
3645    }
3646
3647    # Transfer all expired utmp entries to wtmp
3648    # and delete old wtmp entries
3649
3650    set sql "INSERT INTO global.wtmp (idcor, token, start, ip, stop, stopreason)
3651		SELECT idcor, token, start, ip, lastaccess, 'expired'
3652		    FROM global.utmp
3653		    WHERE lastaccess < NOW() - interval '$idle second'
3654		    ;
3655	     DELETE FROM global.utmp
3656		    WHERE lastaccess < NOW() - interval '$idle second'
3657		    ;
3658	     DELETE FROM global.wtmp
3659		    WHERE stop < NOW() - interval '$wtmpexpire day'
3660		    "
3661    if {! [::pgsql::execsql $dbfd $sql msg]} then {
3662	d dbabort [mc "session expiration"] $msg
3663	return [mc "Cannot un-register connection (%s)" $msg]
3664    }
3665
3666    # Log expired sessions
3667
3668    foreach e $lexp {
3669	lassign $e l tok la
3670	d writelog "auth" "lastaccess $l $tok" $la $l
3671    }
3672
3673    d dbcommit "session expiration"
3674
3675    #
3676    # Check our own authentication token
3677    #
3678
3679    set qtoken [::pgsql::quote $token]
3680    set login ""
3681    set found false
3682    set sql "UPDATE global.utmp t
3683		    SET lastaccess = NOW()
3684		    FROM global.nmuser u
3685		    WHERE token = '$qtoken' AND u.idcor = t.idcor
3686		    RETURNING u.login"
3687    pg_select $dbfd $sql tab {
3688	set login $tab(login)
3689	set found true
3690    }
3691
3692    if {$found} then {
3693	# re-inject cookie (for login/call-cgi)
3694	::webapp::set-cookie "session" $token 0 "" "" 0 0
3695    }
3696
3697    return $found
3698}
3699
3700#
3701# Register a user login, create a session token and displays the start page
3702#
3703# Input:
3704#   - parameters:
3705#	- dbfd : database handle
3706#	- login : the user for which we generate this token
3707#	- casticket : service ticket returned by CAS server, or empty string
3708# Output:
3709#   - return value: error message or empty string
3710#   - database: token is registered in database
3711#
3712# History
3713#   2014/04/12 : pda      : design
3714#   2015/01/21 : pda/jean : added idcor parameter
3715#   2015/02/25 : pda/jean : add code common to PGSQL/LDAP and CAS
3716#   2015/03/04 : pda/jean : register cas ticket
3717#   2015/06/05 : pda/jean : remove idcor parameter
3718#
3719
3720proc register-user-login {dbfd login casticket} {
3721    global env
3722
3723    #
3724    # Search id for the login
3725    #
3726
3727    set qlogin [::pgsql::quote $login]
3728    set idcor -1
3729    set sql "SELECT idcor
3730		FROM global.nmuser
3731		WHERE login = '$qlogin'
3732		    AND present = 1"
3733    pg_select $dbfd $sql tab {
3734	set idcor $tab(idcor)
3735    }
3736    if {$idcor == -1} then {
3737	return [mc "Login '%s' does not exist" $login]
3738    }
3739
3740    #
3741    # Generates a unique (at a given time) token
3742    # In order to test if a generated token is already used, we search it
3743    # in the global.tmp template table (which gathers all utmp and wtmp
3744    # lines)
3745    #
3746
3747    d dblock {global.utmp}
3748
3749    set toklen [dnsconfig get "authtoklen"]
3750
3751    set found true
3752    while {$found} {
3753	set token [get-random $toklen]
3754	set sql "SELECT idcor FROM global.tmp WHERE token = '$token'"
3755	set found false
3756	pg_select $dbfd $sql tab {
3757	    set found true
3758	}
3759    }
3760
3761    #
3762    # Register token in utmp table
3763    #
3764
3765    set ip NULL
3766    if {[info exists env(REMOTE_ADDR)]} then {
3767	set ip "'$env(REMOTE_ADDR)'"
3768    }
3769    set qcas NULL
3770    if {$casticket ne ""} then {
3771	set qcas [::pgsql::quote $casticket]
3772	set qcas "'$qcas'"
3773    }
3774
3775    set sql "INSERT INTO global.utmp (idcor, token, casticket, ip)
3776    		VALUES ($idcor, '$token', $qcas, $ip)"
3777    if {! [::pgsql::execsql $dbfd $sql msg]} then {
3778	d dbabort [mc "session creation for %s" login] $msg
3779	return [mc "Cannot register user login (%s)" $msg]
3780    }
3781
3782    d dbcommit [mc "session creation for %s" $login]
3783
3784    #
3785    # Log successful flogin
3786    #
3787
3788    d writelog "auth" "login $login $token"
3789
3790    #
3791    # Set session cookie
3792    #
3793
3794    ::webapp::set-cookie "session" $token 0 "" "" 0 0
3795
3796    return ""
3797}
3798
3799proc register-user-logout {dbfd login token date reason} {
3800    set idcor -1
3801    set qlogin [::pgsql::quote $login]
3802    set sql "SELECT idcor FROM global.nmuser WHERE login = '$qlogin'"
3803    pg_select $dbfd $sql tab {
3804	set idcor $tab(idcor)
3805    }
3806    if {$idcor == -1} then {
3807	return [mc "Login '%s' does not exist" $login]
3808    }
3809    if {$date eq ""} then {
3810	set date "now()"
3811    } else {
3812	set date "'$date'"
3813    }
3814
3815    set sql "INSERT INTO global.wtmp (idcor, token, start, ip, stop, stopreason)
3816		SELECT $idcor, token, start, ip, $date, '$reason'
3817		    FROM global.utmp
3818		    WHERE idcor = $idcor and token = '$token'
3819		    ;
3820	     DELETE FROM global.utmp
3821		    WHERE idcor = $idcor and token = '$token'"
3822    if {! [::pgsql::execsql $dbfd $sql msg]} then {
3823	return [mc "Cannot un-register connection (%s)" $msg]
3824    }
3825    return ""
3826}
3827
3828##############################################################################
3829# User access rights management
3830##############################################################################
3831
3832#
3833# Check login name validity
3834#
3835# Input:
3836#   - parameters:
3837#	- login : login name
3838# Output:
3839#   - return value: 1 (valid) or 0 (invalid)
3840#
3841# History
3842#   2015/05/07 : pda/jean : design
3843#
3844
3845proc check-login {name} {
3846    return [expr ! [regexp {[()<>*]} $name]]
3847}
3848
3849#
3850# Search attributes associated to a user
3851#
3852# Input:
3853#   - parameters:
3854#	- dbfd : database handle
3855#	- idcor : user id
3856#	- attr : attribute to check (table column)
3857# Output:
3858#   - return value: information found
3859#
3860# History
3861#   2000/07/26 : pda      : design
3862#   2002/05/03 : pda/jean : use in netmagis
3863#   2002/05/06 : pda/jean : groups
3864#   2010/11/29 : pda      : i18n
3865#
3866
3867proc user-attribute {dbfd idcor attr} {
3868    set v 0
3869    set sql "SELECT nmgroup.$attr
3870			FROM global.nmgroup, global.nmuser
3871			WHERE nmuser.idcor = $idcor
3872			    AND nmuser.idgrp = nmgroup.idgrp"
3873    pg_select $dbfd $sql tab {
3874	set v "$tab($attr)"
3875    }
3876    return $v
3877}
3878
3879#
3880# Read informations associated to a user
3881#
3882# Input:
3883#   - parameters:
3884#	- dbfd : database handle
3885#	- login : user login
3886#	- _tabuid : array containing, in return:
3887#		login	  login of the user
3888#		lastname  user name [if ah global variable is set]
3889#		firstname user first name [if ah global variable is set]
3890#		mail	user mail [if ah global variable is set]
3891#		phone	user phone [if ah global variable is set]
3892#		mobile	user mobile phone [if ah global variable is set]
3893#		fax	user fax [if ah global variable is set]
3894#		addr	user address [if ah global variable is set]
3895#		idcor	user id in the database
3896#		idgrp	group id in the database
3897#		group	group name
3898#		present	1 if "present" in the database
3899#		p_admin	1 if admin
3900#		p_smtp  1 if permission to allow hosts to emit with SMTP
3901#		p_ttl   1 if permission to edit host TTL
3902#		p_mac   1 if permission to use the MAC module
3903#		p_genl  1 if permission to generate a link number
3904#		networks list of authorized networks
3905#		eq	regexp matching authorized equipments
3906#		flagsr	flags -n/-e/-E/-v/etc to use in topo programs
3907#		flagsw	flags -n/-e/-E/-v/etc to use in topo programs
3908# Output:
3909#   - return value: -1 if error, or number of found entries
3910#   - parameter _tabuid : values in return
3911#   - parameter _msg : empty string (if return == 1) or message (if return != 1)
3912#
3913# History
3914#   2003/05/13 : pda/jean : design
3915#   2007/10/05 : pda/jean : adaptation to "authuser" and "authbase" objects
3916#   2010/11/09 : pda      : renaming (car plus de recherche par id)
3917#   2010/11/29 : pda      : i18n
3918#   2011/06/17 : pda      : add test on ah global variable
3919#   2012/01/21 : jean     : add generate link number permission
3920#
3921
3922proc read-user {dbfd login _tabuid _msg} {
3923    global ah
3924    upvar $_tabuid tabuid
3925    upvar $_msg msg
3926
3927    catch {unset tabuid}
3928
3929    if {$ah ne ""} then {
3930	#
3931	# Attributes common to all applications
3932	#
3933
3934	set u [::webapp::authuser create %AUTO%]
3935	if {[catch {set n [$ah getuser $login $u]} m]} then {
3936	    set msg [mc "Authentication base problem: %s" $m]
3937	    return -1
3938	}
3939
3940	switch $n {
3941	    0 {
3942		set msg [mc "User '%s' is not in the authentication base" $login]
3943		return 0
3944	    }
3945	    1 {
3946		set msg ""
3947	    }
3948	    default {
3949		set msg [mc "Found more than one entry for login '%s' in the authentication base" $login]
3950		return $n
3951	    }
3952	}
3953
3954	foreach c {login password lastname firstname mail phone mobile fax addr} {
3955	    set tabuid($c) [$u get $c]
3956	}
3957
3958	$u destroy
3959    }
3960
3961    #
3962    # Netmagis specific characteristics
3963    #
3964
3965    set qlogin [::pgsql::quote $login]
3966    set tabuid(idcor) -1
3967    set sql "SELECT * FROM global.nmuser, global.nmgroup
3968			WHERE nmuser.login = '$qlogin'
3969			    AND nmuser.idgrp = nmgroup.idgrp"
3970    pg_select $dbfd $sql tab {
3971	set tabuid(idcor)	$tab(idcor)
3972	set tabuid(idgrp)	$tab(idgrp)
3973	set tabuid(present)	$tab(present)
3974	set tabuid(group)	$tab(name)
3975	set tabuid(p_admin)	$tab(p_admin)
3976	set tabuid(p_smtp)	$tab(p_smtp)
3977	set tabuid(p_ttl)	$tab(p_ttl)
3978	set tabuid(p_mac)	$tab(p_mac)
3979	set tabuid(p_genl)	$tab(p_genl)
3980    }
3981
3982    if {$tabuid(idcor) == -1} then {
3983	set msg [mc "User '%s' is not in the Netmagis base" $login]
3984	return 0
3985    }
3986
3987    #
3988    # Topo specific characteristics
3989    #
3990
3991    # Read authorized L2-only networks
3992    set tabuid(l2only) [allowed-l2only $dbfd $tabuid(idgrp)]
3993
3994    # Read authorized CIDR
3995    set tabuid(networks) [allowed-networks $dbfd $tabuid(idgrp) "consult"]
3996
3997    # Read regexp to allow or deny access to equipments
3998    set tabuid(eqr) [read-authorized-eq $dbfd 0 $tabuid(idgrp)]
3999    set tabuid(eqw) [read-authorized-eq $dbfd 1 $tabuid(idgrp)]
4000
4001    # Build flags to restrict graph to a subset according to
4002    # user rights.
4003    set flagsr {}
4004    set flagsw {}
4005    foreach rw {r w} {
4006	set flags {}
4007	if {$tabuid(p_admin)} then {
4008	    # Administrator sees the whole graph
4009	    lappend flags "-a"
4010
4011	    # Even if he sees the whole graph, administrator has not
4012	    # the right to modify non terminal interfaces
4013	    if {$rw eq "w"} then {
4014		lappend flags "-t"
4015	    }
4016
4017	} else {
4018	    lassign $tabuid(eq$rw) lallow ldeny
4019
4020	    # Build networks rights first: the user has access to
4021	    # all interfaces that "his" networks reach (except if
4022	    # has no right on an equipment)
4023	    foreach r $tabuid(networks) {
4024		set r4 [lindex $r 1]
4025		if {$r4 ne ""} then {
4026		    lappend flags "-n" $r4
4027		}
4028		set r6 [lindex $r 2]
4029		if {$r6 ne ""} then {
4030		    lappend flags "-n" $r6
4031		}
4032	    }
4033
4034	    # Next, build access rights on L2-only networks
4035	    foreach vlan $tabuid(l2only) {
4036		lappend flags "-v" $vlan
4037	    }
4038
4039	    # Next, build access rights on equipements (part 1)
4040	    # The user has access to the whole equipment (including
4041	    # interfaces)
4042	    foreach pat $lallow {
4043		lappend flags "-e" $pat
4044	    }
4045
4046	    # Next, build access rights on equipements (part 2)
4047	    # The user has no access to the whole equipment, even
4048	    # if some parts (equipement or interfaces reached by
4049	    # a network) have been selected previously).
4050	    foreach pat $ldeny {
4051		lappend flags "-E" $pat
4052	    }
4053
4054	    # Last, the user don't have right to modify:
4055	    # - non terminal interfaces
4056	    # - interfaces which transport a foreign network
4057	    if {$rw eq "w"} then {
4058		lappend flags "-t" "-m"
4059	    }
4060	}
4061	set tabuid(flags$rw) [join $flags " "]
4062    }
4063
4064    return 1
4065}
4066
4067##############################################################################
4068# Database management : resources records
4069##############################################################################
4070
4071#
4072# Return all RR with a given name (in different views)
4073#
4074# Input:
4075#   - parameters:
4076#	- dbfd : database handle
4077#	- name : name to search for
4078#	- iddom : id of the domain in which to search for the name
4079# Output:
4080#   - return value: { {idrr idview} {idrr idview} ...}
4081#
4082# History
4083#   2013/04/05 : pda/jean : design
4084#
4085
4086proc all-rr-by-name {dbfd name iddom} {
4087    set qname [::pgsql::quote $name]
4088    set sql "SELECT idrr, idview FROM dns.rr
4089    				WHERE name = '$qname' AND iddom = $iddom"
4090    set l {}
4091    pg_select $dbfd $sql tab {
4092	lappend l [list $tab(idrr) $tab(idview)]
4093    }
4094
4095    return $l
4096}
4097
4098#
4099# Get all informations associated with a name
4100#
4101# Input:
4102#   - parameters:
4103#	- dbfd : database handle
4104#	- name : name to search for
4105#	- iddom : id of the domain in which to search for the name
4106#	- idview: view id
4107#	- _trr : empty array
4108# Output:
4109#   - return value: 1 if ok, 0 if not found
4110#   - _trr parameter : see read-rr-by-id
4111#
4112# History
4113#   2002/04/11 : pda/jean : design
4114#   2002/04/19 : pda/jean : add name and iddom
4115#   2002/04/19 : pda/jean : use read-rr-by-id
4116#   2010/11/29 : pda      : i18n
4117#   2013/04/05 : pda/jean : add view
4118#
4119
4120proc read-rr-by-name {dbfd name iddom idview _trr} {
4121    upvar $_trr trr
4122
4123    set qname [::pgsql::quote $name]
4124    set found 0
4125    set sql "SELECT idrr FROM dns.rr
4126			    WHERE name = '$qname'
4127				AND iddom = $iddom
4128				AND idview = $idview"
4129    pg_select $dbfd $sql tab {
4130	set found 1
4131	set idrr $tab(idrr)
4132    }
4133
4134    if {$found} then {
4135	set found [read-rr-by-id $dbfd $idrr trr]
4136    }
4137
4138    return $found
4139}
4140
4141#
4142# Get all informations associated with a RR given by the MAC Address
4143#
4144# Input:
4145#   - parameters:
4146#       - dbfd : database handle
4147#       - addr : address to search for
4148#       - _trr : empty array
4149# Output:
4150#   - return value: 1 if ok, 0 if not found
4151#   - _trr parameter : see read-rr-by-id
4152#
4153# Note: the given address is supposed to be syntaxically correct.
4154#
4155# History
4156#   2012/04/28 : jean : integrated patch from Benoit.Mandy@u-bordeaux4.fr
4157#
4158
4159proc read-rr-by-mac {dbfd addr _trr} {
4160    upvar $_trr trr
4161
4162    set found 0
4163    set sql "SELECT idrr FROM dns.rr WHERE mac = '$addr'"
4164    pg_select $dbfd $sql tab {
4165	set found 1
4166	set idrr $tab(idrr)
4167    }
4168
4169    if {$found} then {
4170	set found [read-rr-by-id $dbfd $idrr trr]
4171    }
4172
4173    return $found
4174}
4175
4176
4177#
4178# Get all informations associated with a RR given by one of its IP address
4179#
4180# Input:
4181#   - parameters:
4182#	- dbfd : database handle
4183#	- addr : address to search for
4184#	- idview : id of view
4185#	- _trr : empty array
4186# Output:
4187#   - return value: 1 if ok, 0 if not found
4188#   - _trr parameter : see read-rr-by-id
4189#
4190# Note: the given address is supposed to be syntaxically correct.
4191#
4192# History
4193#   2002/04/26 : pda/jean : design
4194#   2010/11/29 : pda      : i18n
4195#
4196
4197proc read-rr-by-ip {dbfd addr idview _trr} {
4198    upvar $_trr trr
4199
4200    set found 0
4201    set sql "SELECT i.idrr
4202			FROM dns.rr_ip i, dns.rr r
4203    			WHERE i.idrr = r.idrr
4204			    AND i.addr = '$addr'
4205			    AND r.idview = $idview"
4206    pg_select $dbfd $sql tab {
4207	set found 1
4208	set idrr $tab(idrr)
4209    }
4210
4211    if {$found} then {
4212	set found [read-rr-by-id $dbfd $idrr trr]
4213    }
4214
4215    return $found
4216}
4217
4218#
4219# Get all informations associated with a RR.
4220#
4221# Input:
4222#   - parameters:
4223#	- dbfd : database handle
4224#	- idrr : RR id to search for
4225#	- _trr : empty array
4226# Output:
4227#   - return value: 1 if ok, 0 if not found
4228#   - parameter _trr :
4229#	_trr(idrr) : id of RR found
4230#	_trr(name) : name (first component of the FQDN)
4231#	_trr(iddom) : domain id
4232#	_trr(idview) : view id
4233#	_trr(domain) : domain name
4234#	_trr(mac) : MAC address
4235#	_trr(iddhcpprof) : DHCP profile id, or 0 if none
4236#	_trr(dhcpprof) : DHCP profile name, or "No profile"
4237#	_trr(idhinfo) : machine info id
4238#	_trr(hinfo) : machine info text
4239#	_trr(sendsmtp) : 1 if host has the right to emit with non auth SMTP
4240#	_trr(ttl) : TTL of the host (for all its IP addresses)
4241#	_trr(comment) : comments
4242#	_trr(respname) : name of the responsible person
4243#	_trr(respmail) : mail of the responsible person
4244#	_trr(idcor) : id of user who has done the last modification
4245#	_trr(date) : date of last modification
4246#	_trr(ip) : list of all IP addresses {{idview addr} ...}
4247#	_trr(mx) : MX list {{idview prio idrr} {idview prio idrr} ...}
4248#	_trr(mxtarg) : list of MX which target this host
4249#	_trr(cname) : list of pointed RR, if name is an alias {{idview idrr}...}
4250#	_trr(aliases) : list of all RR pointing to this object {{idview idrr}..}
4251#	_trr(mailrole) : id of mbox host {{idview idmboxhost idviewmbox} ...}
4252#	_trr(mailaddr) : idrr of mail addresses hosted on this host
4253#		{{idview idmailaddr idviewmailaddr} ...}
4254#
4255# History
4256#   2002/04/19 : pda/jean : design
4257#   2002/06/02 : pda/jean : hinfo becomes an index in a table
4258#   2004/02/06 : pda/jean : add mailrole, mailaddr and roleweb
4259#   2004/08/05 : pda/jean : simplification and add mac
4260#   2005/04/08 : pda/jean : add dhcpprofil
4261#   2008/07/24 : pda/jean : add sendsmtp
4262#   2010/10/31 : pda      : add ttl
4263#   2010/11/29 : pda      : i18n
4264#   2012/10/08 : pda/jean : views
4265#   2013/04/05 : pda/jean : temporary hack for views
4266#   2013/04/10 : pda/jean : remove roleweb
4267#
4268
4269proc read-rr-by-id {dbfd idrr _trr} {
4270    upvar $_trr trr
4271
4272    set fields {name iddom idview
4273	mac iddhcpprof idhinfo sendsmtp ttl comment respname respmail
4274	idcor date}
4275
4276    catch {unset trr}
4277    set trr(idrr) $idrr
4278
4279    set found 0
4280    set columns [join $fields ", "]
4281    set sql "SELECT $columns FROM dns.rr WHERE idrr = $idrr"
4282    pg_select $dbfd $sql tab {
4283	set found 1
4284	foreach v $fields {
4285	    set trr($v) $tab($v)
4286	}
4287    }
4288
4289    if {$found} then {
4290	set idview $trr(idview)
4291	set trr(domain) ""
4292	if {$trr(iddhcpprof) eq ""} then {
4293	    set trr(iddhcpprof) 0
4294	    set trr(dhcpprof) [mc "No profile"]
4295	} else {
4296	    set sql "SELECT name FROM dns.dhcpprofile
4297				WHERE iddhcpprof = $trr(iddhcpprof)"
4298	    pg_select $dbfd $sql tab {
4299		set trr(dhcpprof) $tab(name)
4300	    }
4301	}
4302	set sql "SELECT name FROM dns.hinfo WHERE idhinfo = $trr(idhinfo)"
4303	pg_select $dbfd $sql tab {
4304	    set trr(hinfo) $tab(name)
4305	}
4306	set sql "SELECT name FROM dns.domain WHERE iddom = $trr(iddom)"
4307	pg_select $dbfd $sql tab {
4308	    set trr(domain) $tab(name)
4309	}
4310	set trr(ip) {}
4311	set sql "SELECT addr FROM dns.rr_ip WHERE idrr = $idrr"
4312	pg_select $dbfd $sql tab {
4313	    lappend trr(ip) [list $idview $tab(addr)]
4314	}
4315	set trr(mx) {}
4316	set sql "SELECT prio, mx FROM dns.rr_mx WHERE idrr = $idrr"
4317	pg_select $dbfd $sql tab {
4318	    lappend trr(mx) [list $idview $tab(prio) $tab(mx)]
4319	}
4320	set trr(mxtarg) {}
4321	set sql "SELECT idrr FROM dns.rr_mx WHERE mx = $idrr"
4322	pg_select $dbfd $sql tab {
4323	    lappend trr(mxtarg) [list $idview $tab(idrr)]
4324	}
4325	set trr(cname) ""
4326	set sql "SELECT cname FROM dns.rr_cname WHERE idrr = $idrr"
4327	pg_select $dbfd $sql tab {
4328	    lappend trr(cname) [list $idview $tab(cname)]
4329	}
4330	set trr(aliases) {}
4331	set sql "SELECT idrr FROM dns.rr_cname WHERE cname = $idrr"
4332	pg_select $dbfd $sql tab {
4333	    lappend trr(aliases) [list $idview $tab(idrr)]
4334	}
4335	# is this name a mail address?
4336	set trr(mailrole) ""
4337	set sql "SELECT mr.mboxhost, rrmb.idview AS idviewmbox
4338			    FROM dns.mail_role mr, dns.rr rrmb
4339			    WHERE mr.mailaddr = $idrr
4340				AND mr.mboxhost = rrmb.idrr"
4341	pg_select $dbfd $sql tab {
4342	    lappend trr(mailrole) [list $idview $tab(mboxhost) $tab(idviewmbox)]
4343	}
4344	# all mail addresses pointing to this host
4345	set trr(mailaddr) {}
4346	set sql "SELECT rrma.idrr AS idrrma, rrma.idview AS idviewma
4347			    FROM dns.mail_role mr, dns.rr rrma
4348			    WHERE mboxhost = $idrr
4349				AND mr.mailaddr = rrma.idrr"
4350	pg_select $dbfd $sql tab {
4351	    lappend trr(mailaddr) [list $idview $tab(idrrma) $tab(idviewma)]
4352	}
4353    }
4354
4355    return $found
4356}
4357
4358#
4359# Get RR information filtered for a view
4360#
4361# Input:
4362#   - parameters:
4363#       - _trr : see read-rr-by-id
4364#	- idview : view
4365# Output:
4366#   - return value: list of IP addresses
4367#
4368# History
4369#   2012/10/08 : pda/jean : design
4370#
4371
4372proc rr-ip-by-view {_trr idview} {
4373    upvar $_trr trr
4374
4375    set lip {}
4376    if {[info exists trr(ip)]} then {
4377	foreach ipview $trr(ip) {
4378	    lassign $ipview id ip
4379	    if {$id == $idview} then {
4380		lappend lip $ip
4381	    }
4382	}
4383    }
4384    return $lip
4385}
4386
4387proc rr-cname-by-view {_trr idview} {
4388    upvar $_trr trr
4389
4390    set r ""
4391    if {[info exists trr(cname)]} then {
4392	foreach cv $trr(cname) {
4393	    lassign $cv id cname
4394	    if {$id == $idview} then {
4395		set r $cname
4396		break
4397	    }
4398	}
4399    }
4400    return $r
4401}
4402
4403proc rr-aliases-by-view {_trr idview} {
4404    upvar $_trr trr
4405
4406    set laliases {}
4407    if {[info exists trr(aliases)]} then {
4408	foreach alview $trr(aliases) {
4409	    lassign $alview id idalias
4410	    if {$id == $idview} then {
4411		lappend laliases $idalias
4412	    }
4413	}
4414    }
4415    return $laliases
4416}
4417
4418proc rr-mx-by-view {_trr idview} {
4419    upvar $_trr trr
4420
4421    set lmx {}
4422    if {[info exists trr(mx)]} then {
4423	foreach mxview $trr(mx) {
4424	    lassign $mxview id prio idrr
4425	    if {$id == $idview} then {
4426		lappend lmx [list $prio $idrr]
4427	    }
4428	}
4429    }
4430    return $lmx
4431}
4432
4433proc rr-mxtarg-by-view {_trr idview} {
4434    upvar $_trr trr
4435
4436    set lmxt {}
4437    if {[info exists trr(mxtarg)]} then {
4438	foreach mxview $trr(mxtarg) {
4439	    lassign $mxview id idrr
4440	    if {$id == $idview} then {
4441		lappend lmxt $idrr
4442	    }
4443	}
4444    }
4445    return $lmxt
4446}
4447
4448proc rr-mailrole-by-view {_trr idview} {
4449    upvar $_trr trr
4450
4451    set lrm {}
4452    if {[info exists trr(mailrole)]} then {
4453	foreach rmview $trr(mailrole) {
4454	    lassign $rmview id idmboxhost idviewmboxhost
4455	    if {$id == $idview} then {
4456		set lrm [list $idmboxhost $idviewmboxhost]
4457	    }
4458	}
4459    }
4460    return $lrm
4461}
4462
4463proc rr-mailaddr-by-view {_trr idview} {
4464    upvar $_trr trr
4465
4466    set lam {}
4467    if {[info exists trr(mailaddr)]} then {
4468	foreach amview $trr(mailaddr) {
4469	    lassign $amview id idrraddr idviewaddr
4470	    if {$id == $idview} then {
4471		lappend lam [list $idrraddr $idviewaddr]
4472	    }
4473	}
4474    }
4475    return $lam
4476}
4477
4478#
4479# Delete an alias
4480#
4481# Input:
4482#   - parameters:
4483#	- dbfd : database handle
4484#	- idrr : id of RR to delete (CNAME RR)
4485#	- _msg : error message in return
4486# Output:
4487#   - return value: error message or empty string
4488#
4489# History
4490#   2002/04/19 : pda/jean : design
4491#   2010/11/29 : pda      : i18n
4492#   2012/11/13 : pda/jean : add views
4493#   2013/03/28 : pda/jean : interface simplification
4494#   2013/04/10 : pda/jean : remove views
4495#
4496
4497proc del-alias-by-id {dbfd idrr} {
4498    set msg ""
4499    set sql "DELETE FROM dns.rr_cname WHERE idrr = $idrr"
4500    if {[::pgsql::execsql $dbfd $sql msg]} then {
4501	set msg [del-orphaned-rr $dbfd $idrr]
4502    }
4503    return $msg
4504}
4505
4506#
4507# Delete all IP address for a RR
4508#
4509# Input:
4510#   - parameters:
4511#	- dbfd : database handle
4512#	- idrr : RR id
4513# Output:
4514#   - return value: error message or empty string
4515#
4516# History
4517#   2002/04/19 : pda/jean : design
4518#   2010/11/29 : pda      : i18n
4519#   2012/11/13 : pda/jean : add views
4520#   2012/11/14 : pda/jean : delete addr parameter
4521#   2013/03/28 : pda/jean : interface simplification
4522#   2013/04/10 : pda/jean : remove views
4523#
4524
4525proc del-all-ip-addresses {dbfd idrr} {
4526    set msg ""
4527    set sql "DELETE FROM dns.rr_ip WHERE idrr = $idrr"
4528    if {[::pgsql::execsql $dbfd $sql msg]} then {
4529	set msg ""
4530    }
4531    return $msg
4532}
4533
4534#
4535# Delet all MX associated with an RR
4536#
4537# Input:
4538#   - parameters:
4539#	- dbfd : database handle
4540#	- idrr : RR id of MX
4541# Output:
4542#   - return value: error message or empty string
4543#
4544# History
4545#   2002/04/19 : pda/jean : design
4546#   2010/11/29 : pda      : i18n
4547#   2012/11/13 : pda/jean : add views
4548#   2013/03/28 : pda/jean : interface simplification
4549#   2013/04/10 : pda/jean : remove views
4550#
4551
4552proc del-mx-by-id {dbfd idrr} {
4553    set msg ""
4554    set sql "DELETE FROM dns.rr_mx WHERE idrr = $idrr"
4555    if {[::pgsql::execsql $dbfd $sql msg]} then {
4556	set msg ""
4557    }
4558    return $msg
4559}
4560
4561#
4562# Delete a mailrole
4563#
4564# Input:
4565#   - parameters:
4566#	- dbfd : database handle
4567#	- idrr : RR id
4568# Output:
4569#   - return value: error message or empty string
4570#
4571# History
4572#   2004/02/06 : pda/jean : design
4573#   2010/11/29 : pda      : i18n
4574#   2012/11/13 : pda/jean : add views
4575#   2013/03/28 : pda/jean : interface simplification
4576#   2013/04/10 : pda/jean : remove views
4577#
4578
4579proc del-mailrole-by-id {dbfd idrr} {
4580    set msg ""
4581    set sql "DELETE FROM dns.mail_role WHERE mailaddr = $idrr"
4582    if {[::pgsql::execsql $dbfd $sql msg]} then {
4583	set msg ""
4584    }
4585    return $msg
4586}
4587
4588#
4589# Delete an RR and all associated dependancies
4590#
4591# Input:
4592#   - parameters:
4593#	- dbfd : database handle
4594#	- _trr : RR informations (see read-rr-by-id)
4595# Output:
4596#   - return value: error message or empty string
4597#
4598# History
4599#   2002/04/19 : pda/jean : design
4600#   2004/02/06 : pda/jean : add mailrole and roleweb
4601#   2010/11/29 : pda      : i18n
4602#   2012/11/13 : pda/jean : add views
4603#   2013/03/28 : pda/jean : interface simplification
4604#   2013/04/10 : pda/jean : remove views
4605#
4606
4607proc del-rr-and-dependancies {dbfd _trr} {
4608    upvar $_trr trr
4609
4610    set idrr $trr(idrr)
4611    set idview $trr(idview)
4612
4613    #
4614    # If this host holds mail addresses, don't delete it.
4615    #
4616
4617    set mailaddr [rr-mailaddr-by-view trr $idview]
4618    if {[llength $mailaddr] > 0} then {
4619	return [mc "This host holds mail addresses"]
4620    }
4621
4622    #
4623    # If this host is the target of any MX, don't delete it.
4624    #
4625
4626    set mxt [rr-mxtarg-by-view trr $idview]
4627    if {[llength $mxt] > 0} then {
4628	return [mc "This host is the target of one or more MX"]
4629    }
4630    set sql "SELECT COUNT(*) FROM dns.relay_dom WHERE mx = $idrr"
4631    pg_select $dbfd $sql tab {
4632	set count $tab(count)
4633    }
4634    if {$count > 0} then {
4635	return [mc "This host is the mail relay of one or more domains"]
4636    }
4637
4638    #
4639    # Delete all aliases pointing to this object
4640    #
4641
4642    foreach a [rr-aliases-by-view trr $idview] {
4643	set msg [del-alias-by-id $dbfd $a]
4644	if {$msg ne ""} then {
4645	    return $msg
4646	}
4647    }
4648
4649    #
4650    # Delete all IP addresses
4651    #
4652
4653    set msg [del-all-ip-addresses $dbfd $idrr]
4654    if {$msg ne ""} then {
4655	return $msg
4656    }
4657
4658    #
4659    # Delete all MX
4660    #
4661
4662    set msg [del-mx-by-id $dbfd $idrr]
4663    if {$msg ne ""} then {
4664	return $msg
4665    }
4666
4667    #
4668    # Delete the RR itself (if possible)
4669    #
4670
4671    set msg [del-orphaned-rr $dbfd $idrr]
4672    if {$msg ne ""} then {
4673	return $msg
4674    }
4675
4676    #
4677    # Finished !
4678    #
4679
4680    return ""
4681}
4682
4683#
4684# Delete an RR if nothing points to it (IP address, alias, mail domain, etc.)
4685#
4686# Input:
4687#   - parameters:
4688#	- dbfd : database handle
4689#	- idrr : RR id
4690# Output:
4691#   - return value: empty string or error message
4692#
4693# Note : if the RR is not an orphaned one, it is not deleted and
4694#	an empty string is returned (it is a normal case, not an error).
4695#
4696# History
4697#   2004/02/13 : pda/jean : design
4698#   2010/11/29 : pda      : i18n
4699#
4700
4701proc del-orphaned-rr {dbfd idrr} {
4702    set msg ""
4703    if {[read-rr-by-id $dbfd $idrr trr]} then {
4704	set orphaned 1
4705	foreach x {ip mx aliases cname mailrole mailaddr} {
4706	    if {$trr($x) ne ""} then {
4707		set orphaned 0
4708		break
4709	    }
4710	}
4711
4712	if {$orphaned} then {
4713	    set sql "DELETE FROM dns.rr WHERE idrr = $idrr"
4714	    if {[::pgsql::execsql $dbfd $sql msg]} then {
4715		# it worked, but this function may have modified "msg"
4716		set msg ""
4717	    }
4718	}
4719    }
4720    return $msg
4721}
4722
4723#
4724# Add a new RR
4725#
4726# Input:
4727#   - parameters:
4728#	- dbfd : database handle
4729#	- name : name of RR to create (syntax must be conform to RFC)
4730#	- iddom : domain id
4731#	- idview: view id
4732#	- mac : MAC address, or empty string
4733#	- iddhcpprof : DHCP profile id, or 0
4734#	- idhinfo : HINFO or empty string (default is searched in the database)
4735#	- sendsmtp : 1 if ok to emit with non auth SMTP
4736#	- ttl : TTL value, or -1 for default value
4737#	- comment : commment
4738#	- respname : responsible person name
4739#	- respmail : responsible person mail
4740#	- idcor : user id
4741#	- _trr : in return, will contain RR information
4742# Output:
4743#   - return value: empty string, or error message
4744#   - parameter _trr : see read-rr-by-id
4745#
4746# Warning: name syntax is supposed to be valid. Do not forget to call
4747#	check-name-syntax before calling this function.
4748#
4749# History
4750#   2004/02/13 : pda/jean : design
4751#   2004/08/05 : pda/jean : add mac
4752#   2004/10/05 : pda      : change date format
4753#   2005/04/08 : pda/jean : add dhcpprofil
4754#   2008/07/24 : pda/jean : add sendsmtp
4755#   2010/10/31 : pda      : add ttl
4756#   2010/11/29 : pda      : i18n
4757#   2013/04/05 : pda/jean : add views
4758#
4759
4760proc add-rr {dbfd name iddom idview mac iddhcpprof idhinfo sendsmtp ttl
4761				comment respname respmail idcor _trr} {
4762    upvar $_trr trr
4763
4764    if {$mac eq ""} then {
4765	set qmac NULL
4766    } else {
4767	set qmac "'[::pgsql::quote $mac]'"
4768    }
4769    set qcomment  [::pgsql::quote $comment]
4770    set qrespname [::pgsql::quote $respname]
4771    set qrespmail [::pgsql::quote $respmail]
4772    set hinfodef ""
4773    set hinfoval ""
4774    if {$idhinfo ne ""} then {
4775	set hinfodef "idhinfo,"
4776	set hinfoval "$idhinfo, "
4777    }
4778    if {$iddhcpprof == 0} then {
4779	set iddhcpprof NULL
4780    }
4781    set sql "INSERT INTO dns.rr
4782		    (name, iddom, idview, mac, iddhcpprof, $hinfodef
4783			sendsmtp, ttl, comment, respname, respmail,
4784			idcor)
4785		VALUES
4786		    ('$name', $iddom, $idview, $qmac, $iddhcpprof, $hinfoval
4787			$sendsmtp, $ttl,
4788			'$qcomment', '$qrespname', '$qrespmail',
4789			$idcor)
4790		    "
4791    if {[::pgsql::execsql $dbfd $sql msg]} then {
4792	set msg ""
4793	if {! [read-rr-by-name $dbfd $name $iddom $idview trr]} then {
4794	    set msg [mc "Internal error: '%s' inserted, but not found in database" $name]
4795
4796	}
4797    } else {
4798	set msg [mc "RR addition impossible: %s" $msg]
4799    }
4800    return $msg
4801}
4802
4803#
4804# Add host
4805#
4806# Input:
4807#   - parameters:
4808#	- dbfd : database handle
4809#	- _trr: trr of existing fqdn or empty trr
4810#	- name : name of RR to create (syntax must be conform to RFC)
4811#	- iddom : domain id
4812#	- idview: view id
4813#	- addr: (single) IP address to add
4814#	- mac : MAC address, or empty string
4815#	- iddhcpprof : DHCP profile id, or 0
4816#	- idhinfo : idhinfo (0 for default value)
4817#	- sendsmtp : 1 if ok to emit with non auth SMTP
4818#	- ttl : TTL value, or -1 for default value
4819#	- comment : commment
4820#	- respname : responsible person name
4821#	- respmail : responsible person mail
4822#	- idcor : user id
4823# Output:
4824#   - return value: empty string, or error message
4825#   - parameter _trr: completed RR
4826#
4827# History
4828#   2013/03/28 : pda/jean : shared code between www/cgi/ and utils/
4829#   2013/04/10 : pda/jean : accept only one view
4830#
4831
4832proc add-host {dbfd _trr name iddom idview addr mac iddhcpprof idhinfo sendsmtp ttl comment respname respmail idcor} {
4833    upvar $_trr trr
4834
4835    #
4836    # Handle one of two cases:
4837    # - object does not have an IP address, or
4838    # - it have IP address(es) and user has confirmed
4839    # Insert object in database : (RR + IP addr) or only (IP addr)
4840    #
4841
4842    d dblock {dns.rr dns.rr_ip}
4843
4844    if {$trr(idrr) == ""} then {
4845	#
4846	# Name did not exist, thus we insert a new RR
4847	#
4848	set msg [add-rr $dbfd $name $iddom $idview \
4849			$mac $iddhcpprof $idhinfo $sendsmtp $ttl \
4850			$comment $respname $respmail $idcor trr]
4851	if {$msg ne ""} then {
4852	    d dbabort [mc "add %s" $name] $msg
4853	}
4854
4855    } else {
4856	#
4857	# RR was existing. Host informations may have been modified.
4858	# Update only if needed.
4859	#
4860
4861	if {$trr(ip) eq ""} then {
4862	    #
4863	    # Addition to an existing RR (eg: declare a host when
4864	    # only mail role was existing).
4865	    #
4866	    if {! ($mac eq $trr(mac)
4867			&& $iddhcpprof eq $trr(iddhcpprof)
4868		    	&& $idhinfo eq $trr(idhinfo)
4869		    	&& $sendsmtp eq $trr(sendsmtp)
4870		    	&& $ttl eq $trr(ttl)
4871		    	&& $comment eq $trr(comment)
4872		    	&& $respname eq $trr(respname)
4873		    	&& $respmail eq $trr(respmail))} then {
4874		if {$mac eq ""} then {
4875		    set qmac NULL
4876		} else {
4877		    set qmac "'[::pgsql::quote $mac]'"
4878		}
4879		set qcomment  [::pgsql::quote $comment]
4880		set qrespname [::pgsql::quote $respname]
4881		set qrespmail [::pgsql::quote $respmail]
4882		if {$iddhcpprof == 0} then {
4883		    set iddhcpprof NULL
4884		}
4885		set sql "UPDATE dns.rr SET
4886					mac = $qmac,
4887					iddhcpprof = $iddhcpprof,
4888					idhinfo = $idhinfo,
4889					sendsmtp = $sendsmtp,
4890					ttl = $ttl,
4891					comment = '$qcomment',
4892					respname = '$qrespname',
4893					respmail = '$qrespmail'
4894				    WHERE idrr = $trr(idrr)"
4895		if {! [::pgsql::execsql $dbfd $sql msg]} then {
4896		    d dbabort [mc "modify %s" [mc "host information"]] $msg
4897		}
4898	    }
4899	}
4900    }
4901
4902    set sql "INSERT INTO dns.rr_ip (idrr, addr) VALUES ($trr(idrr), '$addr')"
4903    if {! [::pgsql::execsql $dbfd $sql msg]} then {
4904       d dbabort [mc "add %s" $addr] $msg
4905    }
4906
4907    #
4908    # Keep a note about user
4909    #
4910
4911    set msg [touch-rr $dbfd $trr(idrr)]
4912    if {$msg ne ""} then {
4913	d dbabort [mc "modify %s" [mc "RR"]] $msg
4914    }
4915
4916    set domain [u domainname $iddom]
4917
4918    d dbcommit [mc "add %s" "$name.$domain"]
4919    d writelog "addhost" "add $name.$domain ($addr)/[u viewname $idview]"
4920
4921    return ""
4922}
4923
4924#
4925# Add alias
4926#
4927# Input:
4928#   - parameters:
4929#	- dbfd : database handle
4930#	- _trr: trr of existing fqdn or empty trr
4931#	- name : name of RR to create (syntax must be conform to RFC)
4932#	- domain : domain name
4933#	- idview: view id
4934#	- nameref: name of existing host
4935#	- domainref: domain name of existing host
4936#	- idcor : user id
4937# Output:
4938#   - return value: empty string, or error message
4939#   - parameter _trr: completed RR
4940#
4941# History
4942#   2013/03/28 : pda/jean : shared code between www/cgi/ and utils/
4943#   2013/04/10 : pda/jean : accept only one view
4944#
4945
4946proc add-alias {dbfd name domain idview nameref domainref idcor} {
4947    #
4948    # Check alias and host permissions
4949    #
4950
4951    set msg [check-authorized-host $dbfd $idcor $name $domain $idview trr "alias"]
4952    if {$msg ne ""} then {
4953	return $msg
4954    }
4955    set iddom $trr(iddom)
4956
4957    set msg [check-authorized-host $dbfd $idcor $nameref $domainref $idview trrref "existing-host"]
4958    if {$msg ne ""} then {
4959	return $msg
4960    }
4961
4962    #
4963    # All test are ok, we just have to insert new alias
4964    #
4965
4966    d dblock {dns.rr dns.rr_cname}
4967
4968    #
4969    # This name was unknown, insert a new RR for new alias name
4970    #
4971
4972    if {$trr(idrr) eq ""} then {
4973	set msg [add-rr $dbfd $name $iddom $idview "" 0 "" 0 -1 "" "" "" $idcor trr]
4974	if {$msg ne ""} then {
4975	    d dbabort [mc "add %s" $name] $msg
4976	}
4977    }
4978
4979    #
4980    # Add alias link between alias and host
4981    #
4982
4983    set sql "INSERT INTO dns.rr_cname (idrr, cname)
4984			VALUES ($trr(idrr), $trrref(idrr))"
4985    if {! [::pgsql::execsql $dbfd $sql msg]} then {
4986	d dbabort [mc "add %s" [mc "alias"]] $msg
4987    }
4988
4989    d dbcommit [mc "add %s" "$name.$domain"]
4990    d writelog "addalias" "add alias $name.$domain/[u viewname $idview] -> $nameref.$domainref"
4991
4992    return ""
4993}
4994
4995#
4996# Delete a host or an alias
4997#
4998# Input:
4999#   - parameters:
5000#	- dbfd: database handle
5001#	- trr: RR of name to remove
5002#	- idview: view id
5003# Output:
5004#   - return value: empty string, or error message
5005#
5006# Note: we assume that an SQL transaction is already started
5007#    by the calling procedure. No abort is done in this procedure.
5008#
5009# History
5010#   2013/03/28 : pda/jean : shared code between www/cgi/ and utils/
5011#
5012
5013proc del-host {dbfd _trr idview} {
5014    upvar $_trr trr
5015
5016    set fqdn "$trr(name).$trr(domain)"
5017    set vn [u viewname $idview]
5018
5019    set cname [rr-cname-by-view trr $idview]
5020    if {$cname ne ""} then {
5021	set msg [del-alias-by-id $dbfd $trr(idrr)]
5022	if {$msg ne ""} then {
5023	    return $msg
5024	}
5025
5026	set p "?"
5027	if {[read-rr-by-id $dbfd $cname tc]} then {
5028	    set p "$tc(name).$tc(domain)"
5029	}
5030	d writelog "delalias" "delete alias $fqdn/$vn -> $p"
5031    } else {
5032	#
5033	# This is not an alias: delete all RR dependancies:
5034	# - aliases pointing this object
5035	# - MX
5036	# - IP addresses
5037	#
5038	set msg [del-rr-and-dependancies $dbfd trr]
5039	if {$msg ne ""} then {
5040	    return $msg
5041	}
5042	d writelog "delname" "delete all of $fqdn/$vn"
5043    }
5044
5045    return ""
5046}
5047
5048#
5049# Delete one IP address
5050#
5051# Input:
5052#   - parameters:
5053#	- dbfd: database handle
5054#	- addr: IP address to remove
5055#	- trr: RR in which this address is located
5056#	- idview: view id
5057#	- _delobj: will contain in return the deleted object
5058# Output:
5059#   - return value: empty string, or error message
5060#   - parameter delobj: an IP address or a name if the whole object has
5061#	been removed
5062#
5063# Note: we assume that an SQL transaction is already started
5064#    by the calling procedure. No abort is done in this procedure.
5065#
5066# History
5067#   2013/03/28 : pda/jean : shared code between www/cgi/ and utils/
5068#
5069
5070proc del-ip {dbfd addr _trr idview _delobj} {
5071    upvar $_trr trr
5072    upvar $_delobj delobj
5073
5074    set fqdn "$trr(name).$trr(domain)"
5075    set vn [u viewname $idview]
5076
5077    set lip [rr-ip-by-view trr $idview]
5078    if {[llength $lip] > 1} then {
5079	#
5080	# Only delete one of these addresses
5081	#
5082
5083	set sql "DELETE FROM dns.rr_ip i
5084			USING dns.rr r
5085			WHERE r.idrr = i.idrr
5086			    AND i.addr = '$addr'
5087			    AND r.idview = $idview"
5088	if {! [::pgsql::execsql $dbfd $sql msg]} then {
5089	    return $msg
5090	}
5091
5092	set msg [touch-rr $dbfd $trr(idrr)]
5093	if {$msg ne ""} then {
5094	    return $msg
5095	}
5096
5097	d writelog "deladdr" "delete address $addr from $fqdn/$vn"
5098	set delobj $addr
5099    } else {
5100	#
5101	# Delete the whole object
5102	#
5103
5104	set msg [del-rr-and-dependancies $dbfd trr]
5105	if {$msg ne ""} then {
5106	    return $msg
5107	}
5108	d writelog "deladdr" "delete address $addr -> delete all $fqdn/$vn"
5109	set delobj $fqdn
5110    }
5111
5112    return ""
5113}
5114
5115#
5116# Update references to a RR when a new RR is created after a host renaming
5117#
5118# Input:
5119#   - parameters:
5120#	- dbfd : database handle
5121#	- oidrr: id of old RR
5122#	- nidrr: id of new RR
5123#	- idview: view id
5124# Output:
5125#   - return value: empty string, or error message
5126#
5127# History
5128#   2012/12/07 : pda/jean : design
5129#
5130
5131proc update-host-refs {dbfd oidrr nidrr} {
5132    set sql {}
5133    lappend sql "UPDATE dns.mail_role
5134			    SET mboxhost = $nidrr
5135			    WHERE mboxhost = $oidrr"
5136    lappend sql "UPDATE dns.rr_ip
5137			    SET idrr = $nidrr
5138			    WHERE idrr = $oidrr"
5139    lappend sql "UPDATE dns.rr_cname
5140			    SET cname = $nidrr
5141			    WHERE cname = $oidrr"
5142    lappend sql "UPDATE dns.rr_mx
5143			    SET mx = $nidrr
5144			    WHERE mx = $oidrr"
5145    lappend sql "UPDATE dns.relay_dom
5146			    SET mx = $nidrr
5147			    WHERE mx = $oidrr"
5148    set sql [join $sql ";"]
5149    if {[::pgsql::execsql $dbfd $sql msg]} then {
5150	set msg ""
5151    }
5152    return $msg
5153}
5154
5155#
5156# Update date and user id when a RR is modified
5157#
5158# Input:
5159#   - parameters:
5160#	- dbfd : database handle
5161#	- idrr : RR id
5162# Output:
5163#   - return value: empty string or error message
5164#
5165# History
5166#   2002/05/03 : pda/jean : design
5167#   2004/10/05 : pda      : change date format
5168#   2010/11/13 : pda      : use effective uid
5169#   2010/11/29 : pda      : i18n
5170#
5171
5172proc touch-rr {dbfd idrr} {
5173    set date [clock format [clock seconds]]
5174    set idcor [lindex [d euid] 1]
5175    set sql "UPDATE dns.rr SET idcor = $idcor, date = '$date' WHERE idrr = $idrr"
5176    set msg ""
5177    if {! [::pgsql::execsql $dbfd $sql msg]} then {
5178	set msg [mc "RR update impossible: %s" $msg]
5179    }
5180    return $msg
5181}
5182
5183#
5184# Get group ids of all allowed groups for a list of IP addresses
5185#
5186# Input:
5187#   - parameters:
5188#	- dbfd : database handle
5189#	- laddr : IP addresses to test
5190# Output:
5191#   - return value: list of group ids
5192#
5193# History
5194#   2013/02/27 : pda/jean : design
5195#
5196
5197proc allowed-groups {dbfd laddr} {
5198    array set algrp {}
5199
5200    foreach addr $laddr {
5201	#
5202	# Look for groups which have access to this IP address.
5203	#
5204
5205	set sql "SELECT g.idgrp
5206			FROM global.nmgroup g, dns.p_network p, dns.network n
5207			WHERE g.idgrp = p.idgrp
5208			    AND p.idnet = n.idnet
5209			    AND ('$addr' <<= n.addr4 OR '$addr' <<= n.addr6)
5210			    "
5211	set lidgrp {}
5212	pg_select $dbfd $sql tab {
5213	    lappend lidgrp $tab(idgrp)
5214	}
5215
5216	#
5217	# Among selected groups, search for those who have access to
5218	# this host (checking all other permissions).
5219	#
5220
5221	foreach idgrp $lidgrp {
5222	    set sql "SELECT dns.check_ip_grp ('$addr', $idgrp) AS ok"
5223	    pg_select $dbfd $sql tab {
5224		if {$tab(ok) eq "t"} then {
5225		    set algrp($idgrp) {}
5226		}
5227	    }
5228	}
5229    }
5230
5231    return [array names algrp]
5232}
5233
5234#
5235# Display a RR with HTML
5236#
5237# Input:
5238#   - parameters:
5239#	- dbfd : database handle
5240#	- idrr : RR id to search for, or -1 if _trr is already initialized
5241#	- _trr : empty array, or initialized array (id idrr=-1)
5242#	- idview : view id, or empty string to get all views
5243#	- rrtmpl: URL template for some fields (see below)
5244# Output:
5245#   - return value: empty string or error message
5246#   - parameter _trr : see read-rr-by-id
5247#   - global variables :
5248#	- libconf(tabmachine) : array specification
5249#
5250# Note:
5251#  - rrtmpl is a string ready for "array set" which has the following
5252#	structure {key tmpl key tmpl ...}
5253#	where key is one of:
5254#		ip
5255#		allowed-groups
5256#		<may be more in the future>
5257#	and tmpl has the following format:
5258#		{url {formkey formval} {formkey formval}
5259#	where url is the script name or any url (http://another.host/a/path)
5260#	and formkey/formval are CGI parameters, where formval is formatted
5261#	with value depending upon key:
5262#		ip: %1$s <- ip, %2$s <- idview
5263#		allowed-groups: %1$s <- groupname, %2$s <- ""
5264#
5265# History
5266#   2008/07/25 : pda/jean : design
5267#   2010/10/31 : pda      : add ttl
5268#   2010/11/29 : pda      : i18n
5269#   2012/10/31 : pda/jean : add views
5270#   2012/11/20 : pda/jean : add view filter to display a single view
5271#   2013/03/06 : pda/jean : add rrtmpl
5272#
5273
5274proc display-rr {dbfd idrr _trr idview rrtmpl} {
5275    global libconf
5276    upvar $_trr trr
5277
5278    #
5279    # Read RR if needed
5280    #
5281
5282    if {$idrr != -1 && [read-rr-by-id $dbfd $idrr trr] == -1} then {
5283	return ""
5284    }
5285
5286    #
5287    # Display all fields
5288    #
5289
5290    set lines {}
5291
5292    #
5293    # Special case if it is a CNAME in the view
5294    #
5295
5296    if {$idview ne ""} then {
5297	set cname [rr-cname-by-view trr $idview]
5298	if {$cname ne ""} then {
5299	    set fqdn "$trr(name).$trr(domain)"
5300	    if {! [read-rr-by-id $dbfd $cname tc]} then {
5301		return [mc {Cannot read host-id %s} $idalias]
5302	    }
5303
5304	    set fqdn2 "$tc(name).$tc(domain)"
5305	    lappend lines [list Normal [mc "Alias name"] $fqdn]
5306	    lappend lines [list Normal [mc "Points to"] $fqdn2]
5307	}
5308    }
5309
5310    #
5311    # Standard case
5312    #
5313
5314    if {$lines eq ""} then {
5315	# name
5316	lappend lines [list Normal [mc "Name"] "$trr(name).$trr(domain)"]
5317
5318	# IP address(es)
5319	set lip [rr-ip-by-view trr $idview]
5320	set nip [llength $lip]
5321	if {$nip <= 1} then {
5322	    set at [mc "IP address"]
5323	} else {
5324	    set at [mc "IP addresses"]
5325	}
5326	if {$nip == 0} then {
5327	    set aa [mc "(none)"]
5328	} else {
5329	    set aa {}
5330	    foreach ip $lip {
5331		lappend aa [get-rr-tmpl "ip" $rrtmpl $ip $ip $idview]
5332	    }
5333	    set aa [join $aa ", "]
5334	}
5335	lappend lines [list Normal $at $aa]
5336
5337	# MAC address
5338	lappend lines [list Normal [mc "MAC address"] $trr(mac)]
5339
5340	# DHCP profile
5341	lappend lines [list Normal [mc "DHCP profile"] $trr(dhcpprof)]
5342
5343	# Machine type
5344	lappend lines [list Normal [mc "Type"] $trr(hinfo)]
5345
5346	# Right to emit with non auth SMTP : display only if it is used
5347	# (i.e. if there is at least one group wich owns this right)
5348	set sql "SELECT COUNT(*) AS nsmtp FROM global.nmgroup WHERE p_smtp = 1"
5349	set nsmtp 0
5350	pg_select $dbfd $sql tab {
5351	    set nsmtp $tab(nsmtp)
5352	}
5353	if {$nsmtp > 0} then {
5354	    if {$trr(sendsmtp)} then {
5355		set sendsmtp [mc "Yes"]
5356	    } else {
5357		set sendsmtp [mc "No"]
5358	    }
5359	    lappend lines [list Normal [mc "SMTP emit right"] $sendsmtp]
5360	}
5361
5362	# TTL : display only if it used
5363	# (i.e. if there is at least one group wich owns this right and there
5364	# is a value)
5365	set sql "SELECT COUNT(*) AS nttl FROM global.nmgroup WHERE p_ttl = 1"
5366	set nttl 0
5367	pg_select $dbfd $sql tab {
5368	    set nttl $tab(nttl)
5369	}
5370	if {$nttl > 0} then {
5371	    set ttl $trr(ttl)
5372	    if {$ttl != -1} then {
5373		lappend lines [list Normal [mc "TTL"] $ttl]
5374	    }
5375	}
5376
5377	# comment
5378	lappend lines [list Normal [mc "Comment"] $trr(comment)]
5379
5380	# responsible (name)
5381	lappend lines [list Normal [mc "Responsible (name)"] $trr(respname)]
5382
5383	# responsible (mail)
5384	lappend lines [list Normal [mc "Responsible (mail)"] $trr(respmail)]
5385
5386	# aliases
5387	set la {}
5388	if {$idview eq ""} then {
5389	    foreach va $trr(aliases) {
5390		lassign $va idview idalias
5391		if {[read-rr-by-id $dbfd $idalias ta]} then {
5392		    lappend la "$ta(name).$ta(domain) ([u viewname $idview])"
5393		}
5394	    }
5395	} else {
5396	    foreach idalias [rr-aliases-by-view trr $idview] {
5397		if {[read-rr-by-id $dbfd $idalias ta]} then {
5398		    lappend la "$ta(name).$ta(domain)"
5399		}
5400	    }
5401	}
5402	if {[llength $la] > 0} then {
5403	    lappend lines [list Normal [mc "Aliases"] [join $la " "]]
5404	}
5405
5406	# mail addresses recognized by this host
5407	set la {}
5408	foreach i [rr-mailaddr-by-view trr $idview] {
5409	    lassign $i idmailaddr idviewa
5410	    if {[read-rr-by-id $dbfd $idmailaddr ta]} then {
5411		lappend la "$ta(name).$ta(domain)/[u viewname $idviewa]"
5412	    }
5413	}
5414	if {[llength $la] > 0} then {
5415	    lappend lines [list Normal [mc "Mail addresses"] [join $la " "]]
5416	}
5417
5418	#
5419	# Allowed groups
5420	#
5421
5422	set lidgrp [allowed-groups $dbfd $lip]
5423	set lg {}
5424	foreach idgrp $lidgrp {
5425	    set g [u groupname $idgrp]
5426	    lappend lg [get-rr-tmpl "allowed-groups" $rrtmpl $g $g ""]
5427	}
5428	set lg [lsort $lg]
5429	lappend lines [list Normal [mc "Allowed groups"] [join $lg " "]]
5430    }
5431
5432    set html [::arrgen::output "html" $libconf(tabmachine) $lines]
5433    return $html
5434}
5435
5436proc get-rr-tmpl {key rrtmpl text arg1 arg2} {
5437    array set tmpl $rrtmpl
5438
5439    set text [::webapp::html-string $text]
5440    if {[info exists tmpl($key)]} then {
5441	set uarg [lreplace $tmpl($key) 0 0]
5442	set uarg [format $uarg $arg1 $arg2]
5443	d urlset "" [lindex $tmpl($key) 0] $uarg
5444	set url [d urlget ""]
5445	set link [::webapp::helem "a" $text "href" $url]
5446    } else {
5447	set link $text
5448    }
5449    return $link
5450}
5451
5452#
5453# Generates HTML code for a host description initially invisible
5454# and a link to toggle its visibility.
5455#
5456# Input:
5457#   - parameters:
5458#	- dbfd: database handle
5459#	- _trr: initialized array (see read-rr-by-id)
5460#	- idview: view id in which this host must be shown
5461#	- rrtmpl: URL template for some fields (see display-rr)
5462# Output:
5463#   - return value: list {<link> <desc>} where:
5464#	- link is the HTML code for the link to the host name
5465#	- desc is the HTML code for the host information display
5466#
5467# Note: this function needs an "invdisp" Javascript function in the
5468#   HTML page
5469#
5470# History
5471#   2012/11/20 : pda/jean : design
5472#   2012/11/29 : pda/jean : move to a library function
5473#   2013/03/06 : pda/jean : add rrtmpl
5474#
5475
5476proc display-rr-masked {dbfd _trr idview rrtmpl} {
5477    upvar $_trr trr
5478
5479    h mask-next
5480    set link [h mask-link "$trr(name).$trr(domain)"]
5481    set desc [h mask-text [display-rr $dbfd -1 trr $idview $rrtmpl]]
5482    return [list $link $desc]
5483}
5484
5485##############################################################################
5486# Read domains
5487##############################################################################
5488
5489#
5490# Read all domains from database
5491#
5492# Input:
5493#   - parameters:
5494#	- dbfd: database handle
5495#	- _tabdom: array to fill with domain names
5496#	- _tabid: array to fill with domain ids
5497# Output:
5498#   - parameter _tabdom: tabdom(<domainname>) <id>
5499#   - parameter _tabid: tabdom(<id>) <domainname>
5500#
5501# History
5502#   2011/03/20 : pda      : place in library
5503#
5504
5505proc read-all-domains {dbfd _tabdom _tabid} {
5506    upvar $_tabdom tabdom
5507    upvar $_tabid  tabid
5508
5509    set sql "SELECT name, iddom FROM dns.domain"
5510    pg_select $dbfd $sql tab {
5511	set tabdom($tab(name)) $tab(iddom)
5512	set tabid($tab(iddom)) $tab(name)
5513    }
5514}
5515
5516##############################################################################
5517# Syntax check
5518##############################################################################
5519
5520#
5521# Check FQDN syntax according to RFC 1035.
5522#
5523# Input:
5524#   - parameters:
5525#	- dbfd : database handle
5526#	- fqdn : name to test
5527#	- _name : host name in return
5528#	- _domain : host domain in return
5529#	- _iddom : domain id in return (leave empty to not check domain existence)
5530# Output:
5531#   - return value: empty string or error message
5532#   - parameter _name : host name found
5533#   - parameter _domain : host domain found
5534#   - parameter _iddom : domain id found, or -1 if error
5535#
5536# History
5537#   2004/09/21 : pda/jean : design
5538#   2004/09/29 : pda/jean : add _domain parameter
5539#   2010/11/29 : pda      : i18n
5540#   2011/02/18 : pda      : iddom is optional
5541#
5542
5543proc check-fqdn-syntax {dbfd fqdn _name _domain {_iddom {}}} {
5544    upvar $_name name
5545    upvar $_domain domain
5546
5547    if {! [regexp {^([^\.]+)\.(.*)$} $fqdn bidon name domain]} then {
5548	return [mc "Invalid FQDN '%s'" $fqdn]
5549    }
5550
5551    set msg [check-name-syntax $name]
5552    if {$msg ne ""} then {
5553	return $msg
5554    }
5555
5556    if {$_iddom ne ""} then {
5557	upvar $_iddom iddom
5558
5559	set iddom [read-domain $dbfd $domain]
5560	if {$iddom < 0} then {
5561	    return [mc "Invalid domain '%s'" $domain]
5562	}
5563    }
5564
5565    return ""
5566}
5567
5568#
5569# Check host name syntax (first part of a FQDN) according to RFC 1035
5570#
5571# Input:
5572#   - parameters:
5573#	- name : name to test
5574# Output:
5575#   - return value: empty string or error message
5576#
5577# History
5578#   2002/04/11 : pda/jean : design
5579#   2010/11/29 : pda      : i18n
5580#
5581
5582proc check-name-syntax {name} {
5583    # general case: a letter-or-digit at the beginning, a letter-or-digit
5584    # at the end (minus forbidden at the end) and letter-or-digit-or-minus
5585    # between.
5586    set re1 {[a-zA-Z0-9][-a-zA-Z0-9]*[a-zA-Z0-9]}
5587    # particular case: only one letter
5588    set re2 {[a-zA-Z0-9]}
5589
5590    if {[regexp "^$re1$" $name] || [regexp "^$re2$" $name]} then {
5591	set msg ""
5592    } else {
5593	set msg [mc "Invalid name '%s'" $name]
5594    }
5595
5596    return $msg
5597}
5598
5599#
5600# Check IP address (IPv4 or IPv6) syntax
5601#
5602# Input:
5603#   - parameters:
5604#	- dbfd : database handle
5605#	- addr : address to test
5606#	- type : "inet", "cidr", "loosecidr", "macaddr", "inet4", "cidr4"
5607# Output:
5608#   - return value: empty string or error message
5609#
5610# Note :
5611#   - type "cidr" is strict, "host" bits must be 0 (i.e.: "1.1.1.0/24"
5612#	is valid, but not "1.1.1.1/24")
5613#   - type "loosecidr" accepts "host" bits set to 1
5614#
5615# History
5616#   2002/04/11 : pda/jean : design
5617#   2002/05/06 : pda/jean : add type cidr
5618#   2002/05/23 : pda/jean : accept simplified cidr (a.b/x)
5619#   2004/01/09 : pda/jean : add IPv6 et radical simplification
5620#   2004/10/08 : pda/jean : add inet4
5621#   2004/10/20 : jean     : forbid / for anything else than cidr type
5622#   2008/07/22 : pda      : add type loosecidr (accepts /)
5623#   2010/10/07 : pda      : add type cidr4
5624#
5625
5626proc check-ip-syntax {dbfd addr type} {
5627
5628    switch $type {
5629	inet4 {
5630	    set cast "inet"
5631	    set fam  4
5632	}
5633	cidr4 {
5634	    set cast "cidr"
5635	    set type "cidr"
5636	    set fam  4
5637	}
5638	loosecidr {
5639	    set cast "inet"
5640	    set fam ""
5641	}
5642	default {
5643	    set cast $type
5644	    set fam ""
5645	    set msg "?"
5646	}
5647    }
5648    set addr [::pgsql::quote $addr]
5649    set sql "SELECT $cast\('$addr'\) ;"
5650    set r ""
5651    if {[::pgsql::execsql $dbfd $sql msg]} then {
5652	if {$fam ne ""} then {
5653	    pg_select $dbfd "SELECT family ('$addr') AS fam" tab {
5654		if {$tab(fam) != $fam} then {
5655		    set r [mc {'%1$s' is not a valid IPv%2$s address} $addr $fam]
5656		}
5657	    }
5658	}
5659	if {! ($type eq "cidr" || $type eq "loosecidr")} then {
5660	    if {[regexp {/}  $addr ]} then {
5661		set r [mc "The '/' character is not valid in the address '%s'" $addr]
5662	    }
5663	}
5664    } else {
5665	if {$type eq "macaddr"} then {
5666	    set r [mc "Invalid syntax for MAC address '%s'" $addr]
5667	} else {
5668	    set r [mc "Invalid syntax for IP address '%s'" $addr]
5669	}
5670    }
5671    return $r
5672}
5673
5674#
5675# Check MAC address syntax
5676#
5677# Input:
5678#   - parameters:
5679#	- addr : address to test
5680# Output:
5681#   - return value: empty string or error message
5682#
5683# History
5684#   2004/08/04 : pda/jean : design
5685#   2010/11/29 : pda      : i18n
5686#
5687
5688proc check-mac-syntax {dbfd mac} {
5689    return [check-ip-syntax $dbfd $mac "macaddr"]
5690}
5691
5692#
5693# Check a DHCP profile id
5694#
5695# Input:
5696#   - parameters:
5697#	- dbfd : database handle
5698#	- iddhcpprof : id of DHCP profile, or 0
5699#	- _dhcpprof : variable contenant en retour le nom du profil
5700#	- _msgvar : in return : error message
5701# Output:
5702#   - return value: 1 if ok, 0 if error
5703#   - _dhcpprof : name of found profile (or "No profile")
5704#   - _msg : error message, if any
5705#
5706# History
5707#   2005/04/08 : pda/jean : design
5708#   2010/11/29 : pda      : i18n
5709#
5710
5711proc check-iddhcpprof {dbfd iddhcpprof _dhcpprof _msg} {
5712    upvar $_dhcpprof dhcpprof
5713    upvar $_msg msg
5714
5715    set msg ""
5716
5717    if {! [regexp -- {^[0-9]+$} $iddhcpprof]} then {
5718	set msg [mc "Invalid syntax '%s' for DHCP profile" $iddhcpprof]
5719    } else {
5720	if {$iddhcpprof != 0} then {
5721	    set sql "SELECT name FROM dns.dhcpprofile
5722				WHERE iddhcpprof = $iddhcpprof"
5723	    set msg [mc "Invalid DHCP profile '%s'" $iddhcpprof]
5724	    pg_select $dbfd $sql tab {
5725		set dhcpprof $tab(name)
5726		set msg ""
5727	    }
5728	} else {
5729	    set dhcpprof [mc "No profile"]
5730	}
5731    }
5732
5733    return [string equal $msg ""]
5734}
5735
5736##############################################################################
5737# View validation
5738##############################################################################
5739
5740#
5741# Checks if the selected views are authorized for this user
5742#
5743# Input:
5744#   - parameters:
5745#	- views : list of view ids given by the user
5746# Output:
5747#   - return value: empty string or error message
5748#
5749# History
5750#   2012/10/30 : pda/jean : design
5751#   2012/10/31 : pda/jean : use nmuser class
5752#
5753
5754proc check-views {views} {
5755    set msg ""
5756
5757    if {[llength $views] == 0} then {
5758	set msg [mc "You must select at least one view"]
5759
5760    } else {
5761	#
5762	# Check authorized views
5763	#
5764
5765	set bad {}
5766	foreach id $views {
5767	    if {! [u isallowedview $id]} then {
5768		set name [u viewname $id]
5769		if {$name eq ""} then {
5770		    set name $id
5771		}
5772		lappend bad $name
5773	    }
5774	}
5775
5776	if {[llength $bad]> 0} then {
5777	    set bad [join $bad ", "]
5778	    set msg [mc "You don't have access to these views: %s" $bad]
5779	}
5780    }
5781
5782    return $msg
5783}
5784
5785#
5786# Filter given view ids for host/address deletion/modification
5787#
5788# Input:
5789#   - dbfd: database handle
5790#   - _tabuid: user characteristics
5791#   - mode: type of object ("host", "host-or-alias", "addr" or "mailrole")
5792#		to delete/modify
5793#   - object: FQDN or IP address
5794#   - idviews: list of idviews specified by user, may be empty for all views
5795#   - _chkv: contains, in return, parameters of filtered views
5796# Output:
5797#   - return value: empty string or error message
5798#   - array chkv:
5799#	chkv(<idview>) = {<viewname> <errmsg or ""> <trr-ready-for-array-set>}
5800#	chkv(idviews) = list of checked view ids
5801#	chkv(ok) = list of view ids ok
5802#	chkv(err) = list of view ids in error
5803#
5804# Note:
5805#   - "host" and "addr" modes are for host edition
5806#	object may be a fqdn or an IP address
5807#   - "host-or-alias" and "addr" modes are for host deletion
5808#	object may be a fqdn or an IP address
5809#   - "mailrole" mode is for mail role edition
5810#	object must be a fqdn
5811#
5812# History
5813#   2012/11/14 : pda/jean : design
5814#   2012/11/29 : pda/jean : isolate as a library function
5815#   2012/12/07 : pda/jean : generalization
5816#   2013/03/13 : pda/jean : distinguish alias case
5817#
5818
5819proc filter-views {dbfd _tabuid mode object idviews _chkv} {
5820    upvar $_tabuid tabuid
5821    upvar $_chkv chkv
5822
5823    set chkv(ok) {}
5824    set chkv(err) {}
5825
5826    #
5827    # Are views selected?
5828    #
5829
5830    set nviews [llength $idviews]
5831    if {$nviews == 0} then {
5832	#
5833	# No view selected by user.  We must check all our views
5834	# in order to search deletion/modification candidates.
5835	#
5836	set myviewids [u myviewids]
5837	if {[llength $myviewids] == 0} then {
5838	    return [mc "Sorry, but you do not have access to any view"]
5839	}
5840    } else {
5841	#
5842	# User has selected one or more views. This is a confirmation.
5843	#
5844	set myviewids $idviews
5845	set msg [check-views $myviewids]
5846	if {$msg ne ""} then {
5847	    return $msg
5848	}
5849    }
5850
5851    #
5852    # Split FQDN into name and domain
5853    #
5854    if {$mode in {host host-or-alias mailrole}} then {
5855	set msg [check-fqdn-syntax $dbfd $object name domain]
5856	if {$msg ne ""} then {
5857	    return $msg
5858	}
5859    }
5860
5861    #
5862    # Check object in all views
5863    #
5864
5865    set nok 0
5866    set nerr 0
5867    set mvi {}
5868    foreach idview $myviewids {
5869	set vn [u viewname $idview]
5870
5871	set found 0
5872	set err 0
5873
5874	catch {unset trr}
5875
5876	switch $mode {
5877	    host -
5878	    host-or-alias {
5879		set found 1
5880		set msg [check-authorized-host $dbfd $tabuid(idcor) $name $domain $idview trr "del-name"]
5881
5882		if {[info exists trr(idrr)] && $trr(idrr) eq ""} then {
5883		    #
5884		    # Name does not exist in this view
5885		    #
5886		    set found 0
5887		} elseif {$msg ne ""} then {
5888		    set err 1
5889		} else {
5890		    #
5891		    # Is it an alias in this view?
5892		    #
5893
5894		    set cname [rr-cname-by-view trr $idview]
5895		    set ip [rr-ip-by-view trr $idview]
5896		    if {$mode eq "host-or-alias"} then {
5897			#
5898			# mode == "host-or-alias"
5899			# If it is not an alias, there must be at
5900			# least an IP address
5901			#
5902			if {$cname eq "" && $ip eq ""} then {
5903			    set msg [mc {Name '%1$s' is not a host in view '%2$s'} $object $vn]
5904			    set err 1
5905			}
5906		    } else {
5907			#
5908			# mode == "host"
5909			# It must not be an alias and it must have at
5910			# least an IP address
5911			#
5912			if {$cname ne ""} then {
5913			    set msg [mc {Name '%1$s' is an alias in view '%2$s'} $object $vn]
5914			    set err 1
5915			} elseif {$ip eq ""} then {
5916			    set msg [mc {Name '%1$s' is not a host in view '%2$s'} $object $vn]
5917			    set err 1
5918			}
5919		    }
5920		}
5921	    }
5922	    addr {
5923		#
5924		# IP address. Check that this address exists and get
5925		# all stored informations
5926		#
5927
5928		if {[read-rr-by-ip $dbfd $object $idview trr]} then {
5929		    #
5930		    # Check access to this name
5931		    #
5932
5933		    set found 1
5934		    set name   $trr(name)
5935		    set domain $trr(domain)
5936		    set msg [check-authorized-host $dbfd $tabuid(idcor) $name $domain $idview bidon "del-name"]
5937		    if {$msg ne ""} then {
5938			set err 1
5939		    }
5940		}
5941	    }
5942	    mailrole {
5943		set found 1
5944		set msg [check-authorized-host $dbfd $tabuid(idcor) $name $domain $idview trr "del-mailaddr"]
5945
5946		if {$msg ne ""} then {
5947		    set err 1
5948		}
5949	    }
5950	    default {
5951		return "Internal error: unknown mode '$mode'"
5952	    }
5953	}
5954
5955	if {$found} then {
5956	    if {$err} then {
5957		set chkv($idview) [list $vn $msg [array get trr]]
5958		lappend chkv(err) $idview
5959		incr nerr
5960	    } else {
5961		set chkv($idview) [list $vn "" [array get trr]]
5962		lappend chkv(ok) $idview
5963		incr nok
5964	    }
5965	    lappend mvi $idview
5966	}
5967    }
5968    set myviewids $mvi
5969
5970    #
5971    # If asked for a name, check that name exists
5972    #
5973
5974    if {$mode in {host host-or-alias} && $nok + $nerr == 0} then {
5975	return [mc "Name '%s' does not exist" $object]
5976    }
5977
5978    if {$mode eq "addr" && $nok + $nerr == 0} then {
5979	return [mc "Address '%s' not found" $object]
5980    }
5981
5982    #
5983    # Check that :
5984    # - there is at least one view in which we can delete/modify a name
5985    # - there is no view in error, if some views are specified
5986    #
5987
5988    if {$nok == 0 || ($nviews && $nerr > 0)} then {
5989	set msg ""
5990	foreach idview $myviewids {
5991	    lassign $chkv($idview) vn m t
5992	    if {$m ne ""} then {
5993		append msg [mc {Error detected in view '%1$s': %2$s} $vn $m]
5994		append msg "\n"
5995	    }
5996	}
5997	return $msg
5998    }
5999
6000    #
6001    # At this point, myviewids contains:
6002    # - all user's view ids (good and in error) if confirmation is needed
6003    # - only good view ids if user has already confirmed
6004    # Views which do not include the searched IP address are not in myviewids
6005    #
6006
6007    set chkv(idviews) $myviewids
6008
6009    return ""
6010}
6011
6012#
6013# HTML code for host/idview selection page
6014#
6015# Input:
6016#   - _chkv: parameters of filtered views
6017#   - next: script to call
6018# Output:
6019#   - return value: HTML code ready to be inserted in page
6020#
6021# History
6022#   2012/12/19 : pda/jean : design
6023#
6024
6025proc html-select-view {_chkv next} {
6026    upvar $_chkv chkv
6027
6028    set idviews $chkv(idviews)
6029
6030    set html ""
6031    foreach idview $idviews {
6032	lassign $chkv($idview) vn msg t
6033
6034	if {$msg eq ""} then {
6035	    array unset trr
6036	    array set trr $t
6037
6038	    set fqdn "$trr(name).$trr(domain)"
6039
6040	    d urlset "" $next [list \
6041				    [list "action" "edit"] \
6042				    [list "name" $trr(name)] \
6043				    [list "domain" $trr(domain)] \
6044				    [list "idview" $idview] \
6045				]
6046	    d urladdnext ""
6047	    set url [d urlget ""]
6048
6049	    set a [mc {<a href="%1$s">Modify '%2$s'</a> in view '%3$s'} $url $fqdn $vn]
6050	    append html [::webapp::helem "li" $a]
6051	    append html "\n"
6052	}
6053    }
6054    set html [::webapp::helem "ul" $html]
6055
6056    return $html
6057}
6058
6059##############################################################################
6060# Domain validation
6061##############################################################################
6062
6063#
6064# Search for a domain name in the database
6065#
6066# Input:
6067#   - parameters:
6068#       - dbfd : database handle
6069#	- domain : domain to search (not terminated by a ".")
6070# Output:
6071#   - return value: id of domain if found, -1 if not found
6072#
6073# History
6074#   2002/04/11 : pda/jean : design
6075#   2010/11/29 : pda      : i18n
6076#
6077
6078proc read-domain {dbfd domain} {
6079    set domain [::pgsql::quote $domain]
6080    set iddom -1
6081    pg_select $dbfd "SELECT iddom FROM dns.domain WHERE name = '$domain'" tab {
6082	set iddom $tab(iddom)
6083    }
6084    return $iddom
6085}
6086
6087#
6088# Checks if the domain is authorized for this user
6089#
6090# Input:
6091#   - parameters:
6092#       - dbfd : database handle
6093#	- idcor : user id
6094#	- _iddom : domain id or -1 to read from domain
6095#	- _domain : domain, or "" to read from iddom
6096#	- roles : roles to test (column names in p_dom)
6097# Output:
6098#   - return value: empty string or error message
6099#   - parameters _iddom and _domain : fetched values
6100#
6101# History
6102#   2002/04/11 : pda/jean : design
6103#   2002/05/06 : pda/jean : use groups
6104#   2004/02/06 : pda/jean : add roles
6105#   2010/11/29 : pda      : i18n
6106#
6107
6108proc check-domain {dbfd idcor _iddom _domain roles} {
6109    upvar $_iddom iddom
6110    upvar $_domain domain
6111
6112    set msg ""
6113
6114    #
6115    # Read domain if needed
6116    #
6117    if {$iddom == -1} then {
6118	set iddom [read-domain $dbfd $domain]
6119	if {$iddom == -1} then {
6120	    set msg [mc "Domain '%s' not found" $domain]
6121	}
6122    } elseif {$domain eq ""} then {
6123	set sql "SELECT name FROM dns.domain WHERE iddom = $iddom"
6124	pg_select $dbfd $sql tab {
6125	    set domain $tab(name)
6126	}
6127	if {$domain eq ""} then {
6128	    set msg [mc "Domain-id '%s' not found" $iddom]
6129	}
6130    }
6131
6132    #
6133    # Check if we have rights on this domain
6134    #
6135    if {$msg eq ""} then {
6136	set where ""
6137	foreach r $roles {
6138	    append where "AND p_dom.$r > 0 "
6139	}
6140
6141	set found 0
6142	set sql "SELECT p_dom.iddom FROM dns.p_dom, global.nmuser
6143			    WHERE nmuser.idcor = $idcor
6144				    AND nmuser.idgrp = p_dom.idgrp
6145				    AND p_dom.iddom = $iddom
6146				    $where
6147				    "
6148	pg_select $dbfd $sql tab {
6149	    set found 1
6150	}
6151	if {! $found} then {
6152	    set msg [mc "You don't have rights on domain '%s'" $domain]
6153	}
6154    }
6155
6156    return $msg
6157}
6158
6159#
6160# Check if the IP address is authorized for this user
6161#
6162# Input:
6163#   - parameters:
6164#       - dbfd : database handle
6165#	- idcor : user id
6166#	- addr : IP address to test
6167# Output:
6168#   - return value: 1 if ok, 0 if error
6169#
6170# History
6171#   2002/04/11 : pda/jean : design
6172#   2002/05/06 : pda/jean : use groups
6173#   2004/01/14 : pda/jean : add IPv6
6174#   2010/11/29 : pda      : i18n
6175#
6176
6177proc check-authorized-ip {dbfd idcor addr} {
6178    set r 0
6179    set sql "SELECT dns.check_ip_cor ('$addr', $idcor) AS ok"
6180    pg_select $dbfd $sql tab {
6181	set r [string equal $tab(ok) "t"]
6182    }
6183    return $r
6184}
6185
6186#
6187# Check if the user has adequate rights to a machine, by checking
6188# that he own all IP addresses
6189#
6190# Input:
6191#   - parameters:
6192#       - dbfd : database handle
6193#	- idcor : user id
6194#	- idrr : RR id to search for, or -1 if _trr is already initialized
6195#	- _trr : see read-rr-by-name
6196# Output:
6197#   - return value: 1 if ok, 0 if error
6198#
6199# History
6200#   2002/04/19 : pda/jean : design
6201#   2010/11/29 : pda      : i18n
6202#   2012/10/30 : pda/jean : add views
6203#
6204
6205proc check-name-by-addresses {dbfd idcor idrr _trr} {
6206    upvar $_trr trr
6207
6208    set ok 1
6209
6210    #
6211    # Read RR if needed
6212    #
6213
6214    if {$idrr != -1 && [read-rr-by-id $dbfd $idrr trr] == -1} then {
6215	set trr(ip) {}
6216	set ok 1
6217    }
6218
6219    #
6220    # Check all addresses and views
6221    #
6222
6223    if {[info exists trr(ip)]} then {
6224	foreach viewip $trr(ip) {
6225	    lassign $viewip idview ip
6226	    if {! [u isallowedview $idview]} then {
6227		set ok 0
6228		break
6229	    }
6230	    if {! [check-authorized-ip $dbfd $idcor $ip]} then {
6231		set ok 0
6232		break
6233	    }
6234	}
6235    }
6236
6237    return $ok
6238}
6239
6240#
6241# Check if the user as the right to add/modify/delete a given name
6242# according to a given context.
6243#
6244# Input:
6245#   - parameters:
6246#       - dbfd : database handle
6247#	- idcor : user id
6248#	- name : name to test (first component of FQDN)
6249#	- domain : domain to test (the n-1 last components of FQDN)
6250#	- idview : view id in which this FQDN must be tested
6251#	- trr : in return, information on the host (see read-rr-by-id)
6252#	- context : the context to check
6253# Output:
6254#   - return value: empty string or error message
6255#   - parameter trr : contains informations on the RR found, or if the RR
6256#	doesn't exist, trr(idrr) = "" and trr(iddom) = domain id
6257#
6258# Detail of tests:
6259#    According to context:
6260#	"host"
6261#	    check-domain (domain, idcor, "") and views
6262#	    if name.domain is ALIAS then error
6263#	    if name.domain is MX then error
6264#	    if name.domain is ADDRMAIL
6265#		then check-all-IP-addresses (mail host, idcor)
6266#		      check-domain (domain, idcor, "")
6267#	    if name.domain has IP addresses
6268#		then check-all-IP-addresses (machine, idcor)
6269#	    if no test is false, then OK
6270#	"existing-host"
6271#	    identical to "host", but the name must have at least one IP address
6272#	"del-name"
6273#	    check-domain (domain, idcor, "") and views
6274#	    if name.domain is ALIAS
6275#		then check-all-IP-addresses (pointed host, idcor)
6276#	    if name.domain is MX then error
6277#	    if name.domain has IP addresses
6278#		then check-all-IP-addresses (machine, idcor)
6279#	    if name.domain is ADDRMAIL
6280#		then check-all-IP-addresses (mail host, idcor)
6281#		      check-domain (domain, idcor, "")
6282#	    if no test is false, then OK
6283#	"alias"
6284#	    check-domain (domain, idcor, "") and views
6285#	    if name.domain is ALIAS then error
6286#	    if name.domain is MX then error
6287#	    if name.domain is target of a MX then error
6288#	    if name.domain is ADDRMAIL then error
6289#	    if name.domain has IP addresses then error
6290#	    if no test is false, then OK
6291#	"mx"
6292#	    check-domain (domain, idcor, "") and views
6293#	    if name.domain is ALIAS then error
6294#	    if name.domain is MX
6295#		then check-all-IP-addresses (mail exchangers, idcor)
6296#	    if name.domain is ADDRMAIL then error
6297#	    if no test is false, then OK
6298#	"add-mailaddr"
6299#	    check-domain (domain, idcor, "mailrole") and views
6300#	    if name.domain is ALIAS then error
6301#	    if name.domain is MX then error
6302#	    if name.domain is ADDRMAIL then error
6303#	    if name.domain is MAILHOST then error
6304#	    if name.domain has IP addresses
6305#		check-all-IP-addresses (name.domain, idcor)
6306#	    if no test is false, then OK
6307#	"del-mailaddr"
6308#	    check-domain (domain, idcor, "mailrole") and views
6309#	    if name.domain is ALIAS then error
6310#	    if name.domain is MX then error
6311#	    if name.domain is target of a MX then error
6312#	    if name.domain is NOT ADDRMAIL then error
6313#	    if name.domain is ADDRMAIL
6314#		check-all-IP-addresses (mail host, idcor)
6315#		check-domain (domain, idcor, "")
6316#	    if name.domain has IP addresses
6317#		check-all-IP-addresses (name.domain, idcor)
6318#	    if no test is false, then OK
6319#
6320#    check-IP-addresses (host, idcor)
6321#	if there is no address
6322#	    then error
6323#	    else check that all IP addresses are mine (with an AND)
6324#	end if
6325#
6326# Bug: this procedure is never called with the "mx" parameter
6327#
6328# History
6329#   2004/02/27 : pda/jean : specification
6330#   2004/02/27 : pda/jean : coding
6331#   2004/03/01 : pda/jean : use trr(iddom) instead of iddom
6332#   2010/11/29 : pda      : i18n
6333#   2012/10/30 : pda/jean : add views
6334#   2013/04/10 : pda/jean : accept only one view
6335#
6336
6337proc check-authorized-host {dbfd idcor name domain idview _trr context} {
6338    upvar $_trr trr
6339
6340    array set testrights {
6341	host	{
6342		    {domain	{}}
6343		    {alias	REJECT}
6344		    {mx		REJECT}
6345		    {ip		CHECK}
6346		    {mailaddr	CHECK}
6347		}
6348	existing-host	{
6349		    {domain	{}}
6350		    {alias	REJECT}
6351		    {mx		REJECT}
6352		    {ip		CHECK}
6353		    {ip		EXISTS}
6354		    {mailaddr	CHECK}
6355		}
6356	alias	{
6357		    {domain	{}}
6358		    {alias	REJECT}
6359		    {mx		REJECT}
6360		    {ip		REJECT}
6361		    {mailaddr	REJECT}
6362		}
6363	del-name	{
6364		    {domain	{}}
6365		    {alias	CHECK}
6366		    {mx		REJECT}
6367		    {ip		CHECK}
6368		    {mailaddr	CHECK}
6369		}
6370	mx	{
6371		    {domain	{}}
6372		    {alias	REJECT}
6373		    {mx		CHECK}
6374		    {ip		CHECK}
6375		    {mailaddr	REJECT}
6376		}
6377	add-mailaddr	{
6378		    {domain	mailrole}
6379		    {alias	REJECT}
6380		    {mx		REJECT}
6381		    {mailaddr	REJECT}
6382		    {mailhost	REJECT}
6383		    {ip		CHECK}
6384		}
6385	del-mailaddr	{
6386		    {domain	mailrole}
6387		    {alias	REJECT}
6388		    {mx		REJECT}
6389		    {mailaddr	CHECK}
6390		    {mailaddr	EXISTS}
6391		    {ip		CHECK}
6392		}
6393    }
6394
6395
6396    #
6397    # Get the list of actions associated with the context
6398    #
6399
6400    if {! [info exists testrights($context)]} then {
6401	return [mc "Internal error: invalid context '%s'" $context]
6402    }
6403
6404    #
6405    # For each view, process tests in the given order, and break as
6406    # soon as a test fails
6407    #
6408
6409    set fqdn "$name.$domain"
6410
6411    foreach a $testrights($context) {
6412	set parm [lindex $a 1]
6413	switch [lindex $a 0] {
6414	    domain {
6415		set msg [check-views [list $idview]]
6416		if {$msg ne ""} then {
6417		    return $msg
6418		}
6419		set viewname [u viewname $idview]
6420
6421		set iddom -1
6422		set msg [check-domain $dbfd $idcor iddom domain $parm]
6423		if {$msg ne ""} then {
6424		    return $msg
6425		}
6426
6427		if {! [read-rr-by-name $dbfd $name $iddom $idview trr]} then {
6428		    set trr(idrr) ""
6429		    set trr(iddom) $iddom
6430		}
6431	    }
6432	    alias {
6433		set idcname [rr-cname-by-view trr $idview]
6434		if {$idcname ne ""} then {
6435		    read-rr-by-id $dbfd $idcname t
6436		    set fqdnref "$t(name).$t(domain)"
6437		    switch $parm {
6438			REJECT {
6439			    return [mc {%1$s is an alias of host %2$s in view %3$s} $fqdn $fqdnref $viewname]
6440			}
6441			CHECK {
6442			    set ok [check-name-by-addresses $dbfd $idcor -1 t]
6443			    if {! $ok} then {
6444				return [mc {You don't have rights on some IP addresses of '%1$s' referenced by alias '%2$s'} $fqdnref $fqdn]
6445			    }
6446			}
6447			default {
6448			    return [mc {Internal error: invalid parameter '%1$s' for '%2$s'} $parm "$context/$a"]
6449			}
6450		    }
6451		}
6452	    }
6453	    mx {
6454		set lmx [rr-mx-by-view trr $idview]
6455		foreach mx $lmx {
6456		    switch $parm {
6457			REJECT {
6458			    return [mc "'%s' is a MX" $fqdn]
6459			}
6460			CHECK {
6461			    set idrr [lindex $mx 1]
6462			    set ok [check-name-by-addresses $dbfd $idcor $idrr t]
6463			    if {! $ok} then {
6464				set fqdnmx "$t(name).$t(domain)"
6465				return [mc {You don't have rights on some IP addresses of '%1$s' referenced by MX '%2$s'} $fqdnmx $fqdn]
6466			    }
6467			}
6468			default {
6469			    return [mc {Internal error: invalid parameter '%1$s' for '%2$s'} $parm "$context/$a"]
6470			}
6471		    }
6472		}
6473	    }
6474	    mailaddr {
6475		# get mailbox host for this address
6476		set rm [rr-mailrole-by-view trr $idview]
6477		if {$rm ne ""} then {
6478		    lassign $rm idrr idviewmbx
6479		    # get mbox host
6480		    if {! [read-rr-by-id $dbfd $idrr trrh]} then {
6481			return [mc "Internal error: id '%s' doesn't exists for a mail host" $idrr]
6482		    }
6483		    switch $parm {
6484			REJECT {
6485			    # This name is already a mail address
6486			    # (it already has a mailbox host)
6487			    set fqdnm "$trrh(name).$trrh(domain)"
6488			    return [mc {%1$s in view %2$s is a mail address hosted by %3$s in view %4$s} $fqdn $viewname $fqdnm [u viewname $idviewmbx]]
6489			}
6490			CHECK {
6491
6492			    # IP address check
6493			    set ok [check-name-by-addresses $dbfd $idcor -1 trrh]
6494			    if {! $ok} then {
6495				return [mc "You don't have rights on host holding mail for '%s'" $fqdn]
6496			    }
6497
6498			    # Mail host checking
6499			    set bidon -1
6500			    set msg [check-domain $dbfd $idcor bidon trrh(domain) ""]
6501			    if {$msg ne ""} then {
6502				set r [mc "You don't have rights on host holding mail for '%s'" $fqdn]
6503				append r "\n$msg"
6504				return $r
6505			    }
6506			}
6507			EXISTS {
6508			    # nothing
6509			}
6510			default {
6511			    return [mc {Internal error: invalid parameter '%1$s' for '%2$s'} $parm "$context/$a"]
6512			}
6513		    }
6514		} else {
6515		    # this address has no mailbox host, so it is
6516		    # not a mail role
6517		    switch $parm {
6518			REJECT -
6519			CHECK {
6520			    # nothing
6521			}
6522			EXISTS {
6523			    return [mc {'%1$s' is not a mail role in view '%2$s'} $fqdn $viewname]
6524			}
6525			default {
6526			    return [mc {Internal error: invalid parameter '%1$s' for '%2$s'} $parm "$context/$a"]
6527			}
6528		    }
6529		}
6530	    }
6531	    mailhost {
6532		set laddr [rr-mailaddr-by-view trr $idview]
6533		switch $parm {
6534		    REJECT {
6535			# remove the name (in all views) from the list
6536			# of mail domains hosted on this host
6537			while {[set pos [lsearch -exact -index 0 \
6538					    $laddr $trr(idrr)]] != -1} {
6539			    set laddr [lreplace $laddr $pos $pos]
6540			}
6541			if {[llength $laddr] > 0} then {
6542			    return [mc "'%s' is a mail host for mail domains" $fqdn]
6543			}
6544		    }
6545		    default {
6546			return [mc {Internal error: invalid parameter '%1$s' for '%2$s'} $parm "$context/$a"]
6547		    }
6548		}
6549	    }
6550	    ip {
6551		set lip [rr-ip-by-view trr $idview]
6552		switch $parm {
6553		    REJECT {
6554			if {[llength $lip] > 0} then {
6555			    return [mc {'%1$s' has IP addresses in view '%2$s'} $fqdn $viewname]
6556			}
6557		    }
6558		    EXISTS {
6559			if {[llength $lip] == 0} then {
6560			    return [mc {Name '%1$s' is not a host in view '%2$s'} $fqdn $viewname]
6561			}
6562		    }
6563		    CHECK {
6564			set ok [check-name-by-addresses $dbfd $idcor -1 trr]
6565			if {! $ok} then {
6566			    return [mc "You don't have rights on some IP addresses of '%s'" $fqdn]
6567			}
6568		    }
6569		    default {
6570			return [mc {Internal error: invalid parameter '%1$s' for '%2$s'} $parm "$context/$a"]
6571		    }
6572		}
6573	    }
6574	}
6575    }
6576
6577    return ""
6578}
6579
6580#
6581# Check MX informations (given form field values)
6582#
6583# Input:
6584#   - parameters:
6585#	- dbfd : database handle
6586#	- prio : priority read from the form
6587#	- name : MX name, read from the form
6588#	- domain : MX domain name, read from the form
6589#	- idview : view id
6590#	- idcor : user id
6591#	- _msg : error message
6592# Output:
6593#   - return value: list {prio idmx} where
6594#	- prio = numeric priority (int syntax ok)
6595#	- idmx = existing RR id
6596#   - parameters:
6597#	- _msg : empty string or error message
6598#
6599# History
6600#   2003/04/25 : pda/jean : design
6601#   2004/03/04 : pda/jean : common procedure
6602#   2010/11/29 : pda      : i18n
6603#   2013/03/20 : pda      : add views
6604#
6605
6606proc check-mx-target {dbfd prio name domain idview idcor _msg} {
6607    upvar $_msg msg
6608
6609    #
6610    # Syntaxic checking of priority
6611    #
6612
6613    if {! [regexp {^[0-9]+$} $prio]} then {
6614	set msg [mc {Invalid MX priority '%1$s' for '%2$s'} $prio "$name.$domain"]
6615	return {}
6616    }
6617
6618    #
6619    # Check relay, domain, etc.
6620    #
6621
6622    set msg [check-authorized-host $dbfd $idcor $name $domain $idview trr "existing-host"]
6623    if {$msg ne ""} then {
6624	return $msg
6625    }
6626
6627    #
6628    # Build up the result
6629    #
6630
6631    return [list $prio $trr(idrr)]
6632}
6633
6634#
6635# Check MX
6636#
6637# Input:
6638#   - parameters:
6639#	- dbfd : database handle
6640#	- name : MX name
6641#	- _iddom : in return, domain id
6642#	- domain : MX domain name
6643#	- idview: view id
6644#	- idcor : user id
6645#	- _exists : 1 if RR exists, 0 if not
6646#	- _trr : RR information read from database
6647# Output:
6648#   - return value: empty string or error message
6649#   - parameter _trr : RR information on return
6650#
6651# History
6652#   2010/12/09 : pda      : isolate common code
6653#   2013/03/21 : pda      : add views
6654#
6655
6656proc check-authorized-mx {dbfd idcor name _iddom domain idview _exists _trr} {
6657    upvar $_exists exists
6658    upvar $_iddom iddom
6659    upvar $_trr trr
6660
6661    #
6662    # Validate MX name and domain
6663    #
6664
6665    set msg [check-name-syntax $name]
6666    if {$msg ne ""} then {
6667	d error $msg
6668    }
6669
6670    set iddom -1
6671    set msg [check-domain $dbfd $idcor iddom domain ""]
6672    if {$msg ne ""} then {
6673	d error $msg
6674    }
6675
6676    #
6677    # Get information about this name if it already exists
6678    #
6679
6680    set exists [read-rr-by-name $dbfd $name $iddom $idview trr]
6681    if {$exists} then {
6682	#
6683	# If it already exists, check that it is not a CNAME
6684	#
6685
6686	set cname [rr-cname-by-view trr $idview]
6687	if {$cname ne ""} then {
6688	    return [mc "'%s' is an alias" $name]
6689	}
6690
6691	#
6692	# MX exists, we must check that the user has permissions
6693	# to access all referenced domains.
6694	#
6695
6696	foreach lmx [rr-mx-by-view trr $idview] {
6697	    lassign $lmx prio idmx
6698	    if {! [read-rr-by-id $dbfd $idmx tabmx]} then {
6699		return [mc "Internal error: rr_mx table references RR '%s', not found in the rr table" $idmx]
6700	    }
6701	    set iddom $tabmx(iddom)
6702	    set msg [check-domain $dbfd $idcor iddom tabmx(domain) ""]
6703	    if {$msg ne ""} then {
6704		return [mc {MX '%1$s' points to a domain on which you don't have rights\n%2$s} "$tabmx(name).$tabmx(domain)" $msg]
6705	    }
6706	}
6707    }
6708
6709    return ""
6710}
6711
6712#
6713# Check domains and mail relays
6714#
6715# Input:
6716#   - parameters:
6717#       - dbfd : database handle
6718#	- idcor : user id
6719#	- _iddom : in return, id of found domain
6720#	- domain : the domain to search
6721#	- idview : view id
6722# Output:
6723#   - return value: empty string or error message
6724#   - parameter iddom : id of found domain, or -1 if error
6725#
6726# History
6727#   2004/03/04 : pda/jean : design
6728#   2010/11/29 : pda      : i18n
6729#   2013/03/20 : pda      : add views
6730#
6731
6732proc check-domain-relay {dbfd idcor _iddom domain idview} {
6733    upvar $_iddom iddom
6734
6735    #
6736    # Check the domain
6737    #
6738
6739    set msg [check-domain $dbfd $idcor iddom domain "mailrole"]
6740    if {$msg ne ""} then {
6741	return $msg
6742    }
6743
6744    #
6745    # Check that we own all specified relays
6746    #
6747
6748    set sql "SELECT r.name AS name, d.name AS domain
6749		FROM dns.relay_dom rd, dns.rr r, dns.domain d
6750		WHERE rd.iddom = $iddom
6751			AND rd.mx = r.idrr
6752			AND r.iddom = d.iddom
6753			AND r.idview = $idview
6754		"
6755    pg_select $dbfd $sql tab {
6756	set msg [check-authorized-host $dbfd $idcor $tab(name) $tab(domain) $idview trr "existing-host"]
6757	if {$msg ne ""} then {
6758	    return [mc {You don't have rights to some relays of domain '%1$s': %2$s} $domain $msg]
6759	}
6760    }
6761
6762    return ""
6763}
6764
6765#
6766# Check MAC against syntax errors and DHCP ranges
6767#
6768# Input:
6769#   - parameters:
6770#       - dbfd: database handle
6771#	- mac: MAC address (empty or not empty)
6772#	- trr: trr of host for which this MAC address is
6773#	- idview: view id
6774# Output:
6775#   - return value: empty string or error message
6776#
6777# History
6778#   2013/04/05 : pda/jean : design
6779#
6780
6781proc check-mac {dbfd mac _trr idview} {
6782    upvar $_trr trr
6783
6784    set msg ""
6785    if {$mac ne ""} then {
6786	set msg [check-mac-syntax $dbfd $mac]
6787	if {$msg eq ""} then {
6788	    set msg [check-static-dhcp $dbfd $mac [rr-ip-by-view trr $idview]]
6789	}
6790    }
6791    return $msg
6792}
6793
6794#
6795# Check that no static DHCP association (IP address with an associate
6796# non null MAC address) is within a DHCP range
6797#
6798# Input:
6799#   - parameters:
6800#       - dbfd : database handle
6801#	- mac : MAC address (empty or not empty)
6802#	- lip : IP (IPv4 and IPv6) address list
6803# Output:
6804#   - return value: empty string or error message
6805#
6806# History
6807#   2004/08/04 : pda/jean : design
6808#   2010/11/29 : pda      : i18n
6809#
6810
6811proc check-static-dhcp {dbfd mac lip} {
6812    set r ""
6813    if {$mac ne ""} then {
6814	foreach ip $lip {
6815	    set sql "SELECT min, max
6816			    FROM dns.dhcprange
6817			    WHERE '$ip' >= min AND '$ip' <= max"
6818	    pg_select $dbfd $sql tab {
6819		set r [mc {Impossible to use MAC address '%1$s' because IP address '%2$s' is in DHCP dynamic range [%3$s..%4$s]} $mac $ip $tab(min) $tab(max)]
6820	    }
6821	    if {$r ne ""} then {
6822		break
6823	    }
6824	}
6825    }
6826    return $r
6827}
6828
6829#
6830# Check possible values for a TTL (see RFC 2181)
6831#
6832# Input:
6833#   - parameters:
6834#	- ttl : value to check
6835# Output:
6836#   - return value: empty string or error message
6837#
6838# History
6839#   2010/11/02 : pda/jean : design, from jean's code
6840#   2010/11/29 : pda      : i18n
6841#
6842
6843proc check-ttl {ttl} {
6844    set r ""
6845    # 2^31-1
6846    set maxttl [expr 0x7fffffff]
6847    if {! [regexp {^\d+$} $ttl]} then {
6848	set r [mc "Invalid TTL: must be a positive integer"]
6849    } else {
6850	if {$ttl > $maxttl} then {
6851	    set r [mc "Invalid TTL: must be less than %s" $maxttl]
6852	}
6853    }
6854    return $r
6855}
6856
6857##############################################################################
6858# User checking
6859##############################################################################
6860
6861#
6862# Check syntax of a group name
6863#
6864# Input:
6865#   - parameters:
6866#       - group : name of group
6867# Output:
6868#   - return value: empty string or error message
6869#
6870# History
6871#   2008/02/13 : pda/jean : design
6872#   2010/11/29 : pda      : i18n
6873#
6874
6875proc check-group-syntax {group} {
6876    if {[regexp {^[-A-Za-z0-9]*$} $group]} then {
6877	set r ""
6878    } else {
6879	set r [mc "Invalid group name '%s' (allowed chars: letters, digits and minus symbol)" $group]
6880    }
6881    return $r
6882}
6883
6884
6885##############################################################################
6886# Hinfo checking
6887##############################################################################
6888
6889#
6890# Returns HINFO index in the database
6891#
6892# Input:
6893#   - dbfd : database handle
6894#   - text : hinfo to search
6895# Output:
6896#   - return value: index, or -1 if not found
6897#
6898# History
6899#   2002/05/03 : pda/jean : design
6900#   2010/11/29 : pda      : i18n
6901#
6902
6903proc read-hinfo {dbfd text} {
6904    set qtext [::pgsql::quote $text]
6905    set idhinfo -1
6906    pg_select $dbfd "SELECT idhinfo FROM dns.hinfo WHERE name = '$qtext'" tab {
6907	set idhinfo $tab(idhinfo)
6908    }
6909    return $idhinfo
6910}
6911
6912##############################################################################
6913# DHCP profile checking
6914##############################################################################
6915
6916#
6917# Returns DHCP profile index in the database
6918#
6919# Input:
6920#   - dbfd : database handle
6921#   - text : profile name to search, or ""
6922# Output:
6923#   - return value: index, or -1 if not found
6924#
6925# History
6926#   2005/04/11 : pda/jean : design
6927#   2010/11/29 : pda      : i18n
6928#
6929
6930proc read-dhcp-profile {dbfd text} {
6931    if {$text eq ""} then {
6932	set iddhcpprof 0
6933    } else {
6934	set qtext [::pgsql::quote $text]
6935	set sql "SELECT iddhcpprof FROM dns.dhcpprofile WHERE name = '$qtext'"
6936	set iddhcpprof -1
6937	pg_select $dbfd $sql tab {
6938	    set iddhcpprof $tab(iddhcpprof)
6939	}
6940    }
6941    return $iddhcpprof
6942}
6943
6944##############################################################################
6945# Netmagis standard HTML menus
6946##############################################################################
6947
6948#
6949# Get a ready to use HTML menu to set HINFO values.
6950#
6951# Input:
6952#   - dbfd : database handle
6953#   - field : field name
6954#   - defval : default hinfo (textual value)
6955# Output:
6956#   - return value: ready to use HTML string
6957#
6958# History
6959#   2002/05/03 : pda/jean : design
6960#   2010/12/01 : pda      : i18n
6961#
6962
6963proc menu-hinfo {dbfd field defval} {
6964    set lhinfo {}
6965    set sql "SELECT name FROM dns.hinfo
6966				WHERE present = 1
6967				ORDER BY sort ASC, name ASC"
6968    set i 0
6969    set defindex 0
6970    pg_select $dbfd $sql tab {
6971	lappend lhinfo [list $tab(name) $tab(name)]
6972	if {$tab(name) eq $defval} then {
6973	    set defindex $i
6974	}
6975	incr i
6976    }
6977    return [::webapp::form-menu $field 1 0 $lhinfo [list $defindex]]
6978}
6979
6980#
6981# Get a ready to use HTML menu to set DHCP profile value, or a hidden
6982# field if the group do not have access to any DHCP Profile.
6983#
6984# Input:
6985#   - dbfd : database handle
6986#   - field : field name
6987#   - idcor : user id
6988#   - iddhcpprof : default selected profile, or 0
6989# Output:
6990#   - return value: list with 2 HTML strings {title menu}
6991#
6992# History
6993#   2005/04/08 : pda/jean : design
6994#   2008/07/23 : pda/jean : change output format
6995#   2010/11/29 : pda      : i18n
6996#
6997
6998proc menu-dhcp-profile {dbfd field idcor iddhcpprof} {
6999    #
7000    # Get all DHCP profiles for this group
7001    #
7002
7003    set sql "SELECT d.iddhcpprof, d.name
7004		FROM dns.p_dhcpprofile p, dns.dhcpprofile d, global.nmuser u
7005		WHERE u.idcor = $idcor
7006		    AND p.idgrp = u.idgrp
7007		    AND p.iddhcpprof = d.iddhcpprof
7008		ORDER BY p.sort ASC, d.name"
7009    set lprof {}
7010    set lsel {}
7011    set idx 1
7012    pg_select $dbfd $sql tab {
7013	lappend lprof [list $tab(iddhcpprof) $tab(name)]
7014	if {$tab(iddhcpprof) == $iddhcpprof} then {
7015	    lappend lsel $idx
7016	}
7017	incr idx
7018    }
7019
7020    #
7021    # Is there at least one profile?
7022    #
7023
7024    if {[llength $lprof] > 0} then {
7025	#
7026	# Is the default selected profile in our list?
7027	#
7028
7029	if {$iddhcpprof != 0 && [llength $lsel] == 0} then {
7030	    #
7031	    # We must add it at the end of the list.
7032	    #
7033
7034	    set sql "SELECT iddhcpprof, name
7035			    FROM dns.dhcpprofile
7036			    WHERE iddhcpprof = $iddhcpprof"
7037	    pg_select $dbfd $sql tab {
7038		lappend lprof [list $tab(iddhcpprof) $tab(name)]
7039		lappend lsel $idx
7040	    }
7041	}
7042
7043	#
7044	# Special case at the beginning of the list
7045	#
7046
7047	set lprof [linsert $lprof 0 [list 0 [mc "No profile"]]]
7048
7049	set title [mc "DHCP profile"]
7050	set html [::webapp::form-menu $field 1 0 $lprof $lsel]
7051
7052    } else {
7053	#
7054	# No profile found. We hide the field.
7055	#
7056
7057	set title ""
7058	set html [::webapp::form-hidden $field $iddhcpprof]
7059    }
7060
7061    return [list $title $html]
7062}
7063
7064#
7065# Get an HTML button "SMTP emit right" for a host, or a hidden field
7066# if the group do not have the according right.
7067#
7068# Input:
7069#   - dbfd : database handle
7070#   - field : field name
7071#   - _tabuid : user characteristics
7072#   - sendsmtp : default selected value
7073# Output:
7074#   - return value: list with 2 HTML strings {title menu}
7075#
7076# History
7077#   2008/07/23 : pda/jean : design
7078#   2008/07/24 : pda/jean : use idcor instead of idgrp
7079#   2010/12/01 : pda      : i18n
7080#   2010/12/05 : pda      : use tabuid instead of idcor
7081#
7082
7083proc menu-sendsmtp {dbfd field _tabuid sendsmtp} {
7084    upvar $_tabuid tabuid
7085
7086    #
7087    # Get group access right, in order to display or hide the button
7088    #
7089
7090
7091    if {$tabuid(p_smtp)} then {
7092	set title [mc "Use SMTP"]
7093	set html [::webapp::form-bool $field $sendsmtp]
7094    } else {
7095	set title ""
7096	set html [::webapp::form-hidden $field $sendsmtp]
7097    }
7098
7099    return [list $title $html]
7100}
7101
7102#
7103# Get an HTML input form for a host TTL value, or a hidden field
7104# if the group do not have the according right.
7105#
7106# Input:
7107#   - dbfd : database handle
7108#   - field : field name
7109#   - _tabuid : user characteristics
7110#   - ttl : default value
7111# Output:
7112#   - return value: ready to use HTML string
7113#
7114# History
7115#   2010/10/31 : pda      : design
7116#   2010/12/01 : pda      : i18n
7117#   2010/12/05 : pda      : use tabuid instead of idcor
7118#
7119
7120proc menu-ttl {dbfd field _tabuid ttl} {
7121    upvar $_tabuid tabuid
7122
7123    #
7124    # Convert the TTL value from the database in something which can be
7125    # displayed: the value "-1" means "no TTL set for this host", which
7126    # should be displayed as an empty string.
7127    #
7128
7129    if {$ttl == -1} then {
7130	set ttl ""
7131    }
7132
7133    #
7134    # Get the group permission.
7135    #
7136
7137    if {$tabuid(p_ttl)} then {
7138	set title [mc "TTL"]
7139	set html [::webapp::form-text $field 1 6 10 $ttl]
7140	append html " "
7141	append html [mc "(in seconds)"]
7142    } else {
7143	set title ""
7144	set html [::webapp::form-hidden $field $ttl]
7145    }
7146
7147    return [list $title $html]
7148}
7149
7150
7151#
7152# Get an HTML menu to select a domain. This may be either a simple
7153# text with a hidden field if the group has access to only one domain,
7154# or a dropdown menu.
7155#
7156# Input:
7157#   - dbfd : database handle
7158#   - idcor : user id
7159#   - field : field name
7160#   - where : SQL where clause (without SQL keyword "where") or empty string
7161#   - sel : name of domain to pre-select, or empty string
7162# Output:
7163#   - return value: HTML string
7164#
7165# History :
7166#   2002/04/11 : pda/jean : coding
7167#   2002/04/23 : pda      : add display priority
7168#   2002/05/03 : pda/jean : migrated in libdns
7169#   2002/05/06 : pda/jean : use groups
7170#   2003/04/24 : pda/jean : decompose in two procedures
7171#   2004/02/06 : pda/jean : add where clause
7172#   2004/02/12 : pda/jean : add sel parameter
7173#   2010/11/15 : pda      : delete err parameter
7174#
7175
7176proc menu-domain {dbfd idcor field where sel} {
7177    set lcouples [couple-domains $dbfd $idcor $where]
7178
7179    set lsel [lsearch -exact $lcouples [list $sel $sel]]
7180    if {$lsel == -1} then {
7181	set lsel {}
7182    }
7183
7184    #
7185    # If there is only one domain, present it as a text. If more
7186    # than one domain, use a dropdown menu.
7187    #
7188
7189    set ndom [llength $lcouples]
7190    switch -- $ndom {
7191	0	{
7192	    d error [mc "Sorry, but you do not have any active domain"]
7193	}
7194	1	{
7195	    set v [lindex [lindex $lcouples 0] 0]
7196	    set h [::webapp::form-hidden $field $v]
7197	    set html "$v $h"
7198	}
7199	default	{
7200	    set html [::webapp::form-menu $field 1 0 $lcouples $lsel]
7201	}
7202    }
7203
7204    return $html
7205}
7206
7207#
7208# Returns a list of couples {name name} for each authorized domain
7209#
7210# Input:
7211#   - dbfd : database handle
7212#   - idcor : user id
7213#   - where : SQL where clause (without SQL keyword "where") or empty string
7214# Output:
7215#   - return value: liste of couples
7216#
7217# History :
7218#   2003/04/24 : pda/jean : coding
7219#   2004/02/06 : pda/jean : add where clause
7220#   2010/12/01 : pda      : i18n
7221#
7222
7223proc couple-domains {dbfd idcor where} {
7224    if {$where ne ""} then {
7225	set where " AND $where"
7226    }
7227
7228    set lcouples {}
7229    set sql "SELECT domain.name
7230		FROM dns.domain, dns.p_dom, global.nmuser
7231		WHERE domain.iddom = p_dom.iddom
7232		    AND p_dom.idgrp = nmuser.idgrp
7233		    AND nmuser.idcor = $idcor
7234		    $where
7235		ORDER BY p_dom.sort ASC, domain.name ASC"
7236    pg_select $dbfd $sql tab {
7237	lappend lcouples [list $tab(name) $tab(name)]
7238    }
7239
7240    return $lcouples
7241}
7242
7243#
7244# Get an HTML menu to select one view. This may be either a simple
7245# text with a hidden field if the group has access to only one view,
7246# or a menu.
7247#
7248# Input:
7249#   - dbfd : database handle
7250#   - idcor : user id
7251#   - field : field name
7252#   - sel : list of view id to pre-select, or empty list to pre-select
7253#	default views (those cited in the p_view.selected column)
7254# Output:
7255#   - return value: list {disp html} where disp=true if view menu
7256#	must be displayed, and html is html code (may be of "hidden"
7257#	input type) to be inserted.
7258#
7259# History :
7260#   2012/10/30 : pda/jean : design
7261#   2012/11/07 : pda/jean : add mult parameter and change return value
7262#   2013/04/10 : pda/jean : remove mult parameter
7263#
7264
7265proc menu-view {dbfd idcor field sel} {
7266    set nsel [llength $sel]
7267    set lsel {}
7268    set lcouples {}
7269    set sql "SELECT v.idview, v.name, p.selected
7270		FROM dns.view v, dns.p_view p, global.nmuser
7271		WHERE nmuser.idcor = $idcor
7272		    AND p.idgrp = nmuser.idgrp
7273		    AND v.idview = p.idview
7274		ORDER BY p.sort ASC, v.name ASC"
7275    set i 0
7276    pg_select $dbfd $sql tab {
7277	lappend lcouples [list $tab(idview) $tab(name)]
7278	if {$nsel == 0} then {
7279	    # no sel parameter given: use selected views for this group
7280	    if {$tab(selected)} then {
7281		lappend lsel $i
7282	    }
7283	} else {
7284	    # sel is a list of idviews
7285	    # search our idview in sel
7286	    if {[lsearch -exact $sel $tab(idview)] != -1} then {
7287		lappend lsel $i
7288	    }
7289	}
7290	incr i
7291    }
7292
7293    set nviews [llength $lcouples]
7294    switch $nviews {
7295	0 {
7296	    d error [mc "Sorry, but you do not have access to any view"]
7297	}
7298	1 {
7299	    set idview [lindex [lindex $lcouples 0] 0]
7300	    set disp 0
7301	    set html [::webapp::form-hidden $field $idview]
7302	}
7303	default {
7304	    set disp 1
7305	    set html [::webapp::form-menu $field 1 0 $lcouples $lsel]
7306	}
7307    }
7308
7309    return [list $disp $html]
7310}
7311
7312##############################################################################
7313# Network management
7314##############################################################################
7315
7316#
7317# Return list of networks for a given group and a given privilege
7318#
7319# Input:
7320#   - parameters:
7321#	- dbfd : database handle
7322#	- idgrp : group id
7323#	- priv : "consult", "dhcp" or "acl"
7324# Output:
7325#   - return value: list of networks {idnet cidr4 cidr6 name}
7326#
7327# History
7328#   2004/01/16 : pda/jean : specification and design
7329#   2004/08/06 : pda/jean : extend permissions on networks
7330#   2004/10/05 : pda/jean : adapt to new permissions
7331#   2006/05/24 : pda/jean/boggia : extract in a primary function
7332#   2010/12/01 : pda      : i18n
7333#
7334
7335proc allowed-networks {dbfd idgrp priv} {
7336    #
7337    # Build a WHERE clause from the given privilege
7338    #
7339
7340    switch -- $priv {
7341	consult {
7342	    set w1 ""
7343	    set w2 ""
7344	}
7345	dhcp {
7346	    set w1 "AND p.$priv > 0"
7347	    set w2 "AND n.$priv > 0"
7348	}
7349	acl {
7350	    set w1 "AND p.$priv > 0"
7351	    set w2 ""
7352	}
7353    }
7354
7355    #
7356    # Get all allowed networks for this group and for this privilege
7357    #
7358
7359    set lnet {}
7360    set sql "SELECT n.idnet, n.name, n.addr4, n.addr6
7361			FROM dns.network n, dns.p_network p
7362			WHERE n.idnet = p.idnet
7363			    AND p.idgrp = $idgrp
7364			    $w1 $w2
7365			ORDER BY addr4, addr6"
7366    pg_select $dbfd $sql tab {
7367	lappend lnet [list $tab(idnet) $tab(addr4) $tab(addr6) $tab(name)]
7368    }
7369
7370    return $lnet
7371}
7372
7373#
7374# Return list of allowed vlan-id of L2-only networks for a given group
7375#
7376# Input:
7377#   - parameters:
7378#	- dbfd : database handle
7379#	- idgrp : group id
7380# Output:
7381#   - return value: list of vlan ids
7382#
7383# History
7384#   2013/01/24 : jean : adaptated from allowed-networks
7385#
7386
7387proc allowed-l2only {dbfd idgrp} {
7388    #
7389    # Get all allowed vlans for this group
7390    #
7391
7392    set lvlan {}
7393    set sql "SELECT vlanid FROM topo.p_l2only WHERE idgrp = $idgrp"
7394    pg_select $dbfd $sql tab {
7395	lappend lvlan $tab(vlanid)
7396    }
7397
7398    return $lvlan
7399}
7400
7401#
7402# Returns the list of networks allowed for a group (with a given privilege)
7403# ready to use with form-menu.
7404#
7405# Input:
7406#   - parameters:
7407#	- dbfd : database handle
7408#	- idgrp : group id
7409#	- priv : "consult", "dhcp" or "acl"
7410# Output:
7411#   - return value: list of elements {id name}
7412#
7413# History
7414#   2006/05/24 : pda/jean/boggia : extract procedure heart in allowed-networks
7415#   2010/12/01 : pda      : i18n
7416#   2012/04/26 : pda      : fix bug where non-html chars are replaced here
7417#
7418
7419proc read-networks {dbfd idgrp priv} {
7420    set lnet {}
7421    foreach r [allowed-networks $dbfd $idgrp $priv] {
7422	lassign $r idnet cidr4 cidr6 name
7423	lappend lnet [list $idnet [format "%s\t%s\t(%s)" $cidr4 $cidr6 $name]]
7424    }
7425    return $lnet
7426}
7427
7428#
7429# Check a network id as returned in a form field. This check is done
7430# according to a given group and a given privilege.
7431#
7432# Input:
7433#   - parameters:
7434#	- dbfd : database handle
7435#	- netid : id of network to check
7436#	- idgrp : group id
7437#	- priv : "consult", "dhcp" or "acl"
7438#	- version : 4, 6 or {4 6}
7439#	- _msg : empty string or error message
7440# Output:
7441#   - return value: list of cidr
7442#   - parameter _msg : empty string or error message
7443#
7444# History
7445#   2004/10/05 : pda/jean : specification and design
7446#   2010/12/01 : pda      : i18n
7447#
7448
7449proc check-netid {dbfd netid idgrp priv version _msg} {
7450    upvar $_msg msg
7451
7452    #
7453    # Check syntax of id
7454    #
7455    set netid [string trim $netid]
7456    if {! [regexp {^[0-9]+$} $netid]} then {
7457	set msg [mc "Invalid network id '%s'" $netid]
7458	return {}
7459    }
7460
7461    #
7462    # Convert privilege into an sql where clause
7463    #
7464
7465    switch -- $priv {
7466	consult {
7467	    set w1 ""
7468	    set w2 ""
7469	    set c [mc "You cannot read this network"]
7470	}
7471	dhcp {
7472	    set w1 "AND p.$priv > 0"
7473	    set w2 "AND n.$priv > 0"
7474	    set c [mc "You do not have DHCP access to this network"]
7475	}
7476	acl {
7477	    set w1 "AND p.$priv > 0"
7478	    set w2 ""
7479	    set c [mc "You do not have ACL access to this network"]
7480	}
7481    }
7482
7483    #
7484    # Check network and read associated CIDR(s)
7485    #
7486
7487    set lcidr {}
7488    set msg ""
7489
7490    set sql "SELECT n.addr4, n.addr6
7491		    FROM dns.p_network p, dns.network n
7492		    WHERE p.idgrp = $idgrp
7493			AND p.idnet = n.idnet
7494			AND n.idnet = $netid
7495			$w1 $w2"
7496    set cidrplage4 ""
7497    set cidrplage6 ""
7498    pg_select $dbfd $sql tab {
7499	set cidrplage4 $tab(addr4)
7500	set cidrplage6 $tab(addr6)
7501    }
7502
7503    if {[lsearch -exact $version 4] == -1} then {
7504	set cidrplage4 ""
7505    }
7506    if {[lsearch -exact $version 6] == -1} then {
7507	set cidrplage6 ""
7508    }
7509
7510    set empty4 [string equal $cidrplage4 ""]
7511    set empty6 [string equal $cidrplage6 ""]
7512
7513    switch -glob $empty4-$empty6 {
7514	1-1 {
7515	    set msg $c
7516	}
7517	0-1 {
7518	    lappend lcidr $cidrplage4
7519	}
7520	1-0 {
7521	    lappend lcidr $cidrplage6
7522	}
7523	0-0 {
7524	    lappend lcidr $cidrplage4
7525	    lappend lcidr $cidrplage6
7526	}
7527    }
7528
7529    return $lcidr
7530}
7531
7532##############################################################################
7533# Edition of tabular data
7534##############################################################################
7535
7536#
7537# Generate HTML code to display and edit table content.
7538#
7539# Input:
7540#   - parameters:
7541#	- cwidth : list of column widths {w1 w2 ... wn} (unit = %)
7542#	- ctitle : list of column titles specification, each element
7543#		is {type value} where type = "html" or "text"
7544#	- cspec : list of column specifications, each element
7545#		is {id type defval}, where
7546#		- id : column id in the table, and name of field (idNN or idnNN)
7547#		- type : "text", "string N", "int N", "bool", "menu L",
7548#			"textarea {W H}" or "image URL"
7549#		- defval : default value for new lines
7550#	- dbfd : database handle
7551#	- sql : SQL request to get column values (notably the id column)
7552#	- idnum : column name of the numeric id
7553#	- _tab : in return, will contail the generated HTML code
7554# Output:
7555#   - return value: empty string or error message
7556#   - parameter _tab : HTML code
7557#
7558# History
7559#   2001/11/01 : pda      : specification and documentation
7560#   2001/11/01 : pda      : coding
7561#   2002/05/03 : pda/jean : add type menu
7562#   2002/05/06 : pda/jean : add type textarea
7563#   2002/05/16 : pda      : convert to arrgen
7564#   2010/12/04 : pda      : i18n
7565#
7566
7567proc display-tabular {cwidth ctitle cspec dbfd sql idnum _tab} {
7568    upvar $_tab tab
7569
7570    #
7571    # Minimal integrity test on column number.
7572    #
7573
7574    if {[llength $ctitle] != [llength $cspec] || \
7575		[llength $ctitle] != [llength $cwidth]} then {
7576	return [mc "Internal error: invalid tabular specification"]
7577    }
7578
7579    #
7580    # Build-up the arrgen array specification.
7581    #
7582
7583    set aspec [_build-array-spec $cwidth $ctitle $cspec]
7584    set lines {}
7585
7586    #
7587    # Display title line
7588    #
7589
7590    set l {}
7591    lappend l "Title"
7592    foreach t $ctitle {
7593	lappend l [lindex $t 1]
7594    }
7595    lappend lines $l
7596
7597    #
7598    # Display existing lines from the database
7599    #
7600
7601    pg_select $dbfd $sql tabsql {
7602	set tabsql(:$idnum) $tabsql($idnum)
7603	lappend lines [_display-tabular-line $cspec tabsql $idnum "existing"]
7604    }
7605
7606    #
7607    # Add empty lines at the end to let user enter new values
7608    #
7609
7610    foreach s $cspec {
7611	lassign $s id type defval
7612	set tabdef($id) $defval
7613    }
7614
7615    for {set i 1} {$i <= 5} {incr i} {
7616	set tabdef(:$idnum) "n$i"
7617	lappend lines [_display-tabular-line $cspec tabdef $idnum "new"]
7618    }
7619
7620    #
7621    # Generates HTML code and returns
7622    #
7623
7624    set tab [::arrgen::output "html" $aspec $lines]
7625
7626    return ""
7627}
7628
7629#
7630# Build-up a table specification (for arrgen) from display-tabular parameters
7631#
7632# Input:
7633#   - parameters: see display-tabular
7634# Output:
7635#   - return value: an "arrgen" specification
7636#
7637# History
7638#   2001/11/01 : pda      : design and documentation
7639#   2002/05/16 : pda      : convert to arrgen
7640#   2010/12/04 : pda      : i18n
7641#
7642
7643proc _build-array-spec {cwidth ctitle cspec} {
7644    #
7645    # First, build-up Title pattern
7646    #
7647
7648    set titpat "pattern Title {"
7649    foreach t $ctitle {
7650	append titpat "vbar {yes} "
7651	append titpat "chars {bold} "
7652	append titpat "align {center} "
7653	append titpat "column { "
7654	append titpat "  botbar {yes} "
7655	if {[lindex $t 0] ne "text"} then {
7656	    append titpat "  format {raw} "
7657	}
7658	append titpat "} "
7659    }
7660    append titpat "vbar {yes} "
7661    append titpat "} "
7662
7663    #
7664    # Next, normal lines
7665    #
7666
7667    set norpat "pattern Normal {"
7668    foreach t $cspec {
7669	append norpat "topbar {yes} "
7670	append norpat "vbar {yes} "
7671	append norpat "column { "
7672	append norpat "  align {center} "
7673	append norpat "  botbar {yes} "
7674	set type [lindex [lindex $t 1] 0]
7675	if {$type ne "text"} then {
7676	    append norpat "  format {raw} "
7677	}
7678	append norpat "} "
7679    }
7680    append norpat "vbar {yes} "
7681    append norpat "} "
7682
7683    #
7684    # Finally, global specifications
7685    #
7686
7687    return "global { chars {10 normal} columns {$cwidth} } $titpat $norpat"
7688}
7689
7690#
7691# Display a line of tabular data
7692#
7693# Input:
7694#   - parameters:
7695#	- cspec : see display-tabular
7696#	- tab : array indexed by fields specified in cspec (see display-tabular)
7697#	- idnum : column name of the numeric id
7698#	- new : "existing" or "new"
7699# Output:
7700#   - return value: an "arrgen" line
7701#
7702# History
7703#   2001/11/01 : pda      : specification and documentation
7704#   2001/11/01 : pda      : design
7705#   2002/05/03 : pda/jean : add type menu
7706#   2002/05/06 : pda/jean : add type textarea
7707#   2002/05/16 : pda      : convert to arrgen
7708#   2010/12/04 : pda      : i18n
7709#   2012/01/02 : pda      : add parameter new
7710#
7711
7712proc _display-tabular-line {cspec _tab idnum new} {
7713    upvar $_tab tab
7714
7715    set line {Normal}
7716    foreach s $cspec {
7717	lassign $s id type defval
7718
7719	set value $tab($id)
7720	lassign $type typekw typeopt
7721
7722	set num $tab(:$idnum)
7723	set ref $id$num
7724
7725	switch $typekw {
7726	    text {
7727		set item $value
7728	    }
7729	    string {
7730		set item [::webapp::form-text $ref 1 $typeopt 0 $value]
7731	    }
7732	    int {
7733		set item [::webapp::form-text $ref 1 $typeopt 0 $value]
7734	    }
7735	    bool {
7736		set item [::webapp::form-bool $ref $value]
7737	    }
7738	    menu {
7739		set sel 0
7740		set i 0
7741		foreach e $typeopt {
7742		    set v [lindex $e 0]
7743		    if {$v eq $value} then {
7744			set sel $i
7745		    }
7746		    incr i
7747		}
7748		set item [::webapp::form-menu $ref 1 0 $typeopt [list $sel]]
7749	    }
7750	    textarea {
7751		lassign $typeopt width height
7752		set item [::webapp::form-text $ref $height $width 0 $value]
7753	    }
7754	    image {
7755		if {$new eq "new"} then {
7756		    set item "&nbsp;"
7757		} else {
7758		    set item [format $typeopt $num]
7759		}
7760	    }
7761	}
7762	lappend line $item
7763    }
7764
7765    return $line
7766}
7767
7768##############################################################################
7769# Storing tabular data
7770##############################################################################
7771
7772#
7773# Get modifications from a form generated by display-tabular and
7774# store them if necessary in the database.
7775#
7776# Input:
7777#   - parameters:
7778#	- dbfd : database handle
7779#	- cspec : column specifications (see below)
7780#	- idnum : column name of the numeric id
7781#	- table : name of the SQL table to modify
7782#	- _ftab : array containing form field values
7783#	- check : name of a procedure to call on complete row
7784# Output:
7785#   - return value: none, this function exits if an error is encountered
7786#
7787# Notes :
7788#   - format of "cspec" is {{column type defval} ...}, where:
7789#	- column: column id in the table
7790#	- type : "text", "string N", "int N", "bool", "menu L",
7791#		"textarea {W H}" or "image URL"
7792#	- defval: the default value to store in the table
7793#		if the value is not provided
7794#   - first column of "cspec" is the key used to know if an entry must
7795#	be added or deleted.
7796#   - the check procedure will be called with parameters:
7797#		$check op dbfd _msg id idnum table _tabval
7798#	where:
7799#	- op : nop, mod, add, del
7800#	- dbfd : database handle
7801#	- _msg : error message if any
7802#	- id : id (value) of entry to modify (null if op == add)
7803#	- idnum : column name of the numeric id
7804#	- table : name of the SQL table to modify
7805#	- _tabval : array containing new values	(null if op == del)
7806#	the check procedure may modify _tabval.
7807#	It must returns 1 (ok) or 0 (err)
7808#
7809# History
7810#   2001/11/02 : pda      : specification and documentation
7811#   2001/11/02 : pda      : coding
7812#   2002/05/03 : pda/jean : remove an old constraint
7813#   2010/12/04 : pda      : i18n
7814#   2010/12/14 : pda      : use db lock methods
7815#   2012/01/03 : pda      : use ftab indexes rather than count until max index
7816#   2012/01/09 : pda      : add type to cspec and check parameter
7817#
7818
7819proc store-tabular {dbfd cspec idnum table _ftab check} {
7820    upvar $_ftab ftab
7821
7822    #
7823    # Lock the table
7824    #
7825
7826    d dblock [list $table]
7827
7828    #
7829    # Get used ids
7830    #
7831
7832    set key [lindex [lindex $cspec 0] 0]
7833
7834    set lid [array names ftab -regexp "^$key\[0-9\]+$"]
7835    regsub -all "\[\[:<:\]\]($key)(\[0-9\])" $lid {\2} lid
7836    set lid [lsort -increasing $lid]
7837
7838    #
7839    # Get old ids, if we have to output a precise error message
7840    # when SQL transaction has aborted.
7841    #
7842
7843    pg_select $dbfd "SELECT $key, $idnum FROM $table" tab {
7844	set okey $tab($idnum)
7845	set oldkeys($okey) $tab($key)
7846    }
7847
7848    #
7849    # Traversal of existing ids in the database
7850    #
7851
7852    foreach id $lid {
7853	if {[info exists ftab(${key}${id})]} {
7854	    _fill-tabval $cspec "" $id ftab tabval
7855
7856	    if {$tabval($key) eq ""} then {
7857		#
7858		# Delete entry
7859		#
7860
7861		set ok [_store-tabular-del $dbfd msg $id $idnum $table $check]
7862		if {! $ok} then {
7863		    #
7864		    # Deletion is not possible. Transaction may have been
7865		    # aborted. Look into the saved keys
7866		    #
7867		    set okey ""
7868		    if {[info exists oldkeys($id)]} then {
7869			set okey $oldkeys($id)
7870		    }
7871		    d dbabort [mc "delete %s" $okey] $msg
7872		}
7873	    } else {
7874		#
7875		# Modify entry
7876		#
7877
7878		set ok [_store-tabular-mod $dbfd msg $id $idnum $table tabval $check]
7879		if {! $ok} then {
7880		    d dbabort [mc "modify %s" $tabval($key)] $msg
7881		}
7882	    }
7883	}
7884    }
7885
7886    #
7887    # New entries
7888    #
7889
7890    set idnew 1
7891    while {[info exists ftab(${key}n${idnew})]} {
7892	_fill-tabval $cspec "n" $idnew ftab tabval
7893
7894	if {$tabval($key) ne ""} then {
7895	    #
7896	    # Add entry
7897	    #
7898
7899	    set ok [_store-tabular-add $dbfd msg $table tabval $check]
7900	    if {! $ok} then {
7901		d dbabort [mc "add %s" $tabval($key)] $msg
7902	    }
7903	}
7904
7905	incr idnew
7906    }
7907
7908    #
7909    # Unlock and commit modifications
7910    #
7911
7912    d dbcommit [mc "store"]
7913}
7914
7915#
7916# Read form field values, and add default values, notably for boolean
7917# types (checkboxes) which may be not present.
7918#
7919# Input:
7920#   - parameters:
7921#	- cspec : see store-tabular
7922#	- prefix : "" (existing entry) or "n" (new entry)
7923#	- num : entry number
7924#	- _ftab : form field values (see webapp/get-data)
7925#	- _tabval : array to fill
7926# Output:
7927#   - return value: none
7928#   - parameter _tabval : array filled with usable values
7929#
7930# Example :
7931#   - if cspec = {{login} {name}} and prefix = "n" and num = "5"
7932#     then we search ftab(loginn5) et ftab(namen5) and we place found
7933#	(or default) values in in tabval(login) and tabval(name)
7934#
7935# History :
7936#   2001/04/01 : pda      : design
7937#   2001/04/03 : pda      : documentation
7938#   2001/11/02 : pda      : extension
7939#   2010/12/04 : pda      : i18n
7940#
7941
7942proc _fill-tabval {cspec prefix num _ftab _tabval} {
7943    upvar $_ftab ftab
7944    upvar $_tabval tabval
7945
7946    catch {unset tabval}
7947
7948    foreach c $cspec {
7949	lassign $c var type defval
7950
7951	set form ${var}${prefix}${num}
7952
7953	if {[info exists ftab($form)]} then {
7954	    set tabval($var) [string trim [lindex $ftab($form) 0]]
7955	} else {
7956	    switch [lindex $type 0] {
7957		bool {
7958		    # boolean not checked is absent from form values
7959		    set tabval($var) 0
7960		}
7961		image {
7962		    # don't set variable
7963		    # the generated value is used as a comparison
7964		    # in order to check if value has been modified
7965		}
7966		default {
7967		    set tabval($var) $defval
7968		}
7969	    }
7970	}
7971    }
7972}
7973
7974#
7975# Modify an entry
7976#
7977# Input:
7978#   - parameters:
7979#	- dbfd : database handle
7980#	- _msg : in return, error message if any
7981#	- id : id (value) of entry to modify
7982#	- idnum : column name of the numeric id
7983#	- table : name of the SQL table to modify
7984#	- _tabval : array containing new values
7985#	- check : name of a procedure to call
7986# Output:
7987#   - return value: 1 if ok, 0 if error
7988#   - parameters:
7989#	- msg : error message if an error occurred
7990#
7991# History :
7992#   2001/04/01 : pda      : design
7993#   2001/04/03 : pda      : documentation
7994#   2001/11/02 : pda      : generalization
7995#   2004/01/20 : pda/jean : add NULL if empty string (for ipv6)
7996#   2010/12/04 : pda      : i18n
7997#
7998
7999proc _store-tabular-mod {dbfd _msg id idnum table _tabval check} {
8000    upvar $_msg msg
8001    upvar $_tabval tabval
8002
8003    #
8004    # There is no need to modify anything if all values are identical.
8005    #
8006
8007    set same 1
8008    pg_select $dbfd "SELECT * FROM $table WHERE $idnum = $id" tab {
8009	foreach attribut [array names tabval] {
8010	    if {$tabval($attribut) ne $tab($attribut)} then {
8011		set same 0
8012		break
8013	    }
8014	}
8015    }
8016
8017    if {$same} then {
8018	set ok [$check "nop" $dbfd msg $id $idnum $table tabval]
8019    } else {
8020	#
8021	# It's different, we must do the work...
8022	#
8023
8024	set ok [$check "mod" $dbfd msg $id $idnum $table tabval]
8025	if {$ok} then {
8026	    set l {}
8027	    foreach attr [array names tabval] {
8028		if {$tabval($attr) eq ""} then {
8029		    set v "NULL"
8030		} else {
8031		    set v "'[::pgsql::quote $tabval($attr)]'"
8032		}
8033		lappend l "$attr = $v"
8034	    }
8035	    set sql "UPDATE $table SET [join $l ,] WHERE $idnum = $id"
8036	    set ok [::pgsql::execsql $dbfd $sql msg]
8037	}
8038    }
8039
8040    return $ok
8041}
8042
8043#
8044# Entry deletion
8045#
8046# Input:
8047#   - parameters:
8048#	- dbfd : database handle
8049#	- _msg : in return, error message if any
8050#	- id : id (value) of entry to delete
8051#	- idnum : column name of the numeric id
8052#	- table : name of the SQL table to modify
8053# Output:
8054#   - return value: 1 if ok, 0 if error
8055#   - parameters:
8056#	- msg : error message if an error occurred
8057#
8058# History :
8059#   2001/04/03 : pda      : design
8060#   2001/11/02 : pda      : generalization
8061#   2002/05/03 : pda/jean : remove an old constraint
8062#   2010/12/04 : pda      : i18n
8063#
8064
8065proc _store-tabular-del {dbfd _msg id idnum table check} {
8066    upvar $_msg msg
8067
8068    set ok [$check "del" $dbfd msg $id $idnum $table {}]
8069    if {$ok} then {
8070	set sql "DELETE FROM $table WHERE $idnum = $id"
8071	set ok [::pgsql::execsql $dbfd $sql msg]
8072    }
8073    return $ok
8074}
8075
8076#
8077# Entry addition
8078#
8079# Input:
8080#   - parameters:
8081#	- dbfd : database handle
8082#	- _msg : in return, error message if any
8083#	- table : name of the SQL table to modify
8084#	- _tabval : array containing new values
8085# Output:
8086#   - return value: 1 if ok, 0 if error
8087#   - parameters:
8088#	- msg : error message if an error occurred
8089#
8090# History :
8091#   2001/04/01 : pda      : design
8092#   2001/04/03 : pda      : documentation
8093#   2001/11/02 : pda      : generalization
8094#   2004/01/20 : pda/jean : add NULL attribute if empty string (for ipv6)
8095#   2010/12/04 : pda      : i18n
8096#
8097
8098proc _store-tabular-add {dbfd _msg table _tabval check} {
8099    upvar $_msg msg
8100    upvar $_tabval tabval
8101
8102    set ok [$check "add" $dbfd msg {} {} $table tabval]
8103    if {$ok} then {
8104	#
8105	# Column names
8106	#
8107	set cols [array names tabval]
8108
8109	#
8110	# Column values
8111	#
8112	set vals {}
8113	foreach c $cols {
8114	    if {$tabval($c) eq ""} then {
8115		set v "NULL"
8116	    } else {
8117		set v "'[::pgsql::quote $tabval($c)]'"
8118	    }
8119	    lappend vals $v
8120	}
8121
8122	set sql "INSERT INTO $table ([join $cols ,]) VALUES ([join $vals ,])"
8123	set ok [::pgsql::execsql $dbfd $sql msg]
8124    }
8125    return $ok
8126}
8127
8128##############################################################################
8129# Internal authentication functions
8130##############################################################################
8131
8132#
8133# Internal (PostgreSQL) authenticaion management
8134#
8135# Historique
8136#   2003/05/30 : pda/jean : design
8137#   2003/06/12 : pda/jean : remove lsuser
8138#   2003/06/13 : pda/jean : add genpw, chpw and showuser
8139#   2003/06/27 : pda      : add edituser
8140#   2003/07/28 : pda      : split name and christian name
8141#   2003/12/11 : pda      : simplify
8142#   2005/05/25 : pda/jean : use ldap
8143#   2005/06/07 : pda/jean/zamboni : crypt command
8144#   2005/08/24 : pda      : add ldap port
8145#   2007/10/04 : jean     : ldap directory is no longer modified in setuser
8146#   2007/11/29 : pda/jean : merge old auth.tcl package and libauth.tcl
8147#   2011/01/02 : pda      : integration of libauth in libdns
8148#
8149
8150# Fields in pgauth.user database table
8151set libconf(fields)	{login password lastname firstname mail phone mobile fax addr}
8152
8153# Fields : <title> <field spec> <form var name> <user>
8154# with <user> = 1 if field contains information about user (else : search only)
8155set libconf(editfields) {
8156    {Login 	{string 10} login	1}
8157    {Name	{string 40} lastname	1}
8158    {Method	{yesno {%1$s Regular expression %2$s Phonetic}} phren 0}
8159    {{First name}	{string 40} firstname	1}
8160    {Method	{yesno {%1$s Regular expression %2$s Phonetic}} phrep 0}
8161    {Address	{text 3 40} addr	1}
8162    {Mail	{string 40} mail	1}
8163    {Phone	{string 15} phone	1}
8164    {Fax	{string 15} fax		1}
8165    {Mobile	{string 15} mobile	1}
8166}
8167set libconf(editrealms) {
8168    {{Realms}	{list multi ...} realms 1}
8169}
8170
8171#
8172# Tabular formats (see arrgen(n)):
8173#	- tabuchoice : user selection with clickable login
8174#	- tabumod : user add/modify form
8175#	- tabulist : user list (to display or print)
8176#
8177
8178set libconf(tabuchoice) {
8179    global {
8180	chars {10 normal}
8181	align {left}
8182	botbar {yes}
8183	columns {11 26 35 28 10}
8184	latex {
8185	    linewidth {267}
8186	}
8187    }
8188    pattern Title {
8189	title {yes}
8190	topbar {yes}
8191	chars {bold}
8192	align {center}
8193	vbar {yes}
8194	column { }
8195	vbar {yes}
8196	column { }
8197	vbar {yes}
8198	column { }
8199	vbar {yes}
8200	column { }
8201	vbar {yes}
8202	column { }
8203	vbar {yes}
8204    }
8205    pattern User {
8206	vbar {yes}
8207	column {
8208	    format {raw}
8209	}
8210	vbar {yes}
8211	column { }
8212	vbar {yes}
8213	column { }
8214	vbar {yes}
8215	column { }
8216	vbar {yes}
8217	column { }
8218	vbar {yes}
8219    }
8220}
8221
8222set libconf(tabumod) {
8223    global {
8224	align {left}
8225	botbar {no}
8226	columns {25 75}
8227    }
8228    pattern {Normal} {
8229	vbar {no}
8230	column { }
8231	vbar {no}
8232	column {
8233	    format {raw}
8234	}
8235	vbar {no}
8236    }
8237}
8238
8239set libconf(tabulist) {
8240    global {
8241	chars {10 normal}
8242	align {left}
8243	botbar {yes}
8244	columns {8 16 32 10 10 10 14 10}
8245	latex {
8246	    linewidth {267}
8247	}
8248    }
8249    pattern Title {
8250	title {yes}
8251	topbar {yes}
8252	chars {bold}
8253	align {center}
8254	vbar {yes}
8255	column { }
8256	vbar {yes}
8257	column { }
8258	vbar {yes}
8259	column { }
8260	vbar {yes}
8261	column { }
8262	vbar {yes}
8263	column { }
8264	vbar {yes}
8265	column { }
8266	vbar {yes}
8267	column { }
8268	vbar {yes}
8269	column { }
8270	vbar {yes}
8271    }
8272    pattern User {
8273	chars {8}
8274	vbar {yes}
8275	column { }
8276	vbar {yes}
8277	column { }
8278	vbar {yes}
8279	column { }
8280	vbar {yes}
8281	column { }
8282	vbar {yes}
8283	column { }
8284	vbar {yes}
8285	column { }
8286	vbar {yes}
8287	column { }
8288	vbar {yes}
8289	column { }
8290	vbar {yes}
8291    }
8292}
8293
8294######################################
8295# User management
8296######################################
8297
8298#
8299# Read user entry
8300#
8301# Input:
8302#   - parameters :
8303#	- dbfd : database handle
8304#	- login : user login
8305#	- tab : array containing, in return, user information
8306# Output:
8307#   - return value : 1 if found, 0 if not found
8308#   - parameter tab :
8309#	tab(login)	login
8310#	tab(lastname)	name
8311#	tab(firstname)	christian name
8312#	tab(mail)	email address
8313#	tab(phone)	phone number
8314#	tab(fax)	facsimile number
8315#	tab(mobile)	mobile phone number
8316#	tab(addr)	postal address
8317#	tab(encryption)	"crypt" if password is encrypted
8318#	tab(password)	password (crypted or not)
8319#	tab(realms)	list of realms to which user belongs
8320#
8321# History
8322#   2003/05/13 : pda/jean : design
8323#   2003/05/30 : pda/jean : add realms
8324#   2005/05/25 : pda/jean : add ldap code
8325#   2007/12/04 : pda/jean : remove ldap code
8326#   2010/12/29 : pda      : i18n and netmagis merge
8327#
8328
8329proc pgauth-getuser {dbfd login _tab} {
8330    upvar $_tab tab
8331    global libconf
8332
8333    set found 0
8334    set qlogin [::pgsql::quote $login]
8335    set sql "SELECT * FROM pgauth.user WHERE login = '$qlogin'"
8336    pg_select $dbfd $sql tabsql {
8337	foreach c $libconf(fields) {
8338	    set tab($c) $tabsql($c)
8339	}
8340	set found 1
8341    }
8342    set tab(realms) {}
8343    set sql "SELECT realm FROM pgauth.member WHERE login = '$qlogin'"
8344    pg_select $dbfd $sql tabsql {
8345	lappend tab(realms) $tabsql(realm)
8346    }
8347    return $found
8348}
8349
8350#
8351# Modify or create a user
8352#
8353# Input:
8354#   - parameters :
8355#	- dbfd : database handle
8356#	- tab : see getuser
8357#	- transact : "transaction" (by default) or "no transaction"
8358# Output:
8359#   - return value : empty string or error message
8360#
8361# Note : if password field is nul, a crypted "*" is set by default
8362#	(meaning that this account is not active)
8363#
8364# History
8365#   2003/05/13 : pda/jean : design
8366#   2003/05/30 : pda/jean : add realms
8367#   2003/08/05 : pda      : add transactions
8368#   2007/12/04 : pda/jean : specialization for postgresql
8369#   2010/12/29 : pda      : i18n and netmagis merge
8370#
8371
8372proc pgauth-setuser {dbfd _tab {transact transaction}} {
8373    upvar $_tab tab
8374    global libconf
8375
8376    if {! [regexp -- {^[a-z][-a-z0-9\.]*$} $tab(login)]} then {
8377	return [mc {Invalid login syntax (^[a-z][-a-z0-9\.]*$)}]
8378    }
8379
8380    if {$transact eq "transaction"} then {
8381	set tr 1
8382	d dblock {pgauth.user pgauth.member}
8383    } else {
8384	set tr 0
8385    }
8386
8387    #
8388    # Remove user
8389    #
8390    set msg [pgauth-deluser $dbfd $tab(login) "no transaction"]
8391    if {$msg ne ""} then {
8392	if {$tr} then {
8393	    d dbabort [mc "delete %s" $tab(login)] $msg
8394	}
8395	return $msg
8396    }
8397
8398    #
8399    # If password does not exist, invalid login
8400    #
8401    if {! [info exists tab(password)]} then {
8402	set tab(password) "*"
8403    }
8404
8405    #
8406    # Insert user data in database
8407    #
8408    set cols {}
8409    set vals {}
8410    foreach c $libconf(fields) {
8411	if {[info exists tab($c)]} then {
8412	    lappend cols $c
8413	    lappend vals "'[::pgsql::quote $tab($c)]'"
8414	}
8415    }
8416    set cols [join $cols ","]
8417    set vals [join $vals ","]
8418    set sql "INSERT INTO pgauth.user ($cols) VALUES ($vals)"
8419    if {![::pgsql::execsql $dbfd $sql msg]} then {
8420	if {$tr} then {
8421	    d dbabort [mc "add %s" $tab(login)] $msg
8422	}
8423	return [mc {Unable to insert account '%1$s': %2$s} $tab(login) $msg]
8424    }
8425
8426    #
8427    # Insert membership
8428    #
8429    set sql ""
8430    foreach r $tab(realms) {
8431	append sql "INSERT INTO pgauth.member (login, realm) VALUES
8432			('$tab(login)', '$r') ;"
8433    }
8434    if {! [::pgsql::execsql $dbfd $sql msg]} then {
8435	if {$tr} then {
8436	    d dbabort [mc "add %s" $tab(login)] $msg
8437	}
8438	return [mc {Unable to insert '%1$s' membership: %2$s} $tab(login) $msg]
8439    }
8440
8441    #
8442    # Transaction end
8443    #
8444    if {$tr} then {
8445	d dbcommit [mc "add %s" $tab(login)]
8446    }
8447
8448    return ""
8449}
8450
8451#
8452# Remove user entry
8453#
8454# Input:
8455#   - parameters :
8456#	- dbfd : database handle
8457#	- login : login name
8458#	- transact : "transaction" (default) or "no transaction"
8459# Output:
8460#   - return value : empty string or error message
8461#
8462# History
8463#   2003/05/13 : pda/jean : design
8464#   2003/05/30 : pda/jean : add realms
8465#   2007/12/04 : pda/jean : specialization for postgresql
8466#   2010/12/29 : pda      : i18n and netmagis merge
8467#
8468
8469proc pgauth-deluser {dbfd login {transact transaction}} {
8470    if {$transact eq "transaction"} then {
8471	set tr 1
8472	d dblock {pgauth.user pgauth.member}
8473    } else {
8474	set tr 0
8475    }
8476
8477    set qlogin [::pgsql::quote $login]
8478    set sql "DELETE FROM pgauth.member WHERE login = '$qlogin'"
8479    if {! [::pgsql::execsql $dbfd $sql msg]} then {
8480	if {$tr} then {
8481	    d dbabort [mc "delete %s" $login] $msg
8482	}
8483	return $msg
8484    }
8485
8486    set sql "DELETE FROM pgauth.user WHERE login = '$qlogin'"
8487    if {! [::pgsql::execsql $dbfd $sql msg]} then {
8488	if {$tr} then {
8489	    d dbabort [mc "delete %s" $login] $msg
8490	}
8491	return $msg
8492    }
8493
8494
8495    if {$tr} then {
8496	d dbcommit [mc "add %s" $login]
8497    }
8498
8499    return ""
8500}
8501
8502#
8503# Search a user with criterion
8504#
8505# Input:
8506#   - parameters :
8507#	- dbfd : database handle
8508#	- tabcrit : array containing criterion
8509#		login, lastname, firstname, addr, mail, phone, mobile,
8510#		fax, realm, or phlast, phfirst for phonetic searches
8511#	- sort (optional) : list {sort...} where
8512#		sort = +/- sort-criterion
8513# Output:
8514#   - return value : list of found logins
8515#
8516# Note : each criterion is a regexp (* and ? only)
8517#
8518# History
8519#   2003/06/06 : pda/jean : design
8520#   2003/08/01 : pda/jean : phonetic criterions
8521#   2003/08/11 : pda      : search "or" on more than one realm
8522#   2007/12/04 : pda/jean : specialization for postgresql
8523#   2010/12/29 : pda      : i18n and netmagis merge
8524#
8525
8526proc pgauth-searchuser {dbfd _tabcrit {sort {+lastname +firstname}}} {
8527    upvar $_tabcrit tabcrit
8528
8529    #
8530    # Build-up the "where" clause
8531    #
8532
8533    set clauses {}
8534    set nwheres 0
8535    set from ""
8536    foreach c {login phlast phfirst lastname firstname addr mail phone mobile fax realm} {
8537	if {[info exists tabcrit($c)]} then {
8538	    set re $tabcrit($c)
8539	    if {$re ne ""} then {
8540		set re [::pgsql::quote $re]
8541		# quote SQL special characters
8542		regsub -all -- {%} $re {\\%} re
8543		regsub -all -- {_} $re {\\_} re
8544		# quote *our* special characters
8545		regsub -all -- {\*} $re {%} re
8546		regsub -all -- {\?} $re {_} re
8547
8548		if {$c eq "realm"} then {
8549		    set from ", pgauth.member"
8550		    set table "pgauth.member"
8551		    lappend clauses "pgauth.user.login = member.login"
8552		} else {
8553		    set table "pgauth.user"
8554		}
8555
8556		if {$c eq "phlast" || $c eq "phfirst"} then {
8557		    lappend clauses "$table.$c = pgauth.soundex('$re')"
8558		} elseif {$c eq "realm"} then {
8559		    set or {}
8560		    foreach r $tabcrit(realm) {
8561			set qr [::pgsql::quote $r]
8562			lappend or "$table.realm = '$qr'"
8563		    }
8564		    if {[llength $or] > 0} then {
8565			set sor [join $or " OR "]
8566			lappend clauses "($sor)"
8567		    }
8568		} else {
8569		    # ILIKE = case insensitive LIKE
8570		    lappend clauses "$table.$c ILIKE '$re'"
8571		}
8572		incr nwheres
8573	    }
8574	}
8575    }
8576    if {$nwheres > 0} then {
8577	set where [join $clauses " AND "]
8578	set where "WHERE $where"
8579    } else {
8580	set where ""
8581    }
8582
8583    #
8584    # Build-up sort criterion
8585    #
8586
8587    set sqlsort {}
8588    set sqldistinct {}
8589    foreach t $sort {
8590	set way [string range $t 0 0]
8591	set col [string range $t 1 end]
8592	switch -- $way {
8593	    -		{ set way "DESC" }
8594	    +  		-
8595	    default	{ set way "ASC" }
8596	}
8597	if {$col in {login lastname firstname mail phone addr mobile fax}} then {
8598	    lappend sqlsort "pgauth.user.$col $way"
8599# XXX : I don't understand why I used this distinct clause
8600#	    lappend sqldistinct "pgauth.user.$col"
8601	}
8602    }
8603    if {[llength $sqlsort] == 0} then {
8604	set orderby ""
8605    } else {
8606	set orderby [join $sqlsort ", "]
8607	set orderby "ORDER BY $orderby"
8608    }
8609
8610    if {[llength $sqldistinct] == 0} then {
8611	set distinct ""
8612    } else {
8613	set distinct [join $sqldistinct ", "]
8614	set distinct "DISTINCT ON ($distinct)"
8615    }
8616
8617    #
8618    # Build the list of logins
8619    #
8620
8621    set lusers {}
8622    set sql "SELECT $distinct pgauth.user.login
8623		FROM pgauth.user $from
8624		$where
8625		$orderby"
8626    pg_select $dbfd $sql tab {
8627	lappend lusers $tab(login)
8628    }
8629
8630    return $lusers
8631}
8632
8633#
8634# Process password modification
8635#
8636# Input:
8637#   - parameters :
8638#	- dbfd : database handle
8639#	- login : user login
8640#	- action : list {action parameters} where:
8641#		action = "block"    (no parameter)
8642#		action = "generate" (no parameter)
8643#		action = "change"   (parameters = password twice)
8644#	- mail : {mail} or {nomail}, if the password must be sent by mail or not
8645#		In the "mail" case, this parameter is a list
8646#			{mail from replyto cc bcc subject body}
8647#	- _newpw : in return, new password
8648# Output:
8649#   - return value : empty string or error message
8650#
8651# History
8652#   2003/06/13 : pda/jean : design
8653#   2003/12/08 : pda      : more complete "mail" parameter
8654#   2010/12/29 : pda      : i18n and netmagis merge
8655#
8656
8657proc pgauth-chpw {dbfd login action mail _newpw} {
8658    upvar $_newpw newpw
8659    global libconf
8660
8661    if {! [pgauth-getuser $dbfd $login tab]} then {
8662	return [mc "Login '%s' does not exist" $login]
8663    }
8664
8665    switch -- [lindex $action 0] {
8666	block {
8667	    set newpw [mc "<invalid>"]
8668	    set tab(password) "*"
8669	}
8670	generate {
8671	    set newpw [pgauth-genpw]
8672	    set tab(password) [pgauth-crypt $newpw]
8673	}
8674	change {
8675	    lassign $action c pw1 pw2
8676
8677	    if {$pw1 ne $pw2} then {
8678		return [mc "Password mismatch"]
8679	    }
8680	    set newpw $pw1
8681
8682	    if {[regexp {[\\'"`()]} $newpw]} then {
8683		return [mc "Invalid character in password"]
8684	    }
8685
8686	    set minpwlen [::dnsconfig get "authpgminpwlen"]
8687	    set maxpwlen [::dnsconfig get "authpgmaxpwlen"]
8688
8689	    if {[string length $newpw] < $minpwlen} then {
8690		return [mc "Password to short (< %s characters)" $minpwlen]
8691	    }
8692	    set newpw [string range $newpw 0 [expr $maxpwlen-1]]
8693
8694	    set tab(password) [pgauth-crypt $newpw]
8695	}
8696	default {
8697	    return [mc "Internal error: invalid 'action' value (%s)" $action]
8698	}
8699    }
8700
8701    if {[lindex $mail 0] eq "mail"} then {
8702	lassign $mail b from repl cc bcc subj body
8703	if {[::webapp::valid-email $tab(mail)]} then {
8704	    set body [format $body $login $newpw]
8705	    ::webapp::mail $from $repl $tab(mail) $cc $bcc $subj $body
8706	} else {
8707	    return [mc "Invalid mail address, password is not modified"]
8708	}
8709    }
8710
8711    return [pgauth-setuser $dbfd tab]
8712}
8713
8714######################################
8715# Pgsql realm management
8716######################################
8717
8718#
8719# List existing realms
8720#
8721# Input:
8722#   - parameters :
8723#	- dbfd : database handle
8724#	- tab : in return, array containing realm list
8725#		tab(<realm>) {<descr> <list of users>}
8726# Output:
8727#   - return value : (none)
8728#
8729# History
8730#   2003/05/30 : pda/jean : design
8731#   2007/12/04 : pda/jean : specialization for postgresql
8732#   2010/12/27 : pda      : i18n and netmagis merge
8733#
8734
8735proc pgauth-lsrealm {dbfd _tab} {
8736    upvar $_tab tab
8737
8738    set sql "SELECT * FROM pgauth.realm"
8739    pg_select $dbfd $sql tabsql {
8740	set realm $tabsql(realm)
8741	set descr $tabsql(descr)
8742	set admin $tabsql(admin)
8743	set members {}
8744	set sqlm "SELECT login FROM pgauth.member WHERE realm = '$realm'"
8745	pg_select $dbfd $sqlm tabm {
8746	    lappend members $tabm(login)
8747	}
8748	set tab($realm) [list $descr $members $admin]
8749    }
8750}
8751
8752#
8753# Add a PG realm into database
8754#
8755# Input:
8756#   - parameters :
8757#	- dbfd : database handle
8758#	- realm : realm name
8759#	- descr : realm description
8760#	- admin : 0 or 1
8761#	- _msg : in return, error message (if any)
8762# Output:
8763#   - return value : 1 (ok) or 0 (error)
8764#   - parameter _msg : error message if any
8765#
8766# History
8767#   2003/05/30 : pda/jean : design
8768#   2007/12/04 : pda/jean : specialization for postgresql
8769#   2010/12/27 : pda      : i18n and netmagis merge
8770#   2011/01/07 : pda      : add admin
8771#
8772
8773proc pgauth-addrealm {dbfd realm descr admin _msg} {
8774    upvar $_msg msg
8775
8776    set msg ""
8777    if {[regexp -- {^[a-z][-a-z0-9]*$} $realm]} then {
8778	set qrealm [::pgsql::quote $realm]
8779	set qdescr [::pgsql::quote $descr]
8780	set sql "INSERT INTO pgauth.realm (realm, descr, admin)
8781				VALUES ('$qrealm', '$qdescr', $admin)"
8782	if {! [::pgsql::execsql $dbfd $sql m]} then {
8783	    set msg [mc {Unable to insert realm '%1$s': %2$s} $realm $m]
8784	}
8785    } else {
8786	set msg [mc {Invalid realm syntax (^[a-z][-a-z0-9]*$)}]
8787    }
8788    return [string equal $msg ""]
8789}
8790
8791#
8792# Remove a realm from database
8793#
8794# Input:
8795#   - parameters :
8796#	- dbfd : database handle
8797#	- realm : realm name
8798#	- _msg : in return, error message (if any)
8799# Output:
8800#   - return value : 1 (ok) or 0 (error)
8801#   - parameter _msg : error message if any
8802#
8803# Note : this function do not remove realms which have members
8804#   (thanks to the SQL constraint)
8805#
8806# History
8807#   2003/05/30 : pda/jean : design
8808#   2007/12/04 : pda/jean : specialization for postgresql
8809#   2010/12/28 : pda      : i18n and netmagis merge
8810#
8811
8812proc pgauth-delrealm {dbfd realm _msg} {
8813    upvar $_msg msg
8814
8815    set msg ""
8816    set qrealm [::pgsql::quote $realm]
8817    set sql "DELETE FROM pgauth.realm WHERE realm = '$qrealm'"
8818    if {! [::pgsql::execsql $dbfd $sql m]} then {
8819	set msg [mc {Unable to remove realm '%1$s': %2$s} $realm $m]
8820    }
8821    return [string equal $msg ""]
8822}
8823
8824#
8825# Modify a realm
8826#
8827# Input:
8828#   - parameters :
8829#	- dbfd : database handle
8830#	- realm : realm name
8831#	- descr : realm description
8832#	- admin : 0 or 1
8833#	- members : list of members
8834#	- _msg : in return, error message (if any)
8835# Output:
8836#   - return value : 1 (ok) or 0 (error)
8837#   - parameter _msg : error message if any
8838#
8839# History
8840#   2003/06/04 : pda/jean : design
8841#   2007/12/04 : pda/jean : specialization for postgresql
8842#   2010/12/29 : pda      : i18n and netmagis merge
8843#   2011/01/07 : pda      : add admin
8844#
8845
8846proc pgauth-setrealm {dbfd realm descr admin members _msg} {
8847    upvar $_msg msg
8848
8849    set qrealm [::pgsql::quote $realm]
8850
8851    d dblock {}
8852
8853    #
8854    # If realm does not exists, create it. If it exists, modify description.
8855    #
8856
8857    set sql "SELECT realm FROM pgauth.realm WHERE realm = '$qrealm'"
8858    set found 0
8859    pg_select $dbfd $sql tab {
8860	set found 1
8861    }
8862    if {! $found} then {
8863	if {! [pgauth-addrealm $dbfd $realm $descr $admin msg]} then {
8864	    d dbabort [mc "add %s" $realm] $msg
8865	}
8866    } else {
8867	set qdescr [::pgsql::quote $descr]
8868	set sql "UPDATE pgauth.realm
8869			SET descr = '$qdescr', admin = $admin
8870			WHERE realm = '$qrealm'"
8871	if {! [::pgsql::execsql $dbfd $sql m]} then {
8872	    d dbabort [mc "modify %s" $realm] $msg
8873	}
8874    }
8875
8876    #
8877    # Remove member list
8878    #
8879    set sql "DELETE FROM pgauth.member WHERE realm = '$qrealm'"
8880    if {! [::pgsql::execsql $dbfd $sql m]} then {
8881	d dbabort [mc "modify %s" $realm] $msg
8882    }
8883
8884    #
8885    # Update member list
8886    #
8887    foreach login $members {
8888	set qlogin [::pgsql::quote $login]
8889	set sql "INSERT INTO pgauth.member (login, realm)
8890			VALUES ('$qlogin', '$qrealm')"
8891	if {! [::pgsql::execsql $dbfd $sql msg]} then {
8892	    d dbabort [mc "add %s" "$login/$realm"] $msg
8893	}
8894    }
8895
8896    d dbcommit [mc "modify %s" $realm]
8897
8898    set msg ""
8899    return 1
8900}
8901
8902#
8903# Returns an HTML menu to select realms
8904#
8905# Input:
8906#   - parameters :
8907#	- dbfd : database handle
8908#	- var : name of form variable
8909#	- multiple : 1 if multiple choice, 0 if only one choice
8910#	- realmsel : list of preselected realms (or empty list)
8911# Output:
8912#   - return value : HTML code
8913#
8914# History
8915#   2003/06/03 : pda/jean : design
8916#   2003/06/13 : pda/jean : add parameter realmsel
8917#   2003/06/27 : pda      : package
8918#   2010/12/28 : pda      : i18n and netmagis merge
8919#
8920
8921proc pgauth-htmlrealmmenu {dbfd var multiple realmsel} {
8922    #
8923    # Index pre-selected realms
8924    #
8925    foreach r $realmsel {
8926	set tabsel($r) ""
8927    }
8928
8929    #
8930    # Get realm list
8931    #
8932    pgauth-lsrealm $dbfd tabrlm
8933
8934    #
8935    # Build key/value list for the menu
8936    #
8937
8938    set l {}
8939    set lsel {}
8940    set idx 0
8941    foreach r [lsort [array names tabrlm]] {
8942	lappend l [list $r $r]
8943	if {[info exists tabsel($r)]} then {
8944	    lappend lsel $idx
8945	}
8946	incr idx
8947    }
8948
8949    #
8950    # Multiple choices?
8951    #
8952
8953    if {$multiple} then {
8954	set size [llength [array names tabrlm]]
8955    } else {
8956	set size 1
8957    }
8958
8959    return [::webapp::form-menu $var $size $multiple $l $lsel]
8960}
8961
8962######################################
8963# HTML account management
8964######################################
8965
8966#
8967# Heart of CGI script for applications which manage users.
8968#
8969# Input:
8970#   - parameters :
8971#	- ae : auth execution environment of the script, as an indexed array:
8972#		dbfd : access to auth database
8973#		url : url of CGI script
8974#		realms : realms where application user can belong to.
8975#			If realms = {}, we can access every realm
8976#			If only one realm, realm list is not displayed when
8977#				adding a user
8978#		maxrealms : maximum number of realms displayed in the listbox
8979#			or 0 to use exact number of displayed realms
8980#		page-* : HTML/LaTeX templates
8981#			-index : index of different actions
8982#			-ok : action done
8983#			-add1 : first page of user add
8984#			-choice : choice of user, if more than one found
8985#			-mod : parameter modification
8986#			-del : confirm user removal
8987#			-passwd : actions on user password
8988#			-list : list of users
8989#			-listtex : list of users in latex format
8990#			-sel : user selection with criterion
8991#		specif : application specific user data
8992#				{{<title> <type>} ...}
8993#			(see ::webapp::form-field for type)
8994#		script-* : scripts to execute to access and display user
8995#			characteristics, specific to an application:
8996#			- getuser : display user information and returns a
8997#				list {value ...} in the same order than
8998#				in "specif" list
8999#			- deluser : remove user from application
9000#			- setuser : add or modify user in application
9001#			- chkuser : check if a user modification is authorized
9002#		mailfrom : mail header in case of password generation
9003#		mailreplyto : mail header in case of password generation
9004#		mailcc : mail header in case of password generation
9005#		mailbcc : mail header in case of password generation
9006#		mailsubject : mail header in case of password generation
9007#		mailbody : mail header in case of password generation
9008#	- ftab : form tab
9009# Output:
9010#   - return value : (none)
9011#   - stdout : an HTML page
9012#
9013# History
9014#   2003/07/29 : pda      : design
9015#   2003/07/31 : pda/jean : done
9016#   2003/12/14 : pda      : add mail*
9017#   2010/12/29 : pda      : i18n and netmagis merge
9018#   2011/01/07 : pda      : add ftab array
9019#
9020
9021proc pgauth-accmanage {_ae _ftab} {
9022    upvar $_ae ae
9023    upvar $_ftab ftab
9024
9025    set form {
9026	{action 0 1}
9027	{state  0 1}
9028    }
9029    pgauth-get-data ftab $form
9030    ::webapp::import-vars ftab $form
9031
9032    switch -- $action {
9033	add     { set l [pgauth-ac-add       ae ftab $state] }
9034	list    -
9035	print   { set l [pgauth-ac-consprn   ae ftab $state $action] }
9036	del     -
9037	mod     -
9038	passwd  { set l [pgauth-ac-delmodpwd ae ftab $state $action] }
9039	default { set l [pgauth-ac-nothing   ae ftab $state] }
9040    }
9041    lassign $l format page lsubst
9042
9043    lappend lsubst [list %ACTION% $action]
9044    d urlset "%URLFORM%" $ae(url) {}
9045    d result $page $lsubst
9046    exit 0
9047}
9048
9049proc pgauth-get-data {_ftab form} {
9050    upvar $_ftab ftab
9051
9052    if {[llength [::webapp::get-data ftab $form]] != [llength $form]} then {
9053	d error [mc "Invalid input '%s'" $ftab(_error)]
9054    }
9055}
9056
9057proc pgauth-ac-nothing {_ae _ftab state} {
9058    upvar $_ae ae
9059    upvar $_ftab ftab
9060
9061    return [list "html" $ae(page-index) {}]
9062}
9063
9064proc pgauth-ac-add {_ae _ftab state} {
9065    upvar $_ae ae
9066    upvar $_ftab ftab
9067
9068    set lsubst {}
9069    switch -- $state {
9070	name {
9071	    #
9072	    # User name has been introduced. Search this name.
9073	    #
9074	    set form {
9075		    {lastname 1 1}
9076		}
9077	    pgauth-get-data ftab $form
9078
9079	    set lastname [lindex $ftab(lastname) 0]
9080	    set tabcrit(phlast) $lastname
9081	    set lusers [pgauth-searchuser $ae(dbfd) tabcrit {+lastname +firstname}]
9082	    set nbut [llength $lusers]
9083
9084	    if {$nbut > 0} then {
9085		#
9086		# Some users match this name.
9087		#
9088		#	%ACTION%
9089		#	%MESSAGE%
9090		#	%LISTUSERS%
9091		#	%NONE%
9092		#
9093		set qlast [::webapp::html-string $lastname]
9094		set message [mc "Some accounts match '%s'. Choose one, or ask for a new account" $qlast]
9095		lappend lsubst [list %MESSAGE% $message]
9096
9097		lappend lsubst [list %LISTUSERS% \
9098				    [pgauth-ac-display-choice ae $lusers "ajout"] \
9099				]
9100
9101		d urlset "" $ae(url) [list {action add} \
9102					{state nouveau} \
9103					[list "lastname" $lastname] \
9104				    ]
9105		set url [d urlget ""]
9106		set aucun [::webapp::helem "form" \
9107				    [::webapp::form-submit {} [mc "Create a new account"]]
9108				    "method" "post" "action" $url]
9109		lappend lsubst [list %NONE% $aucun]
9110
9111		set page $ae(page-choice)
9112	    } else {
9113		#
9114		# No user match. Prepare the form to add a new user.
9115		#
9116		#	%ACTION%
9117		#	%STATE%
9118		#	%LOGIN%
9119		#	%PARAMUSER%
9120		#	%TITLE%
9121		#
9122		set lsubst [pgauth-ac-display-mod ae "_new" $lastname]
9123		set page $ae(page-mod)
9124	    }
9125	}
9126	morethanone {
9127	    #
9128	    # One user selected. Prepare form to input user modifications.
9129	    #
9130	    #	%ACTION%
9131	    #	%STATE%
9132	    #	%LOGIN%
9133	    #	%PARAMUSER%
9134	    #	%TITLE%
9135	    #
9136	    set form {
9137		    {login 1 1}
9138		}
9139	    pgauth-get-data ftab $form
9140
9141	    set login [lindex $ftab(login) 0]
9142	    set lsubst [pgauth-ac-display-mod ae $login ""]
9143	    set page $ae(page-mod)
9144	}
9145	nouveau {
9146	    #
9147	    # User addition required. Prepare form to input a new user.
9148	    #
9149	    #	%ACTION%
9150	    #	%LOGIN%
9151	    #	%PARAMUSER%
9152	    #
9153	    set form {
9154		    {lastname 0 1}
9155		}
9156	    pgauth-get-data ftab $form
9157
9158	    set lastname [lindex $ftab(lastname) 0]
9159
9160	    set lsubst [pgauth-ac-display-mod ae "_new" $lastname]
9161	    set page $ae(page-mod)
9162	}
9163	creation {
9164	    #
9165	    # New user data is given. Create user, and give control
9166	    # to the password modification page.
9167	    #
9168	    #	%ACTION% (passwd)
9169	    #	%LOGIN%
9170	    #
9171	    set form {
9172		    {login 1 1}
9173	    }
9174	    pgauth-get-data ftab $form
9175
9176	    set login [lindex $ftab(login) 0]
9177	    if {[pgauth-getuser $ae(dbfd) $login u]} then {
9178		d error [mc "Login '%s' already exists" $login]
9179	    }
9180
9181	    #
9182	    # New user. Ignore supplementary and give control to
9183	    # the password modification page.
9184	    #
9185	    pgauth-ac-store-mod ae ftab $login
9186
9187	    set lsubst [concat $lsubst [pgauth-ac-display-passwd ae $login]]
9188	    set page $ae(page-passwd)
9189	}
9190	ok {
9191	    #
9192	    # Store modification of an existing user.
9193	    #
9194	    #	%TITLEACTION% (ajout)
9195	    #	%COMPLEMENT%
9196	    #
9197	    set form {
9198		    {login 1 1}
9199	    }
9200	    pgauth-get-data ftab $form
9201
9202	    set login [lindex $ftab(login) 0]
9203	    if {! [pgauth-getuser $ae(dbfd) $login u]} then {
9204		d error [mc "Login '%s' does not exist" $login]
9205	    }
9206
9207	    #
9208	    # Existing user in database
9209	    #
9210	    set lsubst [pgauth-ac-store-mod ae ftab $login]
9211	    set page $ae(page-ok)
9212	}
9213	default {
9214	    set page $ae(page-add1)
9215	}
9216    }
9217    return [list "html" $page $lsubst]
9218}
9219
9220proc pgauth-ac-consprn {_ae _ftab state mode} {
9221    upvar $_ae ae
9222    upvar $_ftab ftab
9223    global libconf
9224
9225    set lsubst {}
9226    set format "html"
9227    switch -- $state {
9228	criteres {
9229	    #
9230	    # Criterion is given
9231	    #
9232	    #	%NBUSERS%
9233	    #	%S%
9234	    #	%DATE%
9235	    #	%HEURE%
9236	    #	%TABLEAU%
9237	    #
9238
9239	    set lusers [pgauth-ac-search-crit ae ftab]
9240	    if {[llength $lusers] == 0} then {
9241		#
9242		# No user found. Display again the criterion selection page.
9243		#
9244		set lsubst [pgauth-ac-display-crit ae ftab [mc "No account found"]]
9245		set page $ae(page-sel)
9246	    } else {
9247		#
9248		# Guess output format
9249		#
9250
9251		switch $mode {
9252		    list {
9253			set tabfmt "html"
9254			set page $ae(page-list)
9255		    }
9256		    print {
9257			set format "pdf"
9258			set tabfmt "latex"
9259			set page $ae(page-listtex)
9260		    }
9261		}
9262
9263		#
9264		# Display user list
9265		#
9266
9267		set lines {}
9268		lappend lines [list "Title" \
9269				    [mc "Login"] \
9270				    [mc "Name"] \
9271				    [mc "Address"] \
9272				    [mc "Mail"] \
9273				    [mc "Phone"] \
9274				    [mc "Fax"] \
9275				    [mc "Mobile"] \
9276				    [mc "Realms"] \
9277				]
9278		foreach login $lusers {
9279		    if {[pgauth-getuser $ae(dbfd) $login tab]} then {
9280			set myrealms [pgauth-ac-my-realms ae $tab(realms)]
9281			lappend lines [list "User" \
9282					    $tab(login) \
9283					    "$tab(lastname) $tab(firstname)" \
9284					    $tab(addr) \
9285					    $tab(mail) \
9286					    $tab(phone) $tab(fax) $tab(mobile) \
9287					    $myrealms
9288					] \
9289		    }
9290		}
9291		set tableau [::arrgen::output $tabfmt $libconf(tabulist) $lines]
9292
9293		#
9294		# Time
9295		#
9296
9297		set date  [clock format [clock seconds] -format "%d/%m/%Y"]
9298		set heure [clock format [clock seconds] -format "%Hh%M"]
9299
9300		lappend lsubst [list %TABLEAU% $tableau]
9301	    	lappend lsubst [list %NBUSERS% [llength $lusers]]
9302		lappend lsubst [list %DATE% $date]
9303		lappend lsubst [list %HEURE% $heure]
9304	    }
9305	}
9306	default {
9307	    #
9308	    # Initial page to select criteria
9309	    #
9310	    #	%ACTION%
9311	    #	%MESSAGE%
9312	    #	%CRITERES%
9313	    #
9314	    set lsubst [pgauth-ac-display-crit ae ftab ""]
9315	    set page $ae(page-sel)
9316	}
9317    }
9318    return [list $format $page $lsubst]
9319}
9320
9321proc pgauth-ac-delmodpwd {_ae _ftab state action} {
9322    upvar $_ae ae
9323    upvar $_ftab ftab
9324
9325    switch -- $state {
9326	criteres {
9327	    #
9328	    # Criterion was given
9329	    #
9330	    #	%LOGIN%
9331	    #	%LASTNAME%
9332	    #	%FIRSTNAME%
9333	    #
9334
9335	    set lusers [pgauth-ac-search-crit ae ftab]
9336	    switch [llength $lusers] {
9337		0 {
9338		    #
9339		    # No user found
9340		    #
9341		    set lsubst [pgauth-ac-display-crit ae ftab [mc "No account found"]]
9342		    set page $ae(page-sel)
9343		}
9344		1 {
9345		    #
9346		    # Display page to remove, modify or change password
9347		    # of an user
9348		    #
9349		    set login [lindex $lusers 0]
9350		    switch -- $action {
9351			del {
9352			    set lsubst [pgauth-ac-display-del ae $login]
9353			    set page $ae(page-del)
9354			}
9355			mod {
9356			    set lsubst [pgauth-ac-display-mod ae $login ""]
9357			    set page $ae(page-mod)
9358			}
9359			passwd {
9360			    set lsubst [pgauth-ac-display-passwd ae $login]
9361			    set page $ae(page-passwd)
9362			}
9363			default {
9364			    d error [mc "Invalid input"]
9365			}
9366		    }
9367		}
9368		default {
9369		    #
9370		    # Some users match.
9371		    #
9372		    #	%ACTION%
9373		    #	%MESSAGE%
9374		    #	%LISTUSERS%
9375		    #	%NONE%
9376		    #
9377		    set message [mc "Some accounts match criteria. Choose one"]
9378		    lappend lsubst [list %MESSAGE% $message]
9379
9380		    lappend lsubst [list %LISTUSERS% \
9381					[pgauth-ac-display-choice ae $lusers $action] \
9382				    ]
9383
9384		    lappend lsubst [list %NONE% ""]
9385		    set page $ae(page-choice)
9386		}
9387	    }
9388	}
9389	morethanone {
9390	    #
9391	    # Display page to remove, modify or change password of an user
9392	    #
9393	    set form {
9394		{login 1 1}
9395	    }
9396	    pgauth-get-data ftab $form
9397
9398	    set login [lindex $ftab(login) 0]
9399
9400	    if {! [pgauth-getuser $ae(dbfd) $login u]} then {
9401		d error [mc "Login '%s' does not exist" $login]
9402	    }
9403
9404	    switch -- $action {
9405		del {
9406		    set lsubst [pgauth-ac-display-del ae $login]
9407		    set page $ae(page-del)
9408		}
9409		mod {
9410		    set lsubst [pgauth-ac-display-mod ae $login ""]
9411		    set page $ae(page-mod)
9412		}
9413		passwd {
9414		    set lsubst [pgauth-ac-display-passwd ae $login]
9415		    set page $ae(page-passwd)
9416		}
9417		default {
9418		    d error [mc "Invalid input"]
9419		}
9420	    }
9421
9422	}
9423	ok {
9424	    #
9425	    # Perform action
9426	    #
9427
9428	    set form {
9429		{login 1 1}
9430	    }
9431	    pgauth-get-data ftab $form
9432
9433	    set login [lindex $ftab(login) 0]
9434
9435	    if {! [pgauth-getuser $ae(dbfd) $login u]} then {
9436		d error [mc "Login '%s' does not exist" $login]
9437	    }
9438
9439	    set page $ae(page-ok)
9440	    switch -- $action {
9441		del {
9442		    set lsubst [pgauth-ac-del-user ae ftab $login]
9443		}
9444		mod {
9445		    set lsubst [pgauth-ac-store-mod ae ftab $login]
9446		}
9447		passwd {
9448		    set lsubst [pgauth-ac-store-passwd ae ftab $login]
9449		}
9450		default {
9451		    d error [mc "Invalid input"]
9452		}
9453	    }
9454	}
9455	default {
9456	    #
9457	    # Initial page for criteria
9458	    #
9459	    #	%ACTION%
9460	    #	%MESSAGE%
9461	    #	%CRITERES%
9462	    #
9463	    set lsubst [pgauth-ac-display-crit ae ftab ""]
9464	    set page $ae(page-sel)
9465	}
9466    }
9467
9468    return [list "html" $page $lsubst]
9469}
9470
9471#
9472# Utility functions for pgauth-accmanage
9473#
9474
9475#
9476# Returns a realm list, extract from "realms" , where only authorized
9477# realms (i.ae. those in ae(realms)) are displayed. If ae(realms) is
9478# empty, all realms may be displayed.
9479#
9480
9481proc pgauth-ac-my-realms {_ae realms} {
9482    upvar $_ae ae
9483
9484    if {[llength $ae(realms)] == 0} then {
9485	set rr $realms
9486    } else {
9487	foreach r $ae(realms) {
9488	    set x($r) 0
9489	}
9490	set rr {}
9491	foreach r $realms {
9492	    if {[info exists x($r)]} then {
9493		lappend rr $r
9494	    }
9495	}
9496    }
9497    return $rr
9498}
9499
9500#
9501# Returns a list of users with associated URLs
9502#
9503# Return : value for %LISTUSERS%
9504#
9505
9506proc pgauth-ac-display-choice {_ae lusers action} {
9507    upvar $_ae ae
9508    global libconf
9509
9510    set lines {}
9511    lappend lines [list "Title" \
9512			    [mc "Login"] \
9513			    [mc "Name"] \
9514			    [mc "Address"] \
9515			    [mc "Mail"] \
9516			    [mc "Realms"] \
9517			]
9518    foreach login $lusers {
9519	if {[pgauth-getuser $ae(dbfd) $login tab]} then {
9520	    set hlogin [::webapp::html-string $login]
9521	    d urlset "" $ae(url) [list [list "action" $action] \
9522					{state morethanone} \
9523					[list "login" $login] \
9524				    ]
9525	    set url [d urlget ""]
9526	    set urllogin [::webapp::helem "a" $hlogin "href" $url]
9527	    set myrealms [pgauth-ac-my-realms ae $tab(realms)]
9528	    lappend lines [list "User" \
9529					$urllogin "$tab(lastname) $tab(firstname)" \
9530					$tab(addr) $tab(mail) $myrealms
9531				    ]
9532	}
9533    }
9534    return [::arrgen::output "html" $libconf(tabuchoice) $lines]
9535}
9536
9537#
9538# Returns a form part to input user information
9539#
9540# Retour : values for %LOGIN%, %PARAMUSER%, %STATE% and %TITLE%
9541#
9542
9543proc pgauth-ac-display-mod {_ae login lastname} {
9544    upvar $_ae ae
9545    global libconf
9546
9547    #
9548    # Get auth data for user, or simulate them if this is a creation
9549    #
9550
9551    set new [string equal $login "_new"]
9552    if {$new} then {
9553	array set u {
9554	    login {}
9555	    lastname {}
9556	    firstname {}
9557	    addr {}
9558	    mail {}
9559	    phone {}
9560	    fax {}
9561	    mobile {}
9562	    realms {}
9563	}
9564	set u(lastname) $lastname
9565	set state "creation"
9566	set title [mc "Creation"]
9567    } else {
9568	if {! [pgauth-getuser $ae(dbfd) $login u]} then {
9569	    d error [mc "Login '%s' does not exist" $login]
9570	}
9571	set state "ok"
9572	set title [mc "Modification"]
9573    }
9574
9575    #
9576    # Realm edition choice
9577    #
9578
9579    set menurealms [pgauth-build-realm-index $ae(dbfd) "list" \
9580				0 $ae(realms) $ae(maxrealms) gidx]
9581
9582    #
9583    # Get existing values, or default values for a new user
9584    #
9585
9586    set valu [uplevel 3 [format $ae(script-getuser) $login]]
9587
9588    #
9589    # Input fields for user
9590    #
9591
9592    set lines {}
9593
9594    foreach c [concat $libconf(editfields) $libconf(editrealms)] {
9595	lassign $c ctitle spec var user
9596	if {$var eq "login" && ! $new} then {
9597	    #
9598	    # Special case for "login" field if editable
9599	    #
9600	    set t [::webapp::html-string $login]
9601	    append t [::webapp::form-hidden "login" $login]
9602	} elseif {$var eq "realms"} then {
9603	    #
9604	    # Special case for realms
9605	    #
9606	    if {[llength $menurealms] == 0} then {
9607		set t ""
9608	    } else {
9609		set lidx {}
9610		foreach r $u(realms) {
9611		    if {[info exists gidx($r)]} then {
9612			lappend lidx $gidx($r)
9613		    }
9614		}
9615		set t [::webapp::form-field $menurealms $var $lidx]
9616	    }
9617	} elseif {$user} then {
9618	    #
9619	    # General case : a field to modify
9620	    #
9621	    if {[lindex $spec 0] eq "yesno"} then {
9622		set spec [list "yesno" [mc [lindex $spec 1]]]
9623	    }
9624	    set t [::webapp::form-field $spec $var $u($var)]
9625	} else {
9626	    #
9627	    # Else, it is only a field for search (eg: phlast/phfirst)
9628	    #
9629	    set t ""
9630	}
9631
9632	if {$t ne ""} then {
9633	    set l [list Normal [mc $ctitle] $t]
9634	    lappend lines $l
9635	}
9636    }
9637
9638    #
9639    # Generate input field specific to the application
9640    #
9641
9642    set n 0
9643    foreach c $ae(specif) v $valu {
9644	lassign $c ctitle spec
9645	incr n
9646	set var "uvar$n"
9647	lappend lines [list "Normal" $ctitle [::webapp::form-field $spec $var $v]]
9648    }
9649
9650    set paramutilisateur [::arrgen::output html $libconf(tabumod) $lines]
9651
9652    #
9653    # Substitution lists
9654    #
9655
9656    lappend lsubst [list %LOGIN%     $login]
9657    lappend lsubst [list %PARAMUSER% $paramutilisateur]
9658    lappend lsubst [list %STATE%     $state]
9659    lappend lsubst [list %TITLE%     $title]
9660
9661    return $lsubst
9662}
9663
9664#
9665# Store user information (new or modification)
9666#
9667# Return : values for %TITLEACTION% and %COMPLEMENT%
9668#
9669
9670proc pgauth-ac-store-mod {_ae _ftab login} {
9671    upvar $_ae ae
9672    upvar $_ftab ftab
9673    global libconf
9674
9675    #
9676    # Check if the script is authorized to modify user
9677    #
9678    set msg [uplevel 3 [format $ae(script-chkuser) $login]]
9679    if {$msg ne ""} then {
9680	d error [mc {Unable to modify '%1$s': %2$s} $login $msg]
9681    }
9682
9683    #
9684    # Extract field values
9685    #
9686
9687    set form [pgauth-build-form-spec "mod" \
9688			[concat $libconf(editfields) $libconf(editrealms)] \
9689			$ae(specif) \
9690		    ]
9691    pgauth-get-data ftab $form
9692
9693    #
9694    # Get existing data from database
9695    #
9696    set u(realms) {}
9697    set new [expr ! [pgauth-getuser $ae(dbfd) $login u]]
9698
9699    d dblock {pgauth.user pgauth.member}
9700
9701    #
9702    # Set user data. Realms will be set after.
9703    #
9704    foreach c $libconf(editfields) {
9705	lassign $c title spec var user
9706	if {$user} then {
9707	    set u($var) [lindex $ftab($var) 0]
9708	}
9709    }
9710
9711    #
9712    # Realm management
9713    #	- if ae(realms) is empty
9714    #		authorize all specific realms in form
9715    #	- if ae(realms) contains only one element
9716    #		do not use form data, and add realm in database
9717    #	- lif ae(realms) contains more than one element
9718    #		use form data, and set all realms present in ae(realms)
9719    #
9720    pgauth-lsrealm $ae(dbfd) tabrlm
9721    switch [llength $ae(realms)] {
9722	0 {
9723	    foreach r $ftab(realms) {
9724		if {! [info exists tabrlm($r)]} then {
9725		    d error [mc "Invalid realm '%s'" $r]
9726		}
9727	    }
9728	    set u(realms) $ftab(realms)
9729	}
9730	1 {
9731	    set found 0
9732	    set er [lindex $ae(realms) 0]
9733	    foreach r $u(realms) {
9734		if {$r eq $er} then {
9735		    set found 1
9736		    break
9737		}
9738	    }
9739	    if {! $found} then {
9740		lappend u(realms) $er
9741	    }
9742	}
9743	default {
9744	    foreach r $ae(realms) {
9745		set ar($r) 1
9746	    }
9747
9748	    # nr = u realms, minus realms from ae(realms)
9749	    set nr {}
9750	    foreach r $u(realms) {
9751		if {! [info exists ar($r)]} then {
9752		    lappend nr $r
9753		}
9754	    }
9755	    set u(realms) $nr
9756
9757	    # add form realms, if they are also in ar()
9758	    foreach r $ftab(realms) {
9759		if {! [info exists tabrlm($r)]} then {
9760		    d error [mc "Invalid realm '%s'" $r]
9761		}
9762		if {[info exists ar($r)]} then {
9763		    lappend u(realms) $r
9764		}
9765	    }
9766	}
9767    }
9768
9769    #
9770    # Store user in database
9771    #
9772    set msg [pgauth-setuser $ae(dbfd) u "no transaction"]
9773    if {$msg ne ""} then {
9774	d dbabort [mc "add %s" $login] $msg
9775    }
9776
9777
9778    #
9779    # Store application specific data
9780    #
9781    set lval {}
9782    set i 1
9783    while {[info exists ftab(uvar$i)]} {
9784	lappend lval $ftab(uvar$i)
9785	incr i
9786    }
9787
9788    set msg [uplevel 3 [format $ae(script-setuser) $login $lval]]
9789    if {$msg ne ""} then {
9790	d dbabort [mc "add %s" $login] $msg
9791    }
9792
9793    #
9794    # C'est fini, on y va !
9795    #
9796    d dbcommit [mc "add %s" $login]
9797
9798    if {$new} then {
9799	set title [mc "Account '%s' insertion" $login]
9800    } else {
9801	set title [mc "Account '%s' modification" $login]
9802    }
9803
9804    set lsubst {}
9805    lappend lsubst [list %TITLEACTION% $title]
9806    lappend lsubst [list %COMPLEMENT% ""]
9807    return $lsubst
9808}
9809
9810#
9811# Display search criterion
9812#
9813# Return : values for %CRITERES% and %MESSAGE%
9814#
9815
9816proc pgauth-ac-display-crit {_ae _ftab msg} {
9817    upvar $_ae ae
9818    upvar $_ftab ftab
9819    global libconf
9820
9821    #
9822    # Realm management
9823    #
9824
9825    set menurealms [pgauth-build-realm-index $ae(dbfd) "menu" 1 $ae(realms) 1 {}]
9826    if {[llength $menurealms] == 0} then {
9827	set menurealms {hidden}
9828    }
9829
9830    #
9831    # Generate input form
9832    #
9833
9834    set lines {}
9835    foreach c [concat $libconf(editfields) $libconf(editrealms)] {
9836	lassign $c title spec var user
9837	if {$var eq "realms"} then {
9838	    set t [::webapp::form-field $menurealms $var ""]
9839	} else {
9840	    if {[lindex $spec 0] eq "yesno"} then {
9841		set spec [list "yesno" [mc [lindex $spec 1]]]
9842	    }
9843	    set t [::webapp::form-field $spec $var ""]
9844	}
9845
9846	set l [list "Normal" [mc $title] $t]
9847	lappend lines $l
9848    }
9849    set crit [::arrgen::output html $libconf(tabumod) $lines]
9850
9851    set lsubst {}
9852    lappend lsubst [list %CRITERES% $crit]
9853    lappend lsubst [list %MESSAGE% $msg]
9854
9855    return $lsubst
9856}
9857
9858#
9859# Exploit search criterion to return a list of users
9860#
9861# Return : list of found logins
9862#
9863
9864proc pgauth-ac-search-crit {_ae _ftab} {
9865    upvar $_ae ae
9866    upvar $_ftab ftab
9867    global libconf
9868
9869    #
9870    # Get parameters
9871    #
9872
9873    set form [pgauth-build-form-spec "crit" \
9874			[concat $libconf(editfields) $libconf(editrealms)] \
9875			{} \
9876		    ]
9877    pgauth-get-data ftab $form
9878
9879    foreach f $form {
9880	set var [lindex $f 0]
9881	set $var [string trim [lindex $ftab($var) 0]]
9882    }
9883
9884    #
9885    # If no clause is specified, return an appropriate message (without
9886    # returning all users, which could be long).
9887    # If we really want all users, one must explicit this by using the
9888    # "*" special character in a criterion.
9889    #
9890
9891    set ncrit 0
9892    foreach var {login lastname firstname mail addr realms} {
9893	if {[set $var] ne ""} then {
9894	    incr ncrit
9895	}
9896    }
9897
9898    set allrealms 1
9899    if {! ($realms eq "_" || $realms eq "")} then {
9900	set allrealms 0
9901	incr ncrit
9902    }
9903
9904    if {$ncrit == 0} then {
9905	d error [mc "You did not specify any criterion"]
9906    }
9907
9908    #
9909    # Use phonetic search
9910    #
9911
9912    if {[regexp {^[01]$} $phren] && $phren} then {
9913	set phlast ""
9914    } else {
9915	set phlast $lastname
9916	set lastname ""
9917    }
9918
9919    if {[regexp {^[01]$} $phrep] && $phrep} then {
9920	set phfirst ""
9921    } else {
9922	set phfirst $firstname
9923	set firstname ""
9924    }
9925
9926    #
9927    # Search with specified criterion
9928    #
9929    # Special case for realms: we search for the specified realm, or
9930    # all realms (those defined, or those found in database) is nothing
9931    # is specified.
9932    #
9933
9934    foreach var {login lastname firstname phlast phfirst mail addr} {
9935	set tabcrit($var) [set $var]
9936    }
9937
9938    if {$allrealms} then {
9939	if {[llength $ae(realms)] > 0} then {
9940	    set tabcrit(realm) $ae(realms)
9941	}
9942    } else {
9943	set lr $ae(realms)
9944	if {[llength $lr] == 0} then {
9945	    pgauth-lsrealm $ae(dbfd) tabrlm
9946	    set lr [array names tabrlm]
9947	}
9948	if {[lsearch -exact $lr $realms] == -1} then {
9949	    d error [mc "Realm '%s' not found" $realms]
9950	}
9951	set tabcrit(realm) $realms
9952    }
9953
9954    return [pgauth-searchuser $ae(dbfd) tabcrit {+lastname +firstname}]
9955}
9956
9957#
9958# Display possible actions for a password change
9959#
9960# Return : values for %LOGIN%, %LASTNAME% and %FIRSTNAME%.
9961#
9962
9963proc pgauth-ac-display-passwd {_ae login} {
9964    upvar $_ae ae
9965
9966    if {! [pgauth-getuser $ae(dbfd) $login u]} then {
9967	d error [mc "Login '%s' does not exist" $login]
9968    }
9969
9970    set login  [::webapp::html-string $login]
9971    set lastname  [::webapp::html-string $u(lastname)]
9972    set firstname [::webapp::html-string $u(firstname)]
9973
9974    set minpwlen [::dnsconfig get "authpgminpwlen"]
9975    set maxpwlen [::dnsconfig get "authpgmaxpwlen"]
9976
9977    set lsubst {}
9978    lappend lsubst [list %LOGIN%     $login]
9979    lappend lsubst [list %LASTNAME%  $lastname]
9980    lappend lsubst [list %FIRSTNAME% $firstname]
9981    lappend lsubst [list %MINPWLEN%  $minpwlen]
9982    lappend lsubst [list %MAXPWLEN%  $maxpwlen]
9983
9984    return $lsubst
9985}
9986
9987#
9988# Store a password
9989#
9990# Return : values for %TITLEACTION% and %COMPLEMENT%
9991#
9992
9993proc pgauth-ac-store-passwd {_ae _ftab login} {
9994    upvar $_ae ae
9995    upvar $_ftab ftab
9996
9997    #
9998    # Check if the script is authorized to modify user
9999    #
10000    set msg [uplevel 3 [format $ae(script-chkuser) $login]]
10001    if {$msg ne ""} then {
10002	d error [mc {Unable to change password of '%1$s': %2$s} $login $msg]
10003    }
10004
10005    #
10006    # Get form values
10007    #
10008    set form {
10009	{pw1	0 1}
10010	{pw2	0 1}
10011	{block	0 1}
10012	{gen	0 1}
10013	{change	0 1}
10014    }
10015
10016    pgauth-get-data ftab $form
10017    ::webapp::import-vars ftab $form
10018
10019    set hlogin [::webapp::html-string $login]
10020
10021    if {$block ne ""} then {
10022	set msg [pgauth-chpw $ae(dbfd) $login {block} "nomail" {}]
10023	set res [mc "Block account '%s'" $hlogin]
10024	set comp ""
10025    } elseif {$gen ne ""} then {
10026	set mail [list "mail" $ae(mailfrom) $ae(mailreplyto) \
10027			    $ae(mailcc) $ae(mailbcc) \
10028			    [encoding convertto iso8859-1 $ae(mailsubject)] \
10029			    [encoding convertto iso8859-1 $ae(mailbody)]]
10030	set msg [pgauth-chpw $ae(dbfd) $login {generate} $mail newpw]
10031	set res [mc {Password generation (%1$s) for %2$s} $newpw $hlogin]
10032	set comp [mc "Password has been sent by mail"]
10033    } elseif {$change ne ""} then {
10034	set pw1 [lindex $ftab(pw1) 0]
10035	set pw2 [lindex $ftab(pw2) 0]
10036	set msg [pgauth-chpw $ae(dbfd) $login [list "change" $pw1 $pw2] "nomail" {}]
10037	set res [mc "Password change for '%s'" $hlogin]
10038	set comp ""
10039    } else {
10040	d error [mc "Invalid input"]
10041    }
10042
10043    if {$msg ne ""} then {
10044	d error $msg
10045    }
10046
10047    #
10048    # Display result
10049    #
10050
10051    set lsubst {}
10052    lappend lsubst [list %TITLEACTION% $res]
10053    lappend lsubst [list %COMPLEMENT% $comp]
10054
10055    return $lsubst
10056}
10057
10058#
10059# Display removal confirmation page
10060#
10061# Return : value for %USER%
10062#
10063
10064proc pgauth-ac-display-del {_ae login} {
10065    upvar $_ae ae
10066
10067    if {! [pgauth-getuser $ae(dbfd) $login u]} then {
10068	return [mc "Login '%s' does not exist" $login]
10069    }
10070
10071    set lsubst {}
10072    lappend lsubst [list %USER%  $login]
10073    lappend lsubst [list %LOGIN% [::webapp::html-string $login]]
10074    return $lsubst
10075}
10076
10077#
10078# Remove user
10079#
10080# Return : values for %TITLEACTION% and %COMPLEMENT%
10081#
10082
10083proc pgauth-ac-del-user {_ae _ftab login} {
10084    upvar $_ae ae
10085    upvar $_ftab ftab
10086
10087    #
10088    # Default messages
10089    #
10090    set msg [mc "Remove '%s' from application" $login]
10091    set comp [mc "Account is still active in authentication subsystem"]
10092
10093    #
10094    # Check if the script is authorized to modify user
10095    #
10096    set msg [uplevel 3 [format $ae(script-chkuser) $login]]
10097    if {$msg ne ""} then {
10098	d error [mc {Unable to modify '%1$s': %2$s} $login $msg]
10099    }
10100
10101    #
10102    # Remove rights on application
10103    #
10104    set msg [uplevel 3 [format $ae(script-deluser) $login]]
10105    if {$msg ne ""} then {
10106	d error $msg
10107    }
10108
10109    #
10110    # Remove from realms
10111    #
10112    if {! [pgauth-getuser $ae(dbfd) $login u]} then {
10113	set comp [mc "Login '%s' does not exist" $login]
10114    } else {
10115	set rmr {}
10116	set nr {}
10117	foreach r $u(realms) {
10118	    if {[lsearch -exact $ae(realms) $r] == -1} then {
10119		# realm is not one of the realms to remove
10120		lappend nr $r
10121	    } else {
10122		# realm to remove
10123		lappend rmr $r
10124	    }
10125	}
10126	if {[llength $nr] != [llength $u(realms)]} then {
10127	    set u(realms) $nr
10128	    set m [pgauth-setuser $ae(dbfd) u]
10129	    if {$m eq ""} then {
10130		set rmr [join $rmr ", "]
10131		set comp [mc "Account has been removed from realms: %s" $rmr]
10132	    } else {
10133		set comp [mc {Error while removing realms %1$s: %2$s} $rmr $m]
10134	    }
10135	}
10136    }
10137
10138    set lsubst {}
10139    lappend lsubst [list %TITLEACTION% [::webapp::html-string $msg]]
10140    lappend lsubst [list %COMPLEMENT% [::webapp::html-string $comp]]
10141    return $lsubst
10142}
10143
10144#
10145# Build a form spec
10146#
10147# Input:
10148#	- modif : "mod" or "crit"
10149#	- spec1 : see variable libconf(editfields)
10150#	- spec2 : see ae(specif) in pgauth-accmanage
10151# Output:
10152#	- list ready for get-data
10153#
10154
10155proc pgauth-build-form-spec {modif spec1 spec2} {
10156    set form {}
10157
10158    foreach c $spec1 {
10159	lassign $c title spec var user
10160	set kw [lindex $spec 0]
10161	if {$modif eq "mod"} then {
10162	    if {$user} then {
10163		switch -- $kw {
10164		    list	{ lappend form [list $var 0 99999] }
10165		    default	{ lappend form [list $var 1 1] }
10166		}
10167	    }
10168	} else {
10169	    switch -- $kw {
10170		list	{ lappend form [list $var 1 1] }
10171		default	{ lappend form [list $var 1 1] }
10172	    }
10173	}
10174    }
10175
10176    set nvar 0
10177    foreach c $spec2 {
10178	incr nvar
10179	set kw [lindex [lindex $c 1] 0]
10180	set var "uvar$nvar"
10181	switch -- $kw {
10182	    list	{ lappend form [list $var 0 99999] }
10183	    default	{ lappend form [list $var 1 1] }
10184	}
10185    }
10186
10187    return $form
10188}
10189
10190#
10191# Build a menu or a listbox with realms
10192#
10193# Input:
10194#	- dbfd : database handle
10195#	- type : list or menu
10196#	- all : true if entry "All" should be displayed
10197#	- rlmlist : list of realms to manage
10198#	- maxrlm : max number of realms to display
10199#	- _gidx : in return, array of indexes
10200# Output :
10201#	- field ready to be displayed by form-field
10202#
10203
10204proc pgauth-build-realm-index {dbfd type all rlmlist maxrlm _gidx} {
10205    upvar $_gidx gidx
10206
10207    pgauth-lsrealm $dbfd tabrlm
10208
10209    set menurealms {}
10210    set i 0
10211    switch [llength $rlmlist] {
10212	0 {
10213	    #
10214	    # Menu with all available realms
10215	    #
10216	    if {$all} then {
10217		lappend menurealms [list "_" [mc "All"]]
10218		incr i
10219	    }
10220	    foreach r [lsort [array names tabrlm]] {
10221		set gidx($r) $i
10222		lappend menurealms [list $r $r]
10223		incr i
10224	    }
10225	}
10226	1 {
10227	    #
10228	    # Don't authorize realm input
10229	    #
10230	}
10231	default {
10232	    #
10233	    # Authorize selected realm input
10234	    #
10235	    if {$all} then {
10236		lappend menurealms [list "_" [mc "All"]]
10237		incr i
10238	    }
10239	    foreach r $rlmlist {
10240		if {[info exists tabrlm($r)]} then {
10241		    set gidx($r) $i
10242		    lappend menurealms [list $r $r]
10243		} else {
10244		    lappend menurealms [list [mc "Invalid realm '%s'"] $r]
10245		}
10246		incr i
10247	    }
10248	}
10249    }
10250
10251    set nrealms [llength $menurealms]
10252    if {$nrealms > 0} then {
10253	if {$maxrlm > 0 && $nrealms > $maxrlm} then {
10254	    set nrealms $maxrlm
10255	}
10256	if {$type eq "list"} then {
10257	    set menurealms [linsert $menurealms 0 "list" "multi" $nrealms]
10258	} else {
10259	    set menurealms [linsert $menurealms 0 "menu"]
10260	}
10261    }
10262
10263    return $menurealms
10264}
10265
10266##############################################################################
10267# Topo library
10268##############################################################################
10269
10270#
10271# Read topo status
10272#
10273# Input:
10274#   - parameters:
10275#	- dbfd : database handle
10276#	- admin : 1 if user is administrator
10277# Output:
10278#   - return value: HTML status message, or empty (if user is not admin
10279#	or if there is no message)
10280#
10281# History
10282#   2010/11/15 : pda      : extract in an autonomous function
10283#   2010/11/23 : pda      : use keepstate table
10284#   2010/12/04 : pda      : i18n
10285#
10286
10287proc topo-status {dbfd admin} {
10288    set msgsta ""
10289    if {$admin} then {
10290	set found 0
10291	set sql "SELECT * FROM topo.keepstate WHERE type = 'anaconf'"
10292	pg_select $dbfd $sql tab {
10293	    set date $tab(date)
10294	    set msg  $tab(message)
10295	    set found 1
10296	}
10297	if {! $found} then {
10298	    set msg [mc "No message from anaconf"]
10299	    set date [mc "(no date)"]
10300	} elseif {$msg eq "Resuming normal operation"} then {
10301	    set msg ""
10302	}
10303
10304	if {$msg ne ""} then {
10305	    set msg [::webapp::html-string $msg]
10306	    regsub -all "\n" $msg "<br>" msg
10307
10308	    set text [::webapp::helem "p" [mc "Topod messages"]]
10309	    append text [::webapp::helem "p" \
10310			    [::webapp::helem "font" $msg "color" "#ff0000"] \
10311			    ]
10312	    append text [::webapp::helem "p" [mc "... since %s" $date]]
10313
10314	    set msgsta [::webapp::helem "div" $text "class" "alerte"]
10315	}
10316    }
10317    return $msgsta
10318}
10319
10320#
10321# Wrapper function to call topo programs on topo host
10322#
10323# Input:
10324#   - cmd: topo program with arguments
10325#   - _msg : in return, text read from program or error message
10326# Output:
10327#   - return value: 1 if ok, 0 if failure
10328#   - parameter _msg: text read or error message
10329#
10330# History
10331#   2010/12/14 : pda/jean : design
10332#   2010/12/19 : pda      : added topouser
10333#   2012/04/24 : pda      : the graph file is local to the www server
10334#
10335
10336proc call-topo {cmd _msg} {
10337    upvar $_msg msg
10338
10339    #
10340    # Quote shell metacharacters to prevent interpretation
10341    #
10342    regsub -all {[<>|;'"${}()&\[\]*?]} $cmd {\\&} cmd
10343
10344    set topobindir [get-local-conf "topobindir"]
10345    set topograph  [get-local-conf "topograph"]
10346    set topohost   [get-local-conf "topohost"]
10347
10348    set cmd "$topobindir/$cmd < $topograph"
10349    set r [catch {exec sh -c $cmd} msg option]
10350    return [expr !$r]
10351}
10352
10353#
10354# Compare two interface names (for sort function)
10355#
10356# Input:
10357#   - parameters:
10358#       - i1, i2 : interface names
10359# Output:
10360#   - return value: -1, 0 or 1 (see string compare)
10361#
10362# History
10363#   2006/12/29 : pda      : design
10364#   2010/12/04 : pda      : i18n
10365#
10366
10367proc compare-interfaces {i1 i2} {
10368    #
10369    # Isolate all words
10370    # Eg: "GigabitEthernet1/0/1" -> " GigabitEthernet 1/0/1"
10371    #
10372    regsub -all {[A-Za-z]+} $i1 { & } i1
10373    regsub -all {[A-Za-z]+} $i2 { & } i2
10374    #
10375    # Remove all special characters
10376    # Eg: " GigabitEthernet 1/0/1" -> " GigabitEthernet 1 0 1"
10377    #
10378    regsub -all {[^A-Za-z0-9]+} $i1 { } i1
10379    regsub -all {[^A-Za-z0-9]+} $i2 { } i2
10380    #
10381    # Remove unneeded spaces
10382    #
10383    set i1 [string trim $i1]
10384    set i2 [string trim $i2]
10385
10386    #
10387    # Compare word by word
10388    #
10389    set r 0
10390    foreach m1 [split $i1] m2 [split $i2] {
10391	if {[regexp {^[0-9]+$} $m1] && [regexp {^[0-9]+$} $m2]} then {
10392	    if {$m1 < $m2} then {
10393		set r -1
10394	    } elseif {$m1 > $m2} then {
10395		set r 1
10396	    } else {
10397		set r 0
10398	    }
10399	} else {
10400	    set r [string compare $m1 $m2]
10401	}
10402	if {$r != 0} then {
10403	    break
10404	}
10405    }
10406
10407    return $r
10408}
10409
10410#
10411# Compare two IP addresses, used in sort operations.
10412#
10413# Input:
10414#   - parameters:
10415#       - ip1, ip2 : IP addresses (IPv4 or IPv6)
10416# Output:
10417#   - return value: -1, 0 ou 1 (see string compare)
10418#
10419# History
10420#   2006/06/20 : pda      : design
10421#   2006/06/22 : pda      : documentation
10422#   2010/12/04 : pda      : i18n
10423#
10424
10425proc compare-ip {ip1 ip2} {
10426    set ip1 [::ip::normalize $ip1]
10427    set v1  [::ip::version $ip1]
10428    set ip2 [::ip::normalize $ip2]
10429    set v2  [::ip::version $ip2]
10430
10431    set r 0
10432    if {$v1 == 4 && $v2 == 4} then {
10433	set l1 [split [::ip::prefix $ip1] "."]
10434	set l2 [split [::ip::prefix $ip2] "."]
10435	foreach e1 $l1 e2 $l2 {
10436	    if {$e1 < $e2} then {
10437		set r -1
10438		break
10439	    } elseif {$e1 > $e2} then {
10440		set r 1
10441		break
10442	    }
10443	}
10444    } elseif {$v1 == 6 && $v2 == 6} then {
10445	set l1 [split [::ip::prefix $ip1] ":"]
10446	set l2 [split [::ip::prefix $ip2] ":"]
10447	foreach e1 $l1 e2 $l2 {
10448	    if {"0x$e1" < "0x$e2"} then {
10449		set r -1
10450		break
10451	    } elseif {"0x$e1" > "0x$e2"} then {
10452		set r 1
10453		break
10454	    }
10455	}
10456    } else {
10457	set r [expr $v1 < $v2]
10458    }
10459    return $r
10460}
10461
10462#
10463# Check if an IP address (IPv4 or IPv6) is in an address range
10464#
10465# Input:
10466#   - parameters:
10467#       - ip : IP address (or CIDR) to check
10468#	- net : address range
10469# Output:
10470#   - return value: 0 (ip not in range) or 1 (ip is in range)
10471#
10472# History
10473#   2006/06/22 : pda      : design
10474#   2010/12/04 : pda      : i18n
10475#
10476
10477proc ip-in {ip net} {
10478    set v [::ip::version $net]
10479    if {[::ip::version $ip] != $v} then {
10480	return 0
10481    }
10482
10483    set defmask [expr "$v==4 ? 32 : 128"]
10484
10485    set ip [::ip::normalize $ip]
10486    set net [::ip::normalize $net]
10487
10488    set mask [::ip::mask $net]
10489    if {$mask eq ""} then {
10490	set mask $defmask
10491    }
10492
10493    set prefnet [::ip::prefix $net]
10494    regsub {(/[0-9]+)?$} $ip "/$mask" ip2
10495    set prefip  [::ip::prefix $ip2]
10496
10497    return [string equal $prefip $prefnet]
10498}
10499
10500#
10501# Check metrology id against user permissions
10502#
10503# Input:
10504#   - parameters:
10505#	- dbfd : database handle
10506#	- id : id du point de collecte (ou id+id+...)
10507#	- _tabuid : user characteristics
10508#	- _title : title of graph
10509# Output:
10510#   - return value: empty string or error message
10511#   - parameter _title : title of graph found
10512#
10513# History
10514#   2006/08/09 : pda/boggia : design
10515#   2006/12/29 : pda        : parameter vlan
10516#   2008/07/30 : pda        : adapt to new extractcoll
10517#   2008/07/30 : pda        : multiple ids
10518#   2008/07/31 : pda        : add "|"
10519#   2010/12/04 : pda        : i18n
10520#
10521
10522proc check-metro-id {dbfd id _tabuid _title} {
10523    upvar $_tabuid tabuid
10524    upvar $_title title
10525    global libconf
10526
10527    #
10528    # If ids are more than one
10529    #
10530
10531    set lid [split $id "+|"]
10532
10533    #
10534    # Get the metrology sensor list, according to user permissions
10535    #
10536
10537    set cmd [format $libconf(extractcoll) $tabuid(flagsr)]
10538    if {! [call-topo $cmd msg]} then {
10539	return [mc "Cannot read sensor list: %s" $msg]
10540    }
10541    foreach line [split $msg "\n"] {
10542	lassign [split $line] kw i
10543	set n [lsearch -exact $lid $i]
10544	if {$n >= 0} then {
10545	    set idtab($i) $line
10546	    if {[info exists firstkw]} then {
10547		if {$firstkw ne $kw} then {
10548		    return [mc "Divergent sensor types"]
10549		}
10550	    } else {
10551		set firstkw $kw
10552	    }
10553	    set lid [lreplace $lid $n $n]
10554	}
10555    }
10556
10557    #
10558    # Error if id is not found
10559    #
10560
10561    if {[llength $lid] > 0} then {
10562	return [mc "Sensor '%s' not found" $id]
10563    }
10564
10565    #
10566    # Try to guess an appropriate title
10567    #
10568
10569    set lid [array names idtab]
10570    switch [llength $lid] {
10571	0 {
10572	    return [mc "No sensor selected"]
10573	}
10574	1 {
10575	    set i [lindex $lid 0]
10576	    set l $idtab($i)
10577	    switch $firstkw {
10578		trafic {
10579		    set eq    [lindex $l 2]
10580		    set iface [lindex $l 4]
10581		    set vlan  [lindex $l 5]
10582
10583		    if {$vlan ne "-"} then {
10584			set t [mc {Traffic on vlan %1$s of interface %2$s of %3$s}]
10585		    } else {
10586			set t [mc {Traffic on interface %2$s of %3$s}]
10587		    }
10588		    set title [format $t $vlan $iface $eq]
10589		}
10590		nbauthwifi -
10591		nbassocwifi {
10592		    set eq    [lindex $l 2]
10593		    set iface [lindex $l 4]
10594		    set ssid  [lindex $l 5]
10595
10596		    if {$firstkw eq "nbauthwifi"} then {
10597			set t [mc {Number of auhentified users on ssid %1$s of interface %2$s of %3$s}]
10598		    } else {
10599			set t [mc {Number of associated hosts on ssid %1$s of interface %2$s of %3$s}]
10600		    }
10601		    set title [format $t $ssid $iface $eq]
10602		}
10603		default {
10604		    return [mc "Internal error: invalid extractcoll output format"]
10605		}
10606	    }
10607	}
10608	default {
10609	    switch $firstkw {
10610		trafic {
10611		    set le {}
10612		    foreach i $lid {
10613			set l $idtab($i)
10614			set eq    [lindex $l 2]
10615			set iface [lindex $l 4]
10616			set vlan  [lindex $l 5]
10617
10618			set e "$eq/$iface"
10619			if {$vlan ne "-"} then {
10620			    append e ".$vlan"
10621			}
10622			lappend le $e
10623		    }
10624		    set le [join $le ", "]
10625		    set title [mc "Traffic on interfaces %s" $le]
10626		}
10627		nbauthwifi -
10628		nbassocwifi {
10629		    if {$firstkw eq "nbauthwifi"} then {
10630			set t [mc "Number of auhentified users on %s"]
10631		    } else {
10632			set t [mc "Number of associated hosts on %s"]
10633		    }
10634		    foreach i $lid {
10635			set l $idtab($i)
10636			set eq    [lindex $l 2]
10637			set iface [lindex $l 4]
10638			set ssid  [lindex $l 5]
10639
10640			set e "$eq/$iface ($ssid)"
10641			lappend le $e
10642		    }
10643		    set le [join $le ", "]
10644		    set title [format $t $le]
10645		}
10646		default {
10647		    return [mc "Internal error: invalid extractcoll output format"]
10648		}
10649	    }
10650	}
10651    }
10652
10653    return ""
10654}
10655
10656#
10657# Get regexp giving authorized equipments for a given group.
10658#
10659# Input:
10660#   - parameters:
10661#       - dbfd : database handle
10662#	- rw : read (0) or write (1)
10663#	- idgrp : group id
10664# Output:
10665#   - return value: {{re_allow_1 ... re_allow_n} {re_deny_1 ... re_deny_n}}
10666#
10667# History
10668#   2006/08/10 : pda/boggia : design with an on-disk file
10669#   2010/11/03 : pda/jean   : data are now in the database
10670#   2010/12/05 : pda        : i18n
10671#
10672
10673proc read-authorized-eq {dbfd rw idgrp} {
10674    set r {}
10675    foreach allow_deny {1 0} {
10676	set sql "SELECT pattern
10677			FROM topo.p_eq
10678			WHERE idgrp = $idgrp
10679			    AND rw = $rw
10680			    AND allow_deny = $allow_deny"
10681	set d {}
10682	pg_select $dbfd $sql tab {
10683	    lappend d $tab(pattern)
10684	}
10685	lappend r $d
10686    }
10687    return $r
10688}
10689
10690#
10691# Fetch a graph from the metrology host and return it back.
10692#
10693# Input:
10694#   - parameters:
10695#       - url : URL of the graph on the metrology host
10696# Output:
10697#   - none : the fetched graph is printed on stdout with usual HTTP headers
10698#
10699# History
10700#   2006/05/17 : jean       : design for dhcplog
10701#   2006/08/09 : pda/boggia : extract and use in this library
10702#   2010/11/15 : pda        : remove err parameter
10703#   2010/12/05 : pda        : i18n
10704#
10705
10706proc gengraph {url} {
10707    package require http			;# tcllib
10708
10709    set token [::http::geturl $url]
10710    set status [::http::status $token]
10711
10712    if {$status ne "ok"} then {
10713	set code [::http::code $token]
10714	d error [mc "No access: %s" $code]
10715    }
10716
10717    upvar #0 $token state
10718
10719    #
10720    # Determine image type
10721    #
10722
10723    array set meta $state(meta)
10724    switch -exact $meta(Content-Type) {
10725	image/png {
10726	    set contenttype "png"
10727	}
10728	image/jpeg {
10729	    set contenttype "jpeg"
10730	}
10731	image/gif {
10732	    set contenttype "gif"
10733	}
10734	default {
10735	    set contenttype "html"
10736	}
10737    }
10738
10739    #
10740    # Return the result back
10741    #
10742
10743    ::webapp::send $contenttype $state(body)
10744}
10745
10746#
10747# Decode a date (supposed to be input by a human)
10748#
10749# Input:
10750#   - parameters:
10751#       - date : date imput by an user in a form
10752#	- hour : hour (from 00:00:00 to 23:59:59)
10753# Output:
10754#   - return value: converted date in potsgresql format, or "" if no date
10755#
10756# History
10757#   2000/07/18 : pda      : design
10758#   2000/07/23 : pda      : add hour
10759#   2001/03/12 : pda      : extract in this library
10760#   2008/07/30 : pda      : add special case for 24h (= 23:59:59)
10761#   2010/12/05 : pda      : i18n
10762#
10763
10764proc decode-date {date hour} {
10765    set date [string trim $date]
10766    if {$date eq ""} then {
10767	set datepg ""
10768    }
10769    if {$hour eq "24"} then {
10770	set hour "23:59:59"
10771    }
10772    set l [split $date "/"]
10773    lassign $l dd mm yyyy
10774    switch [llength $l] {
10775	1	{
10776	    set mm   [clock format [clock seconds] -format "%m"]
10777	    set yyyy [clock format [clock seconds] -format "%Y"]
10778	    set datepg "$mm/$dd/$yyyy $hour"
10779	}
10780	2	{
10781	    set yyyy [clock format [clock seconds] -format "%Y"]
10782	    set datepg "$mm/$dd/$yyyy $hour"
10783	}
10784	3	{
10785	    set datepg "$mm/$dd/$yyyy $hour"
10786	}
10787	default	{
10788	    set datepg ""
10789	}
10790    }
10791
10792    if {$datepg ne ""} then {
10793	if {[catch {clock scan $datepg}]} then {
10794	    set datepg ""
10795	}
10796    }
10797    return $datepg
10798}
10799
10800#
10801# Convert a 802.11b/g radio frequency (2.4 GHz band) into a channel
10802#
10803# Input:
10804#   - parameters:
10805#       - freq : frequency
10806#   - global variable libconf(freq:<frequency>) : conversion table
10807# Output:
10808#   - return value: channel
10809#
10810# History
10811#   2008/07/30 : pda      : design
10812#   2008/10/17 : pda      : channel "dfs"
10813#   2010/12/05 : pda      : i18n
10814#
10815
10816proc conv-channel {freq} {
10817    global libconf
10818
10819    switch -- $freq {
10820	dfs {
10821	    set channel "auto"
10822	}
10823	default {
10824	    if {[info exists libconf(freq:$freq)]} then {
10825		set channel $libconf(freq:$freq)
10826	    } else {
10827		set channel "$freq MHz"
10828	    }
10829	}
10830    }
10831    return $channel
10832}
10833
10834#
10835# Read list of interfaces on an equipment
10836#
10837# Input:
10838#   - parameters:
10839#	- eq : equipment name
10840#	- _tabuid : user's characteristics (including graph flags)
10841#   - global variables :
10842#	- libconf(extracteq) : call to extracteq
10843# Output:
10844#   - return value: {eq type model location iflist liferr arrayif arrayvlan}
10845#	where
10846#	- iflist is the sorted list of interfaces
10847#	- liferr is the list of interfaces which are are writable but not
10848#		readable (e.g. this is an error)
10849#	- arrayif (ready for "array set") gives an array indexed by
10850#		interface name:
10851#		tab(iface) {name edit radio stat mode desc link native {vlan...}}
10852#		(see extracteq output format)
10853#	- arrayvlan (ready for "array set") gives an array indexed by vlanid:
10854#		tab(id) {desc-in-hex voip-0-or-1}
10855#
10856# History
10857#   2010/11/03 : pda      : design
10858#   2010/11/15 : pda      : remove parameter err
10859#   2010/11/23 : pda/jean : get writable interfaces
10860#   2010/11/25 : pda      : add manual
10861#   2010/12/05 : pda      : i18n
10862#
10863
10864proc eq-iflist {eq _tabuid} {
10865    global libconf
10866    upvar $_tabuid tabuid
10867
10868    #
10869    # First call to extracteq : get the list of "readable" interfaces
10870    #
10871
10872    set found 0
10873
10874    set cmd [format $libconf(extracteq) $tabuid(flagsr) $eq]
10875    if {! [call-topo $cmd msg]} then {
10876	d error [mc {Error during extraction of readable interfaces from '%1$s': %2$s} $eq $msg]
10877    }
10878    foreach line [split $msg "\n"] {
10879	switch [lindex $line 0] {
10880	    eq {
10881		set r [lreplace $line 0 0]
10882
10883		set location [lindex $r 3]
10884		if {$location eq "-"} then {
10885		    set location ""
10886		} else {
10887		    set location [binary format H* $location]
10888		}
10889		set r [lreplace $r 3 3 $location]
10890
10891		# manual = "manual" or "auto"
10892		set manual [lindex $r 4]
10893		set r [lreplace $r 4 4]
10894
10895		set found 1
10896	    }
10897	    iface {
10898		set if [lindex $line 1]
10899		# prepare "edit" item, which may be set in the second
10900		# call to extracteq
10901		set line [linsert $line 2 "-"]
10902		set tabiface($if) [lreplace $line 0 0]
10903	    }
10904	}
10905    }
10906
10907    if {! $found} then {
10908	d error [mc "Equipment '%s' not found" $eq]
10909    }
10910
10911    #
10912    # Second call to exctracteq : get the list of "writable" interfaces
10913    #
10914
10915    set liferr {}
10916
10917    if {$manual eq "auto"} then {
10918	set cmd [format $libconf(extracteq) $tabuid(flagsw) $eq]
10919	if {! [call-topo $cmd msg]} then {
10920	    d error [mc {Error during extraction of writable interfaces from '%1$s': %2$s} $eq $msg]
10921	}
10922	foreach line [split $msg "\n"] {
10923	    switch [lindex $line 0] {
10924		iface {
10925		    set if [lindex $line 1]
10926		    if {! [info exists tabiface($if)]} then {
10927			# add this interface to the list of error interfaces
10928			lappend liferr $if
10929		    } else {
10930			# set the "edit" attribute on this interface
10931			set tabiface($if) [lreplace $tabiface($if) 1 1 "edit"]
10932		    }
10933		}
10934		vlan {
10935		    lassign $line bidon id desc voip
10936		    set tabvlan($id) [list $desc $voip]
10937		}
10938	    }
10939	}
10940	set liferr [lsort -command compare-interfaces $liferr]
10941    }
10942
10943    lappend r $liferr
10944
10945    #
10946    # Sort interfaces
10947    #
10948
10949    set iflist [lsort -command compare-interfaces [array names tabiface]]
10950
10951    #
10952    # Return value
10953    #
10954
10955    lappend r $iflist
10956    lappend r [array get tabiface]
10957    lappend r [array get tabvlan]
10958
10959    return $r
10960}
10961
10962#
10963# Get graph and equipment status
10964#
10965# Input:
10966#   - parameters:
10967#	- dbfd : database handle
10968#	- eq : equipment name
10969#	- iface (optional) : interface name
10970# Output:
10971#   - return value: HTML text giving graph and equipment status
10972#
10973# History:
10974#   2010/11/29 : pda/jean : design
10975#   2010/12/05 : pda      : i18n
10976#
10977
10978proc eq-graph-status {dbfd eq {iface {}}} {
10979    global libconf
10980
10981    #
10982    # Search for unprocessed modifications and build information.
10983    #
10984
10985    set wif ""
10986    if {$iface ne ""} then {
10987	set qiface [::pgsql::quote $iface]
10988	set wif "AND iface = '$qiface'"
10989    }
10990
10991    set qeq [::pgsql::quote $eq]
10992
10993    set sql "SELECT * FROM topo.ifchanges
10994			WHERE eq = '$qeq' AND processed = 0 $wif
10995			ORDER BY reqdate DESC"
10996    set lines {}
10997    lappend lines [list Title4 [mc "Date"] [mc "Login"] [mc "Interface"] [mc "Change"]]
10998    pg_select $dbfd $sql tab {
10999	set ifdesc $tab(ifdesc)
11000	set ethervlan $tab(ethervlan)
11001	set voicevlan $tab(voicevlan)
11002	set chg [mc "description='%s'" $ifdesc]
11003	if {$ethervlan == -1} then {
11004	    append chg ", "
11005	    append chg [mc "deactivated interface"]
11006	} else {
11007	    append chg ", "
11008	    append chg [mc "vlan=%s" $ethervlan]
11009	    if {$voicevlan != -1} then {
11010		append chg ", "
11011		append chg [mc "voip=%s" $voicevlan]
11012	    }
11013	}
11014	lappend lines [list Normal4 $tab(reqdate) $tab(login) $tab(iface) $chg]
11015    }
11016    if {[llength $lines] == 1} then {
11017	set ifchg ""
11018    } else {
11019	set ifchg [::webapp::helem "p" [mc "Changes currently processed:"]]
11020	append ifchg [::arrgen::output "html" $libconf(tabeqstatus) $lines]
11021    }
11022
11023    #
11024    # Search for current topod status
11025    #
11026
11027    set sql "SELECT message FROM topo.keepstate WHERE type = 'status'"
11028    set action ""
11029    pg_select $dbfd $sql tab {
11030	catch {lassign [lindex $tab(message) 0] date action}
11031    }
11032
11033    switch -nocase -glob $action {
11034	rancid* -
11035	building* {
11036	    set graph [::webapp::helem "p" [mc "Graph currenty re-builded. Informations presented here are not necessarily consistent with current equipement configuration."]]
11037	}
11038	default {
11039	    set graph ""
11040	}
11041    }
11042
11043    #
11044    # Present information from $ifchg and $graph
11045    #
11046
11047    if {$ifchg eq "" && $graph eq ""} then {
11048	set html ""
11049    } else {
11050	set html "$graph\n$ifchg"
11051	set html [::webapp::helem "font" $html "color" "#ff0000"]
11052	set html "<hr>$html<hr>"
11053    }
11054
11055    return $html
11056}
11057
11058
11059#
11060# Check if a VLAN name is valid
11061#
11062# Input:
11063#   - parameters:
11064#       - name : VLAN name
11065#       - _msg : error message
11066#   - global variable libconf(vlan-chars) : authorized characters
11067# Output:
11068#   - return value: 1 if name is valid, 0 otherwise
11069#   - msg: error message
11070#
11071# History
11072#   2014/02/18 : jean      : converted to function from "list-vlans"
11073#
11074
11075proc check-vlan-name {name _msg} {
11076    global libconf
11077    upvar $_msg msg
11078
11079
11080    if {[regexp "^\[$libconf(vlan-chars)\]+$" $name]} then {
11081	set ok 1
11082	set msg ""
11083    } else {
11084	set ok 0
11085	set msg "invalid characters in vlan name '$name' (not in $libconf(vlan-chars))"
11086    }
11087
11088    return $ok
11089}
11090
11091
11092##############################################################################
11093# Topo*d subsystem
11094##############################################################################
11095
11096#
11097# Set function tracing
11098#
11099# Input:
11100#   - lfunct : list of function names
11101# Output: none
11102#
11103# History
11104#   2010/10/20 : pda/jean : minimal design
11105#   2010/12/15 : pda/jean : splitted in library
11106#
11107
11108proc set-trace {lfunct} {
11109    foreach c $lfunct {
11110	trace add execution $c enter report-enter
11111	trace add execution $c leave report-leave
11112    }
11113}
11114
11115proc report-enter {cmd enter} {
11116    puts "> $cmd"
11117}
11118
11119proc report-leave {cmd code result leave} {
11120    puts "< $cmd -> $code/$result"
11121}
11122
11123#
11124# Run a program as a daemon
11125#
11126# Input:
11127#   - argv0  : path to the script
11128#   - argstr : argument string
11129# Output: none
11130#
11131# History
11132#   2012/03/27 : pda/jean : design
11133#
11134
11135proc run-as-daemon {argv0 argstr} {
11136    exec sh -c "exec $argv0 $argstr" &
11137    exit 0
11138}
11139
11140##############################################################################
11141# Utility functions
11142##############################################################################
11143
11144#
11145# Initialize system logger
11146#
11147# Input:
11148#   - logger : shell command line to log messages
11149# Output: none
11150#
11151# History
11152#   2010/12/15 : pda/jean : minimal design
11153#
11154
11155set ctxt(logger) ""
11156
11157proc set-log {logger} {
11158    global ctxt
11159
11160    set ctxt(logger) $logger
11161}
11162
11163#
11164# Add a message to the log
11165#
11166# Input:
11167#   - msg : error/warning message
11168# Output: none
11169#
11170# History
11171#   2010/10/20 : pda/jean : minimal design
11172#
11173
11174proc log-error {msg} {
11175    global ctxt
11176
11177    if {[catch {open "|$ctxt(logger)" "w"} fd]} then {
11178	puts stderr "$msg (log to syslog: $fd)"
11179    } else {
11180	puts $fd $msg
11181	close $fd
11182    }
11183}
11184
11185#
11186# Set verbosity level
11187#
11188# Input:
11189#   - level : threshold (verbosity level) of messages to display
11190# Output:
11191#   - return value: none
11192#   - ctxt(verbose) : verbose threshold
11193#
11194# History
11195#   2010/10/21 : pda/jean : design
11196#
11197
11198proc topo-set-verbose {level} {
11199    global ctxt
11200
11201    set ctxt(verbose) $level
11202}
11203
11204#
11205# Display debug message according to verbosity level
11206#
11207# Input:
11208#   - msg : message
11209#   - level : verbosity level
11210# Output: none
11211#
11212# History
11213#   2010/10/21 : pda/jean : design
11214#
11215
11216proc topo-verbositer {msg level} {
11217    global ctxt
11218
11219    if {$level <= $ctxt(verbose)} then {
11220	puts stderr $msg
11221    }
11222}
11223
11224##############################################################################
11225# Status management
11226##############################################################################
11227
11228#
11229# Update status
11230# Status keeps last topo*d operations.
11231#
11232# Input:
11233#   - status : current operation
11234# Output: none
11235#
11236# Note: status is in topo.keepstate table, topo.message is a list
11237# {{date1 msg1} {date2 msg2} ...} where 1 is the most recent entry.
11238# We keep only last N entries.
11239#
11240# History
11241#   2010/11/05 : pda/jean : design
11242#
11243
11244proc reset-status {} {
11245    set sql "DELETE FROM topo.keepstate WHERE type = 'status'"
11246    toposqlexec $sql 2
11247}
11248
11249proc set-status {status} {
11250    global ctxt
11251
11252    set cur {}
11253    set sql "SELECT message FROM topo.keepstate WHERE type = 'status'"
11254    if {! [toposqlselect $sql tab { set cur $tab(message) } 2]} then {
11255	return
11256    }
11257
11258    # insert new entry before all others
11259    set date [clock format [clock seconds]]
11260    set cur [linsert $cur 0 [list $date $status]]
11261
11262    # remove oldest entries at the end
11263    if {[llength $cur] > $ctxt(maxstatus)} then {
11264	set cur [lreplace $cur $ctxt(maxstatus) end]
11265    }
11266
11267    set qcur [::pgsql::quote $cur]
11268
11269    set sql "DELETE FROM topo.keepstate WHERE type = 'status' ;
11270		INSERT INTO topo.keepstate (type, message)
11271			VALUES ('status', '$qcur')"
11272    toposqlexec $sql 2
11273}
11274
11275##############################################################################
11276# Topo*d database handling
11277##############################################################################
11278
11279#
11280# Connect to database if needed
11281#
11282# Input:
11283#   - chan : database channel
11284#   - ctxt(dbfd1), ctxt(dbfd2) : database handles for each channel
11285# Output:
11286#   - ctxt(dbfd<n>) : database handle updated
11287#
11288# History
11289#   2010/10/20 : pda/jean : documentation
11290#
11291
11292proc lazy-connect {{chan 1}} {
11293    global ctxt
11294
11295    set r 1
11296    if {[string equal $ctxt(dbfd$chan) ""]} then {
11297	set conninfo [get-conninfo "dnsdb"]
11298	set d [catch {set ctxt(dbfd$chan) [pg_connect -conninfo $conninfo]} msg]
11299	if {$d} then {
11300	    set r 0
11301	} else {
11302	    ::dnsconfig setdb $ctxt(dbfd$chan)
11303	    log-error "Connexion to database succeeded"
11304	}
11305    }
11306    return $r
11307}
11308
11309#
11310# Execute a SQL request to get data (as with pg_select), and manage
11311# database reconnect
11312#
11313# Input:
11314#   - sql : SQL request
11315#   - arrayname : array used in the script
11316#   - script : procedure ou script
11317#   - chan : optionnal channel (1 or 2)
11318# Output:
11319#   - return value: 1 if ok, 0 if error
11320#
11321# History
11322#   2010/10/20 : pda/jean : design (woaw !)
11323#
11324
11325proc toposqlselect {sql arrayname script {chan 1}} {
11326    global ctxt
11327
11328    if {[lazy-connect $chan]} {
11329	set cmd [list pg_select $ctxt(dbfd$chan) $sql $arrayname $script]
11330	if {[catch {uplevel 1 $cmd} err]} then {
11331	    log-error "Connexion to database lost in toposqlselect ($err)"
11332	    catch {pg_disconnect $ctxt(dbfd$chan)}
11333	    set ctxt(dbfd$chan) ""
11334	    set r 0
11335	} else {
11336	    set r 1
11337	}
11338    } else {
11339	set r 0
11340    }
11341    return $r
11342}
11343
11344#
11345# Execute a SQL request to modify data (INSERT, UPDATE or DELETE, as
11346# with pg_exec) and manage database reconnect
11347#
11348# Input:
11349#   - sql : SQL request
11350#   - chan : optionnal channel (1 or 2)
11351# Output:
11352#   - return value: 1 if ok, 0 if error
11353#
11354# History
11355#   2010/10/20 : pda/jean : design
11356#
11357
11358proc toposqlexec {sql {chan 1}} {
11359    global ctxt
11360
11361    if {[lazy-connect]} {
11362	if {[catch {pg_exec $ctxt(dbfd$chan) $sql} res]} then {
11363	    log-error "Connection to database lost in toposqlexec ($res)"
11364	    catch {pg_disconnect $ctxt(dbfd$chan)}
11365	    set ctxt(dbfd$chan) ""
11366	    set r 0
11367	} else {
11368	    switch -- [pg_result $res -status] {
11369		PGRES_COMMAND_OK -
11370		PGRES_TUPLES_OK -
11371		PGRES_EMPTY_QUERY {
11372		    set r 1
11373		    pg_result $res -clear
11374		}
11375		default {
11376		    set err [pg_result $res -error]
11377		    pg_result $res -clear
11378		    log-error "Internal error in toposqlexec. Connexion to database lost ($err)"
11379		    catch {pg_disconnect $ctxt(dbfd$chan)}
11380		    set ctxt(dbfd$chan) ""
11381		    set r 0
11382		}
11383	    }
11384	}
11385    } else {
11386	set r 0
11387    }
11388    return $r
11389}
11390
11391#
11392# Start a SQL transaction and manage database reconnect
11393#
11394# Input:
11395#   - chan : optionnal channel (1 or 2)
11396# Output:
11397#   - return value: 1 if ok, 0 if error
11398#
11399# History
11400#   2010/10/21 : pda/jean : design
11401#
11402
11403proc toposqllock {{chan 1}} {
11404    return [toposqlexec "START TRANSACTION" $chan]
11405}
11406
11407#
11408# End a SQL transaction and manage database reconnect
11409#
11410# Input:
11411#   - commit : "commit" or "abort"
11412# Output:
11413#   - return value: 1 if ok, 0 if error
11414#
11415# History
11416#   2010/10/21 : pda/jean : design
11417#
11418
11419proc toposqlunlock {commit {chan 1}} {
11420    switch $commit {
11421	commit { set sql "COMMIT WORK" }
11422	abort  { set sql "ABORT WORK" }
11423    }
11424    return [toposqlexec $sql $chan]
11425}
11426
11427
11428##############################################################################
11429# Topo*d mail management
11430##############################################################################
11431
11432#
11433# Send a mail if event message changes
11434#
11435# Input:
11436#   - ev : event ("rancid", "anaconf", etc.)
11437#   - msg : event message
11438# Output:
11439#   - none
11440#
11441# History
11442#   2010/10/21 : pda/jean : design
11443#
11444
11445proc keep-state-mail {ev msg} {
11446    #
11447    # Get previous message
11448    #
11449
11450    set oldmsg ""
11451    set qev [::pgsql::quote $ev]
11452    set sql "SELECT message FROM topo.keepstate WHERE type = '$qev'"
11453    if {! [toposqlselect $sql tab { set oldmsg $tab(message) } 2]} then {
11454	# we don't know what to do...
11455	return
11456    }
11457
11458    if {$msg ne $oldmsg} then {
11459	#
11460	# New message is different from previous one. We must
11461	# send it by mail and store it in keepstate table.
11462	#
11463	# Design choice: if database access is out of order, we
11464	# can't access keepstate. The choice is to not send mail.
11465	# The risk is we won't known new messages, but the advantage
11466	# is that our mailboxes will not be polluted by a new
11467	# identical mail every X seconds. On the other hand, risk
11468	# is minimized by the fact that no new change will be detected
11469	# and/or processed while database is out of order.
11470	#
11471
11472	set qmsg [::pgsql::quote $msg]
11473	set sql "DELETE FROM topo.keepstate WHERE type = '$qev' ;
11474		    INSERT INTO topo.keepstate (type, message)
11475			    VALUES ('$qev', '$qmsg')"
11476	if {[toposqlexec $sql 2]} then {
11477	    #
11478	    # Database access is ok. Send the mail.
11479	    #
11480
11481	    set from    [::dnsconfig get "topofrom"]
11482	    set to	[::dnsconfig get "topoto"]
11483	    set replyto	""
11484	    set cc	""
11485	    set bcc	""
11486	    set subject	"\[auto\] topod status changed for $ev"
11487	    ::webapp::mail $from $replyto $to $cc $bcc $subject $msg
11488	}
11489    }
11490}
11491
11492##############################################################################
11493# Equipment types
11494##############################################################################
11495
11496#
11497# Read type and model for all equipments in the graph.
11498#
11499# Input:
11500#   - _tabeq : name of array containing, in return, types and models
11501# Output:
11502#   - return value: empty string or error message
11503#   - tabeq : array, indexed by FQDN of equipement, containing:
11504#	tabeq(<eq>) {<type> <model>}
11505#
11506# History
11507#   2010/02/25 : pda/jean : design
11508#   2010/10/21 : pda/jean : manage only fully qualified host names
11509#
11510
11511set libconf(dumpgraph-read-eq-type) "dumpgraph -a -o eq"
11512
11513proc read-eq-type {_tabeq} {
11514    global libconf
11515    upvar $_tabeq tabeq
11516
11517    set-status "Reading equipement types"
11518
11519    set defdom [dnsconfig get "defdomain"]
11520
11521    set cmd $libconf(dumpgraph-read-eq-type)
11522
11523    if {[call-topo $cmd msg]} then {
11524	foreach line [split $msg "\n"] {
11525	    switch [lindex $line 0] {
11526		eq {
11527		    array set t $line
11528		    set eq $t(eq)
11529		    set type $t(type)
11530		    set model $t(model)
11531
11532		    append eq ".$defdom"
11533
11534		    set tabeq($eq) [list $type $model]
11535
11536		    array unset t
11537		}
11538	    }
11539	}
11540	set msg ""
11541    }
11542
11543    return $msg
11544}
11545
11546##############################################################################
11547# Detection of modifications in files
11548##############################################################################
11549
11550#
11551# Detect modifications in a directory
11552#
11553# Input:
11554#   - dir : directory path
11555#   - _err : in return, empty string or error message
11556# Output:
11557#   - return value : list {{<code> <file> <date>} {<code> <file> <date>}...}
11558#	where <code> = "add", "del", "mod" or "err"
11559#	and <date> = date in clock_t format
11560#	if <code> = "err", error message is in "<date>"
11561#   - parameter err : in return, all error messages
11562#
11563# History
11564#   2010/11/12 : pda/jean : design
11565#
11566
11567proc detect-dirmod {dir _err} {
11568    upvar $_err err
11569
11570    set err ""
11571
11572    #
11573    # First pass: get all files in directory and keep them in an array:
11574    #	ntab(<file>) <date>
11575    #
11576    foreach file [glob -nocomplain "$dir/*.eq"] {
11577	if {[catch {file mtime $file} date]} then {
11578	    append err "$date\n"
11579	} else {
11580	    set ntab($file) $date
11581	}
11582    }
11583
11584    #
11585    # Second pass: get all files in database for this directory and
11586    # keep them in an array:
11587    #	otab(<file>) <date>
11588    #
11589    set sql "SELECT path, date FROM topo.filemonitor
11590				WHERE path ~ '^$dir/\[^/\]+$'"
11591    if {! [toposqlselect $sql tab { set otab($tab(path)) [clock scan $tab(date)] }]} then {
11592	append err "Cannot execute SQL SELECT query for $dir\n"
11593	return {}
11594    }
11595
11596    #
11597    # Difference analysis
11598    #
11599    set r {}
11600    if {$err eq ""} then {
11601	foreach f [array names otab] {
11602	    if {[info exists ntab($f)]} then {
11603		if {$otab($f) != $ntab($f)} then {
11604		    lappend r [list "mod" $f $ntab($f)]
11605		}
11606		unset ntab($f)
11607	    } else {
11608		lappend r [list "del" $f ""]
11609	    }
11610	    unset otab($f)
11611	}
11612
11613	foreach f [array names ntab] {
11614	    lappend r [list "add" $f $ntab($f)]
11615	}
11616    }
11617
11618    return $r
11619}
11620
11621#
11622# Detect if a file has been modified
11623#
11624# Input:
11625#   - path : directory path
11626# Output:
11627#   - return value : see detect-dirmod
11628#
11629# History
11630#   2010/11/12 : pda/jean : design
11631#
11632
11633proc detect-filemod {path} {
11634    set oldfmod -1
11635    set qpath [::pgsql::quote $path]
11636    set sql "SELECT date FROM topo.filemonitor WHERE path = '$qpath'"
11637    if {[toposqlselect $sql tab {set oldfmod [clock scan $tab(date)]}]} then {
11638	if {[catch {file mtime $path} newfmod]} then {
11639	    #
11640	    # Error: we suppose that file does not exist
11641	    #
11642	    if {$oldfmod == -1} then {
11643		# file did not exist before, does not exists now
11644		set r [list "err" $path "Error on '$path': $newfmod"]
11645	    } else {
11646		# file was existing, but not now
11647		set r [list "del" $path ""]
11648	    }
11649	    set newfmod ""
11650	} else {
11651	    #
11652	    # File exists
11653	    #
11654	    if {$oldfmod == -1} then {
11655		# the file is new
11656		set r [list "add" $path $newfmod]
11657	    } elseif {$oldfmod == $newfmod} then {
11658		# dates are the same: file has not been modified
11659		set r {}
11660	    } else {
11661		# file is modified
11662		set r [list "mod" $path $newfmod]
11663	    }
11664	}
11665    } else {
11666	set r [list $path "err" "Error on '$path' : SQL query failed"]
11667    }
11668    topo-verbositer "detect-filemod: $path => $r" 9
11669
11670    return $r
11671}
11672
11673#
11674# Update file modification times in database
11675#
11676# Input:
11677#   - lf : list (see detect-dirmod for format)
11678# Output:
11679#   - return value : 1 if ok, 0 if error
11680#
11681# History
11682#   2010/11/12 : pda/jean : design
11683#
11684
11685proc sync-filemonitor {lf} {
11686    set sql {}
11687    foreach f $lf {
11688	lassign $f code path date
11689	set qpath [::pgsql::quote $path]
11690	switch $code {
11691	    add {
11692		set qdate [clock format $date]
11693		lappend sql "INSERT INTO topo.filemonitor (path, date)
11694					VALUES ('$qpath', '$qdate')"
11695	    }
11696	    mod {
11697		set qdate [clock format $date]
11698		lappend sql "UPDATE topo.filemonitor
11699					SET date = '$qdate'
11700					WHERE path = '$qpath'"
11701	    }
11702	    del {
11703		lappend sql "DELETE FROM topo.filemonitor
11704					WHERE path = '$qpath'"
11705	    }
11706	}
11707    }
11708    set r 1
11709    if {[llength $sql] > 0} then {
11710	set sql [join $sql ";"]
11711	set r [toposqlexec $sql]
11712    }
11713
11714    return $r
11715}
11716