1#
2#
3# Librairie de fonctions TCL utilisables dans les scripts CGI
4#
5# Historique
6#   1999/02/25 : pda : conception en package
7#   2000/07/19 : pda : ajout de generer-menu
8#   2001/02/28 : pda : suppression de get-raw-data ajouté par je ne sais pas qui
9#   2001/05/02 : pda : utilisation du package Pgtcl pour l'accès à PostgreSQL
10#   2001/10/20 : pda : ajout de la procédure sortie-html
11#   2002/05/11 : pda : ajout de la procédure sortie-latex
12#   2002/05/11 : pda : ajout des variables tmp et pdflatex
13#   2002/05/20 : pda : ajout de la procédure send à la place de sortie-*
14#   2002/06/04 : pda : ajout de la procédure nologin
15#   2002/12/26 : pda : ajout de la procédure error-exit
16#   2003/06/07 : pda : ajout de la procédure call-cgi
17#   2003/06/27 : pda : ajout de la procédure cgi-exec
18#   2003/09/29 : pda : ajout de la procédure mail
19#   2003/11/05 : pda : utilisation de string equal à la place de string length
20#   2004/02/12 : pda/jean : ajout form-bool
21#   2005/04/13 : pda : correction d'un bug dans form-text
22#   2006/08/29 : pda : ajout de import-vars
23#   2007/10/05 : pda/jean : ajout des objets auth et user
24#   2007/10/23 : pda/jean : ajout de l'objet log
25#   2008/06/12 : pda/jean : ajout de interactive-tree et helem
26#   2010/11/05 : pda      : méthode opened-postgresql pour l'objet log
27#   2010/11/09 : pda      : suppression generer-menu
28#   2010/11/27 : pda      : ajout locale
29#   2010/11/27 : pda      : envoi utf-8 systématique
30#   2010/12/09 : pda      : ajout form-submit et form-reset
31#   2010/12/16 : pda      : add import-vars optional fspec parameter
32#   2012/01/09 : pda      : add cmdpath
33#   2015/03/11 : pda/jean : add myurl
34#
35
36# packages nécessaires pour l'acces à la base d'authentification
37
38package require mime ;			# tcllib
39package require snit ;			# tcllib >= 1.10
40package require ldapx ;			# tcllib >= 1.10
41package require pgsql ;			# package local
42
43# package require Pgtcl
44
45package provide webapp 1.17
46
47namespace eval webapp {
48    namespace export log pathinfo user myurl locale \
49	form-field form-yesno form-bool form-menu form-text form-hidden \
50	form-submit form-reset \
51	hide-parameters file-subst \
52	helem interactive-tree \
53	get-data import-vars valid-email \
54	post-string html-string \
55	call-cgi \
56	mail \
57	random \
58	set-cookie get-cookie \
59	nologin send redirect error-exit \
60	debug cgidebug cgi-exec \
61	cmdpath
62
63    variable tmpdir	/tmp
64    variable pdflatex	/usr/local/bin/pdflatex
65    variable debuginfos {}
66    variable sendmail	{/usr/sbin/sendmail -t}
67
68    # element HTML (4.01) sans tag de fermeture
69    # cf http://www.w3.org/TR/1999/REC-html401-19991224/index/elements.html
70    variable noendtags	{area base basefont br col frame hr img input isindex
71				link meta param}
72    # url des images (pour la génération d'arbre interactif)
73    # relativement à la racine du serveur web
74    variable treeimages	css/images
75
76    # code Javascript de l'arbre interactif
77    variable treejs {
78	<script type="text/javascript">
79	  <!--
80	    // fonction pour initialiser la vue de l'arborescence
81	    // id : id de l'ul de l'arborescence à initialiser
82	    // disp : "none" ou "block"
83	    // Cette fonction masque tous les ul compris sous l'id,
84	    // puis affiche juste l'ul correspondant à l'id
85	    function multide(id, disp) {
86	      var x = document.getElementById (id) ;
87	      // vérification de cohérence
88	      if (! x || x.nodeName != "UL")
89		return 'PAS UN UL' ;
90	      tab = x.getElementsByTagName ("UL") ;
91	      for (var i = 0 ; i < tab.length ; i++) {
92		tab [i].style.display = disp ;
93	      }
94	      x.style.display = "block" ;
95	    }
96
97	    // fonction de déroulement/enroulement
98	    // img : un objet de type IMG (élément HTML) dont on
99	    //   veut dérouler/enrouler la liste associée
100	    //	 Typiquement, img est l'image "+" ou "-", et
101	    //   on veut dérouler/enrouler le ul qui suit
102	    //   dans la liste des frères
103	    function de(img) {
104	      var ul ;
105	      // vérification de cohérence
106	      if (img.nodeName != "IMG")
107		return 'PAS UN IMG'
108	      // parcourir tous les frères pour trouver l'UL qui doit suivre
109	      ul = img ;
110	      while (ul && ul.nodeName != "UL")
111		ul = ul.nextSibling ;
112	      if (! ul || ul.nodeName != "UL")
113		return 'PAS UN UL'
114	      // dérouler ou enrouler ?
115	      if (ul.style.display == "none") {
116		// dérouler
117		ul.style.display = "block" ;
118		img.src = "%TREEIMAGES%/tree-minus.gif" ;
119		img.alt = "[-]" ;
120	      } else {
121		// enrouler
122		ul.style.display = "none" ;
123		img.src = "%TREEIMAGES%/tree-plus.gif" ;
124		img.alt = "[+]" ;
125	      }
126	      return 'OK' ;
127	    }
128	  //-->
129	</script>
130    }
131
132    # CSS de l'arbre interactif (avec un trou correspondant à l'id)
133    variable treecss {
134	<style type="text/css">
135	<!--
136
137        html body ul#%ID% ul {
138          background: url("%TREEIMAGES%/tree-line.gif") repeat-y 0px 0px;
139          padding-left: 24px;
140          margin-left: 0;
141        }
142
143        html body ul#%ID% ul.last {
144          background: none;
145        }
146
147        html body ul#%ID% li {
148          background: none;
149          list-style: none;
150          padding: 0;
151          margin: 0;
152        }
153
154        html body ul#%ID% li ul li {
155          background: none;
156          list-style: none;
157          padding: 0;
158          margin: 0;
159        }
160
161        html body ul#%ID% li ul li ul li{
162          background: none;
163          list-style: none;
164          padding: 0;
165          margin: 0;
166        }
167
168        html body ul#%ID% li ul li ul li ul li{
169          background: none;
170          list-style: none;
171          padding: 0;
172          margin: 0;
173        }
174
175        html body ul#%ID% a {
176          padding: 0;
177          margin: 0;
178        }
179
180        html body ul#%ID% img {
181          padding: 0;
182          margin: 0;
183        }
184
185        html body ul#%ID% img.click {
186          cursor: pointer;
187        }
188
189	-->
190	</style>
191    }
192}
193
194##############################################################################
195# Set some variables
196##############################################################################
197
198proc ::webapp::cmdpath {cmd path} {
199    switch $cmd {
200	pdflatex { set ::webapp::pdflatex $path }
201	tmpdir   { set ::webapp::tmpdir	$path }
202	sendmail { set ::webapp::sendmail $path }
203    }
204}
205
206##############################################################################
207# Debug de certaines fonctions du script
208##############################################################################
209
210#
211# Positionne les informations de debug
212#
213# Entrée :
214#   - paramètres :
215#	- infos : listes de comportements à déboguer
216# Sortie :
217#   - valeur de retour : -
218#   - variables globales :
219#	- debuginfo : les informations de debug souhaitées
220#
221# Note : informations de debug possibles
222#	latexfiles : laisse les fichiers latex en l'état dans /tmp
223#	latexsource : sort le source latex et non le généré pdf
224#
225# Historique :
226#   2002/05/12 : pda : conception
227#
228
229proc ::webapp::debug {infos} {
230    set ::webapp::debuginfos $infos
231}
232
233
234##############################################################################
235# Fichier de log
236##############################################################################
237
238#
239# Ajoute une ligne dans un fichier de log
240#
241# Entrée :
242#   - paramètres :
243#	- fichier : nom du fichier de log
244#	- message : message à envoyer dans le log
245#   - variables d'environnement :
246#	- SCRIPT_NAME : voir procédure script-name
247#	- REMOTE_HOST, REMOTE_ADDR : nom du client, ou à défaut son adresse IP
248# Sortie :
249#   - pas de sortie
250#
251# Historique :
252#   1999/04/06 : pda : conception
253#   2000/12/12 : ??? : signalement de l'erreur d'ouverture sur stderr
254#
255
256proc ::webapp::log {fichier message} {
257    global env
258
259    set name [::webapp::script-name]
260
261    if {[info exists env(REMOTE_HOST)]} then {
262	set remote $env(REMOTE_HOST)
263    } else {
264	set remote $env(REMOTE_ADDR)
265    }
266
267    set date [clock format [clock seconds]]
268
269    if {[catch {open $fichier a} fd] == 0} then {
270	puts $fd [format "%s %s %s %s" $name $date $remote $message]
271	close $fd
272    } else {
273	puts stderr "erreur ouverture $fichier"
274    }
275}
276
277
278
279##############################################################################
280# Traitement des variables d'environnement
281##############################################################################
282
283#
284# Renvoie le contenu de la variable PATH_INFO
285#
286# Entrée :
287#   - variables d'environnement :
288#	- PATH_INFO : une chaîne de la forme "/relative/path/to/script"
289# Sortie :
290#   - valeur de retour : liste des composants
291#
292# Historique :
293#   1994/08/xx : pda : conception et codage
294#   1999/02/25 : pda : documentation
295#
296
297proc ::webapp::pathinfo {} {
298    global env
299
300    # vérifie que la variable existe
301    if {! [info exists env(PATH_INFO)]} then {
302	return {}
303    }
304
305    # découpe la variable en éléments de liste
306    set path [split $env(PATH_INFO) /]
307    # le premier élément est nul puisque le chemin commence par "/"
308    set path [lreplace $path 0 0]
309
310    return $path
311}
312
313
314#
315# Renvoie le nom du script courant
316#
317# Entrée :
318#   - variables d'environnement :
319#	- SCRIPT_NAME : une chaîne de la forme "/relative/path/to/script"
320# Sortie :
321#   - valeur de retour :
322#	- le nom, ou vide si rien
323#
324# Historique :
325#   1994/08/xx : pda : conception et codage
326#   1999/02/25 : pda : documentation
327#   1999/07/14 : pda : changement d'interface
328#
329
330proc ::webapp::script-name {} {
331    global env
332
333    if {[info exists env(SCRIPT_NAME)]} then {
334	set n [split $env(SCRIPT_NAME) "/"]
335	set nm [lindex $n [expr [llength $n]-1]]
336    } else {
337	set nm {}
338    }
339    return $nm
340}
341
342#
343# Renvoie le nom de l'utilisateur courant (authentification apache)
344#
345# Entrée :
346#   - variables d'environnement :
347#	- REMOTE_USER : une chaîne de la forme "login""
348# Sortie :
349#   - valeur de retour :
350#	- le nom, ou vide si rien
351#
352# Historique :
353#   1999/10/24 : pda : conception et codage
354#
355
356proc ::webapp::user {} {
357    global env
358
359    if {[info exists env(REMOTE_USER)]} then {
360	set nm $env(REMOTE_USER)
361    } else {
362	set nm {}
363    }
364    return $nm
365}
366
367#
368# Returns the complete URL of current script
369#
370# Input:
371#   - level: nomber of components to remove from the end of the URL
372#	(0: complete URL, 1: remove the last component, etc.)
373#   - environment variables:
374#	- REQUEST_URI
375#	- REQUEST_SCHEME
376#	- SERVER_NAME
377#	- SERVER_PORT
378# Output:
379#   - return value: URL or empty string
380#
381# History:
382#   2015/03/11 : pda/jean : design
383#
384
385proc ::webapp::myurl {{level 0}} {
386    global env
387
388    foreach {v e} {
389		    uri    REQUEST_URI
390		    scheme REQUEST_SCHEME
391		    server SERVER_NAME
392		    port   SERVER_PORT
393		} {
394	if {! [info exists env($e)]} then {
395	    return ""
396	}
397	set $v $env($e)
398    }
399
400    # special case for IPv6 numeric addresses
401    if {[regexp {:} $server]} then {
402	set server "\[$server\]"
403    }
404
405    # remove some components from the request URI
406    for {set i 0} {$i < $level} {incr i} {
407	regsub {/[^/]*$} $uri {} uri
408    }
409    if {$uri eq ""} then {
410	set uri "/"
411    }
412
413    return "$scheme://$server:$port$uri"
414}
415
416#
417# Renvoie les langues acceptées par l'utilisateur
418#
419# Entrée :
420#   - avail : locales disponibles
421#   - variables d'environnement :
422#	- HTTP_ACCEPT_LANGUAGE : une chaîne au format RFC 2616
423#		lang[;q=\d],...
424# Sortie :
425#   - valeur de retour : locale à utiliser
426#
427# Historique :
428#   2010/11/27 : pda : conception et codage
429#
430
431proc ::webapp::locale {avail} {
432    global env
433
434    if {[info exists env(HTTP_ACCEPT_LANGUAGE)]} then {
435	#
436	# Analyse la chaîne et crée un tableau indexé par
437	# le facteur de qualité (q=)
438	#
439	set accepted [string tolower $env(HTTP_ACCEPT_LANGUAGE)]
440	foreach a [split $accepted ","] {
441	    regsub -all {\s+} $a {} a
442	    set s [split $a ";"]
443	    set lang [lindex $s 0]
444	    set q 1
445	    foreach param [lreplace $s 0 0] {
446		regexp {^q=([.0-9]+)$} $param bidon q
447	    }
448	    lappend tabl($q) $lang
449	    set tabq($lang) $q
450	}
451	#
452	# En cas de langage avec un sous-tag, ajouter le langage
453	# primaire s'il n'existe pas.
454	# Il peut y avoir un nombre quelconque de sous-tags
455	# fr-fr-paris-xive-alesia
456	#
457	foreach l [array names tabq] {
458	    set q $tabq($l)
459	    set ll [split $l "-"]
460	    while {[llength $ll] > 1} {
461		set ll [lreplace $ll end end]
462		set llp [join $ll "-"]
463		if {! [info exists tabq($llp)]} then {
464		    lappend tabl($q) $llp
465		    set tabq($llp) $q
466		}
467	    }
468	}
469
470	#
471	# Croise avec les langages disponibles, en tenant
472	# compte du facteur de qualité
473	#
474	set avail [string tolower $avail]
475	set locale "C"
476	foreach q [lsort -real -decreasing [array names tabl]] {
477	    foreach l $tabl($q) {
478		if {[lsearch -exact $avail $l] != -1} then {
479		    set locale $l
480		    break
481		}
482	    }
483	    if {$locale ne "C"} then {
484		break
485	    }
486	}
487    } else {
488	set locale "C"
489    }
490
491    return $locale
492}
493
494##############################################################################
495# Génération de fragments de code HTML
496##############################################################################
497
498#
499# Génération de balises HTML conformes à HTML 4.01
500#
501# Entrée :
502#   - paramètres :
503#	- tag : balise HTML ("img", "ul", "a", etc.)
504#	- content : texte associé au tag (entre les balises)
505#	- args : attributs de la balise
506# Sortie :
507#   - code HTML généré
508#
509# Exemple :
510#   puts [helem "a" "cliquer ici" "href" "http://www.tcl.tk"]
511#
512# Historique :
513#   2008/06/12 : pda/jean/lauce : intégration webapp
514#
515
516proc ::webapp::helem {tag content args} {
517    set tag [string tolower $tag]
518    set r "<$tag"
519    foreach {attr value} $args {
520	set attr [string tolower $attr]
521	append r " $attr=\"$value\""
522    }
523    append r ">$content"
524    # ne mettre une fermeture que pour les tags qui ne figurent pas
525    # dans la liste des tags sans fermeture
526    if {[lsearch $::webapp::noendtags $tag] == -1} then {
527	append r "</$tag>"
528    }
529    return $r
530}
531
532#
533# Génération du code HTML pour éditer un champ
534#
535# Entrée :
536#   - paramètres :
537#	- spec : spécification du champ, sous la forme
538#		string [<largeur> [<largeurmax>]]
539#		hidden
540#		text [<hauteur> [<largeur>]]
541#		menu <item> ... <item>, où <item>={<valeur envoyée> <affichée>}
542#		list <mono/multi> <taille> <item> ... <item>
543#		password [<largeur> [<largeurmax>]]
544#		bool
545#		hidden
546#		yesno [fmt]
547#	- var : variable du formulaire
548#	- val : valeur initiale (par défaut)
549# Sortie :
550#   - code HTML généré
551#
552# Historique :
553#   2003/08/01 : pda : conception
554#
555
556proc ::webapp::form-field {spec var val} {
557    set nargs [llength $spec]
558    switch -- [lindex $spec 0] {
559	string {
560	    switch $nargs {
561		2 {
562		    set largeur	[lindex $spec 1]
563		    set max	0
564		}
565		3 {
566		    set largeur [lindex $spec 1]
567		    set max     [lindex $spec 2]
568		}
569		default {
570		    set largeur	0
571		    set max	0
572		}
573	    }
574	    set h [::webapp::form-text $var 1 $largeur $max $val]
575	}
576	bool {
577	    set h [::webapp::form-bool $var $val]
578	}
579	password {
580	    set hval [::webapp::unpost-string $val]
581	    set h <INPUT TYPE=PASSWORD NAME=$var VALUE=\"$hval\">"
582	}
583	text {
584	    switch $nargs {
585		2 {
586		    set hauteur	[lindex $spec 1]
587		    set largeur	0
588		}
589		3 {
590		    set hauteur [lindex $spec 1]
591		    set largeur [lindex $spec 2]
592		}
593		default {
594		    set hauteur	0
595		    set largeur	0
596		}
597	    }
598	    set h [::webapp::form-text $var $hauteur $largeur 0 $val]
599	}
600	menu {
601	    set items [lreplace $spec 0 0]
602	    set h [::webapp::form-menu $var 1 0 $items $val]
603	}
604	list {
605	    set monomulti [lindex $spec 1]
606	    set taille    [lindex $spec 2]
607	    set items	  [lreplace $spec 0 2]
608	    set multiple 0
609	    if {[string equal $monomulti "multi"]} then {
610		set multiple 1
611	    }
612	    set h [::webapp::form-menu $var $taille $multiple $items $val]
613	}
614	yesno {
615	    set fmt {%1$s&nbsp;Oui&nbsp;&nbsp;&nbsp;%2$s&nbsp;Non}
616	    if {$nargs >= 2} then {
617		set fmt [lindex $spec 1]
618	    }
619	    set h [::webapp::form-yesno $var $val $fmt]
620	}
621	hidden {
622	    set h [::webapp::form-hidden $var $val]
623	}
624	default {
625	    set h "ERREUR"
626	}
627    }
628    return $h
629}
630
631#
632# Génération du code HTML pour réaliser un item oui/non
633#
634# Entrée :
635#   - paramètres :
636#	- var : variable du formulaire pour ce menu
637#	- defval : valeur par défaut
638#	- fmt : format pour la sortie de l'HTML
639# Sortie :
640#   - code HTML généré
641#
642# Historique :
643#   2001/06/18 : pda : conception
644#
645
646proc ::webapp::form-yesno {var defval fmt} {
647    set oui "<INPUT TYPE=radio NAME=$var VALUE=1"
648    set non "<INPUT TYPE=radio NAME=$var VALUE=0"
649    if {! [string equal $defval ""] && $defval} then {
650	append oui " CHECKED"
651    } else {
652	append non " CHECKED"
653    }
654    append oui ">"
655    append non ">"
656    set html [format $fmt $oui $non]
657    return $html
658}
659
660#
661# Génération du code HTML pour réaliser un item booléen (case cochée ou non)
662#
663# Entrée :
664#   - paramètres :
665#	- var : variable du formulaire pour ce menu
666#	- defval : valeur par défaut (=0 ou !=0)
667# Sortie :
668#   - code HTML généré
669#
670# Historique :
671#   2004/02/12 : pda/jean : conception
672#
673
674proc ::webapp::form-bool {var defval} {
675    set checked ""
676    if {[regexp {^[0-9]+$} $defval] && $defval} then {
677	set checked " CHECKED"
678    }
679    set html "<INPUT TYPE=CHECKBOX NAME=$var VALUE=1$checked>"
680    return $html
681}
682
683#
684# Génération du code HTML pour réaliser un menu déroulant ou une
685# liste à choix multiples
686#
687# Entrée :
688#   - paramètres :
689#	- var : variable du formulaire pour ce menu
690#	- taille : taille de la liste (1 si menu déroulant)
691#	- multiple : 1 si choix multiple autorisé, 0 sinon
692#	- liste : liste de couples { <valeur renvoyée> <item affiché> }
693#	- lsel : liste des indices des items sélectionnés
694# Sortie :
695#   - code HTML généré
696#
697# Historique :
698#   2001/04/27 : pda      : conception
699#   2004/01/16 : pda/jean : correction d'un bug si lsel non trié
700#
701
702proc ::webapp::form-menu {var taille multiple liste lsel} {
703    set indice 0
704
705    set lsel [lsort -integer $lsel]
706
707    set optsel [lindex $lsel 0]
708    set lsel [lreplace $lsel 0 0]
709
710    set m ""
711    if {$multiple} then { set m "MULTIPLE" }
712
713    set html "<SELECT SIZE=\"$taille\" NAME=\"$var\" $m>\n"
714
715    foreach item $liste {
716	set valeur  [::webapp::html-string [lindex $item 0]]
717	set libelle [::webapp::html-string [lindex $item 1]]
718
719	append html "<OPTION"
720
721	if {! [string equal $valeur ""]} then {
722	    append html " VALUE=\"$valeur\""
723	}
724
725	if {[string equal $indice $optsel]} then {
726	    append html " SELECTED"
727	    set optsel [lindex $lsel 0]
728	    set lsel [lreplace $lsel 0 0]
729	}
730
731	append html ">$libelle\n"
732
733	incr indice
734    }
735    append html "</SELECT>\n"
736
737    return $html
738}
739
740#
741# Génération du code HTML pour réaliser un bouton (submit ou reset)
742#
743# Entrée :
744#   - paramètres :
745#	- var : variable du formulaire pour ce menu ou {}
746#	- val : valeur par défaut (=0 ou !=0)
747# Sortie :
748#   - code HTML généré
749#
750# Historique :
751#   2010/12/09 : pda      : conception
752#
753
754proc ::webapp::form-submit {var val} {
755    set name ""
756    if {$var ne ""} then {
757	set name "name=\"$var\""
758    }
759    set val [::webapp::html-string $val]
760    set html "<input type=submit $name value=\"$val\">"
761    return $html
762}
763
764proc ::webapp::form-reset {var val} {
765    set name ""
766    if {$var ne ""} then {
767	set name "name=\"$var\""
768    }
769    set val [::webapp::html-string $val]
770    set html "<input type=reset $name value=\"$val\">"
771    return $html
772}
773
774#
775# Génération du code HTML pour réaliser une ligne de texte
776#
777# Entrée :
778#   - paramètres :
779#	- var : variable du formulaire pour cette ligne
780#	- hauteur : hauteur de l'entrée, ou 0 pour la hauteur par défaut
781#	- largeur : taille de l'entrée, ou 0 pour la taille par défaut
782#	- max : nb maximum de caractères autorisés, 0 pour la valeur par défaut
783#	- valeur : valeur initiale
784# Sortie :
785#   - code HTML généré
786#
787# Historique :
788#   2001/04/27 : pda : conception
789#   2005/04/13 : pda : manquait ">" si input sans valeur par défaut
790#
791
792proc ::webapp::form-text {var hauteur largeur max valeur} {
793    set v [::webapp::html-string $valeur]
794    if {$hauteur <= 1} then {
795	#
796	# Simple ligne
797	#
798	set html "<INPUT TYPE=text NAME=\"$var\""
799
800	if {$largeur > 0} then {
801	    append html " SIZE=\"$largeur\""
802	}
803
804	if {$max > 0} then {
805	    append html " MAXLENGTH=\"$max\""
806	}
807
808	if {! [string equal $valeur ""]} then {
809	    append html " VALUE=\"$v\""
810	}
811
812	append html ">"
813    } else {
814	#
815	# Zone de texte multi-ligne
816	#
817	set html "<TEXTAREA NAME=\"$var\" ROWS=\"$hauteur\""
818
819	if {$largeur > 0} then {
820	    append html " COLS=\"$largeur\""
821	}
822	append html ">$v</TEXTAREA>"
823    }
824
825    return $html
826}
827
828#
829# Génération du code HTML pour réaliser un champ hidden
830#
831# Entrée :
832#   - paramètres :
833#	- var : variable du formulaire pour ce menu
834#	- defval : valeur par défaut
835# Sortie :
836#   - code HTML généré
837#
838# Historique :
839#   2003/08/03 : pda : conception
840#
841
842proc ::webapp::form-hidden {var defval} {
843    set v [::webapp::html-string $defval]
844    return "<INPUT TYPE=HIDDEN NAME=\"$var\" VALUE=\"$v\">"
845}
846
847#
848# Génération d'un arbre interactif (avec Javascript)
849#
850# Entrée :
851#   - paramètres :
852#	- id : id de l'élément racine (tag html "ul") de l'arbre généré
853#	- tree : arbre, au format :
854#		{<code-html> <arbre-fils> <arbre-fils> ... <arbre-fils>}
855#	    chaque <arbre-fils> pouvant être lui-même un arbre.
856#	    Si un arbre n'a pas de racine unique, le <code-html> de la
857#	    racine est vide, et chaque fils constitue une racine.
858#	- expcoll : liste de deux textes à afficher (pour tout dérouler
859#	    et tout enrouler, dans l'ordre)
860# Sortie :
861#   - valeur de retour : liste contenant les éléments suivants :
862#		{head1 head2 onload html}
863#	où :
864#	- head1 : code HTML prêt à être inséré dans l'en-tête HTML
865#		de la page. Ce code est toujours le même quel que
866#		soit l'arbre (fonctions Javascript)
867#	- head2 : code HTML prêt à être inséré dans l'en-tête HTML
868#		de la page. Ce code est spécifique à l'arbre
869#		(spécifications CSS dépendant de l'id)
870#	- onload : code Javascript pour l'état initial (enroulé ou déroulé)
871#		initial de l'arbre
872#	- html : code HTML pour l'arbre lui-même
873#
874# Exemple d'arbre :
875#	{/
876#	    {/bin
877#		ls sh rm mkdir rmdir }
878#	    {/etc passwd
879#		{/etc/mail sendmail.cf submit.cf}}
880#	    {/usr
881#		{/usr/include
882#		    {/usr/include/sys types.h}
883#		    stdio.h}
884#		{/usr/bin ...}
885#	    }
886#	}
887#
888# Historique :
889#   2008/06/12 : pda/jean : conception
890#   2008/08/14 : pda      : ajout expcoll
891#
892
893proc ::webapp::interactive-tree {id tree expcoll} {
894    set root      [lindex $tree 0]
895    set children  [lreplace $tree 0 0]
896    set nchildren [llength $children]
897
898    #
899    # Générer le code HTML non spécifique de l'en-tête
900    #
901
902    set head1 $::webapp::treejs
903    regsub -all "%TREEIMAGES%" $head1 $::webapp::treeimages head1
904
905    #
906    # Générer le code HTML de l'en-tête spécifique à cet arbre
907    #
908
909    set head2 $::webapp::treecss
910    regsub -all "%ID%" $head2 $id head2
911    regsub -all "%TREEIMAGES%" $head2 $::webapp::treeimages head2
912
913    #
914    # Générer le code Javascript du "body onload"
915    #
916
917    set onload "javascript:multide('$id','none');"
918
919    #
920    # Générer le code HTML de l'arbre
921    #
922
923    if {$root eq ""} then {
924	set li ""
925	for {set i 0} {$i < $nchildren} {incr i} {
926	    set lastnext [expr {$i == $nchildren-1}]
927	    append li [::webapp::interactive-tree-rec 1 \
928						      [lindex $children $i] \
929						      $lastnext \
930						      ]
931	    append li "\n"
932	}
933    } else {
934	set li [::webapp::interactive-tree-rec 1 $tree 1]
935    }
936    set ul [helem ul $li "id" $id]
937
938    #
939    # Afficher les boutons "tout enrouler" et "tout dérouler"
940    #
941
942    if {[llength $expcoll] > 0} then {
943	set de [lindex $expcoll 0]
944	set en [lindex $expcoll 1]
945
946	set i1 [helem "img" "" \
947			    "src" "$::webapp::treeimages/tree-plus-only.png" \
948			    "alt" "+" \
949			    "onclick" "multide('$id', 'block')" \
950			    "class" "click" \
951			]
952	set i2 [helem "img" "" \
953			    "src" "$::webapp::treeimages/tree-minus-only.png" \
954			    "alt" "+" \
955			    "onclick" "multide('$id', 'none')" \
956			    "class" "click" \
957			]
958	set ul "$i1 $de &nbsp;&nbsp;&nbsp; $i2 $en\n$ul"
959    }
960
961    #
962    # Résultat final : assemblage des quatre éléments
963    #
964
965    return [list $head1 $head2 $onload $ul]
966}
967
968# level : profondeur (1 .. n) de l'arbre en cours
969# tree : arbre en cours
970# last : 1 si l'arbre est le dernier des fils de l'arbre père
971proc ::webapp::interactive-tree-rec {level tree last} {
972    set root [lindex $tree 0]
973    set children [lreplace $tree 0 0]
974    set nchildren [llength $children]
975
976    if {$nchildren == 0} then {
977	if {$last} then {
978	    set file "$::webapp::treeimages/tree-joinbottom.gif"
979	} else {
980	    set file "$::webapp::treeimages/tree-join.gif"
981	}
982	set img [helem "img" "" src $file]
983	set li [helem "li" "$img\n$root\n"]
984    } else {
985	set img [helem "img" "" \
986				"src" "$::webapp::treeimages/tree-plus.gif" \
987				"alt" "+" \
988				"onclick" "de(this)" \
989				"class" "click" \
990			    ]
991
992	set li ""
993	for {set i 0} {$i < $nchildren} {incr i} {
994	    set lastnext [expr {$i == $nchildren-1}]
995	    append li [::webapp::interactive-tree-rec [expr $level+1] \
996						      [lindex $children $i] \
997						      $lastnext \
998						      ]
999	    append li "\n"
1000	}
1001	set class "niv$level"
1002	if {$last} then {
1003	    append class " last"
1004	}
1005	set ul [helem "ul" $li "class" $class]
1006
1007	set li [helem "li" "$img\n$root\n$ul\n"]
1008    }
1009
1010    return $li
1011}
1012
1013
1014##############################################################################
1015# Cacher des paramètres dans une liste de champs INPUT HIDDEN
1016##############################################################################
1017
1018#
1019# Cache des paramètres dans une liste de champs INPUT HIDDEN
1020#
1021# Entrée :
1022#   - paramètres :
1023#	- champs : liste de champs à chercher dans le tableau
1024#	- formtab : tableau de champs tels qu'issu de get-data
1025# Sortie :
1026#   - valeur de retour : une suite de balises INPUT
1027#
1028# Historique
1029#   1999/11/01 : pda : conception et codage
1030#   2000/07/25 : pda : ajout de \n entre deux HIDDEN
1031#   2006/11/02 : pda : re-ajout de \n entre deux HIDDEN
1032#
1033
1034proc ::webapp::cacher-parametres {champs formtab} {
1035    upvar $formtab ftab
1036
1037    return [::webapp::hide-parameters $champs ftab]
1038}
1039
1040proc ::webapp::hide-parameters {champs formtab} {
1041    upvar $formtab ftab
1042
1043    set html {}
1044    foreach regexp $champs {
1045	foreach c [array names ftab] {
1046	    if {! [info exists dejavu($c)] && [regexp "^$regexp\$" $c]} then {
1047		set dejavu($c) 1
1048		foreach v $ftab($c) {
1049		    lappend html [::webapp::form-hidden $c $v]
1050		}
1051	    }
1052	}
1053    }
1054    return [join $html "\n"]
1055}
1056
1057##############################################################################
1058# Appel d'un autre script cgi
1059##############################################################################
1060
1061#
1062# Appelle un script CGI en respectant le protocole.
1063#
1064# Entrée :
1065#   - paramètres :
1066#	- formtab : tableau, passé par référence, contenant les champs
1067#		de formulaire, tels que get-data les extrait
1068#
1069# Sortie :
1070#   - valeur de retour : aucune
1071#   - sortie standard : les données du script appelées sont envoyées sur stdout
1072#
1073# Notes : les variables d'environnement suivantes sont modifiées
1074#   - REQUEST_METHOD : mis à GET
1075#   - PATH_INFO : remis à ""
1076#   - QUERY_STRING : la partie après le "?" dans l'URL
1077#   - HTTP_COOKIE : les cookies enregistrés via set-cookie
1078#   Les autres variables ne sont pas changées.
1079#
1080# Historique :
1081#   2003/06/07 : pda      : conception et codage
1082#   2014/05/09 : pda/jean : add cookies
1083#
1084
1085proc ::webapp::call-cgi {script formtab} {
1086    global env
1087
1088    upvar $formtab ftab
1089
1090    #
1091    # On utilise la méthode "GET"
1092    #
1093
1094    set env(REQUEST_METHOD) "GET"
1095
1096    #
1097    # Positionner la "query string" en fonction des paramètres
1098    #
1099
1100    set query {}
1101    foreach key [array names ftab] {
1102	set qkey [::webapp::post-string $key]
1103	foreach val $ftab($key) {
1104	    set qval [::webapp::post-string $val]
1105	    lappend query "$qkey=$qval"
1106	}
1107    }
1108    set env(QUERY_STRING) [join $query "&"]
1109
1110    #
1111    #  Détruit PATH_INFO
1112    #
1113
1114    catch {unset env(PATH_INFO)}
1115
1116    #
1117    # Passer les cookies
1118    #
1119
1120    set hc {}
1121    global wcooktab
1122    foreach name [array names wcooktab] {
1123	lappend hc $wcooktab($name)
1124    }
1125    set env(HTTP_COOKIE) [join $hc ";"]
1126
1127    #
1128    # Appeler le script
1129    #
1130
1131    return [exec $script]
1132}
1133
1134##############################################################################
1135# Traitement des formulaires
1136##############################################################################
1137
1138#
1139# Récupère le contenu d'une FORM, ou de QUERY_STRING ou de PATH_INFO
1140# et place dans le tableau fourni en paramètre les champs trouvés.
1141#
1142# Entrée :
1143#   - paramètres :
1144#	- formtab : tableau, passé par référence
1145#	- param : liste des paramètres des champs, sous la forme
1146#		d'une liste {champ nbmin nbmax def}, avec :
1147#			champ : nom du champ (regexp)
1148#			nbmin/mbmax : nb d'occurrences du champ (si checkbox)
1149#			def : valeur par défaut
1150#   - variables d'environnement :
1151#	- CONTENT_TYPE : doit être "application/x-www-form-urlencoded"
1152#	- REQUEST_METHOD : doit être POST
1153#	- CONTENT_LENGTH : longueur des données du formulaire
1154#	- PATH_INFO : la partie d'URL après le nom du script CGI
1155#	- QUERY_STRING : la partie après le "?" dans l'URL
1156# Sortie :
1157#   - paramètre formtab : chaque champ du formulaire est placé
1158#	dans le tableau, avec comme index l'intitulé du champ
1159#   - valeur de retour : {} si erreur, liste des champs lus si pas d'erreur
1160#
1161# Historique :
1162#   1994/08/xx : pda : conception et codage
1163#   1999/02/25 : pda : documentation
1164#   1999/02/26 : pda : changement du test de CONTENT_TYPE (peut être vide)
1165#   1999/04/05 : pda : possibilité d'avoir plusieurs fois le même champ
1166#   1999/04/05 : pda : ajout de la vérification des champs
1167#   1999/10/02 : pda : gestion de plusieurs sources (pathinfo et querystring)
1168#   1999/10/29 : pda : traitement des noms de champs comme des regexp
1169#   1999/11/01 : pda : possibilité de multiples appels et chgt valeur de retour
1170#
1171
1172set ::webapp::gotform 0
1173
1174proc ::webapp::get-data {_ftab param} {
1175    global ::webapp::gotform ::webapp::formtab
1176    upvar $_ftab ftab
1177
1178    if {! $::webapp::gotform} then {
1179	#
1180	# On n'essayera plus de relire les paramètres (ça serait bloquant
1181	# si on essayait de relire sur stdin) lors des appels ultérieurs.
1182	#
1183
1184	set ::webapp::gotform 1
1185
1186	#
1187	# Récupérer les informations de :
1188	#	- PATH_INFO
1189	#	- QUERY_STRING
1190	#	- les champs du formulaire
1191	#
1192
1193	set lus 0
1194	incr lus [::webapp::recuperer-pathinfo    ftab $param]
1195	incr lus [::webapp::recuperer-querystring ftab $param]
1196	incr lus [::webapp::recuperer-form        ftab $param]
1197
1198	#
1199	# Si on n'a rien lu, il n'y a rien à vérifier
1200	#
1201
1202	if {$lus == 0} then {
1203	    return {}
1204	}
1205    }
1206
1207    #
1208    # Boucle de vérification : analyser tous les champs
1209    # listés en paramètre.
1210    # En passant, on met specfield(champ) à 1 pour chaque champ
1211    # trouvé dans le formulaire.
1212    #
1213
1214    foreach p $param {
1215	set nom   [lindex $p 0]
1216	set nbmin [lindex $p 1]
1217	set nbmax [lindex $p 2]
1218	set def   [lindex $p 3]
1219	if {[info exists ftab($nom)]} then {
1220	    if {[::webapp::trouve-form ftab $nom $nbmin $nbmax] == 0} then {
1221		return {}
1222	    }
1223	    set specfield($nom) 1
1224	} else {
1225	    set trouve 0
1226	    foreach p [array names ftab] {
1227		if {[regexp "^$nom\$" $p]} then {
1228		    if {[::webapp::trouve-form ftab $p $nbmin $nbmax] == 0} then {
1229			return {}
1230		    }
1231		    set specfield($p) 1
1232		    set trouve 1
1233		}
1234	    }
1235
1236	    if {! $trouve} then {
1237		if {$nbmin > 0} then {
1238		    set ftab(_error) "mandatory field '$nom' not found"
1239		    return {}
1240		} else {
1241		    set ftab($nom) $def
1242		    set specfield($nom) 1
1243		}
1244	    }
1245	}
1246    }
1247
1248    #
1249    # On renvoie maintenant la liste des éléments trouvés
1250    #
1251
1252    return [array names specfield]
1253}
1254
1255proc ::webapp::trouve-form {formtab nom nbmin nbmax} {
1256    upvar $formtab tab
1257    set n [llength $tab($nom)]
1258    if {$n < $nbmin || $n > $nbmax} then {
1259	set tab(_error) "invalid number of fields ($n) for parameter '$nom'"
1260	return 0
1261    }
1262}
1263
1264proc ::webapp::get-keyval {formtab l} {
1265    upvar $formtab tab
1266
1267    foreach arg $l {
1268	if {[regexp {^([^=]+)=(.*)$} $arg bidon key val]} then {
1269	    set key [::webapp::unpost-string $key]
1270	    set val [::webapp::unpost-string $val]
1271	    lappend tab($key) $val
1272	}
1273    }
1274}
1275
1276proc ::webapp::recuperer-pathinfo {formtab param} {
1277    upvar $formtab tab
1278
1279    set lcomposants [::webapp::pathinfo]
1280
1281    if {[llength $lcomposants] == 0} then {
1282	return 0
1283    }
1284
1285    ::webapp::get-keyval tab $lcomposants
1286
1287    return 1
1288}
1289
1290proc ::webapp::recuperer-querystring {formtab param} {
1291    global env
1292    upvar $formtab tab
1293
1294    if {! [info exists env(QUERY_STRING)]} then {
1295	return 0
1296    }
1297
1298    ::webapp::get-keyval tab [split $env(QUERY_STRING) "&"]
1299
1300    return 1
1301}
1302
1303#
1304# Décode les éléments d'un formulaire en format "x-www-form-urlencoded"
1305#
1306# Entrée :
1307#   - paramètres :
1308#	- formtab : tableau de champs, cf get-data
1309# Sortie :
1310#   - valeur de retour : 1 si ok, 0 si erreur
1311#
1312# Historique
1313#   2003/06/01 : pda : séparation de recuperer-form
1314#
1315
1316proc ::webapp::x-www-form-urlencoded {formtab} {
1317    global env
1318    upvar $formtab tab
1319
1320    #
1321    # Méthode classique pour récupérer les champs
1322    # des formulaires
1323    #
1324
1325    if {! [info exists env(CONTENT_LENGTH)]} then {
1326	lappend tab(_error) "non existant CONTENT_LENGTH"
1327	return 0
1328    }
1329    set line [read stdin $env(CONTENT_LENGTH)]
1330
1331    ::webapp::get-keyval tab [split $line "&"]
1332
1333    return 1
1334}
1335
1336#
1337# Décode une sous-partie MIME d'un formulaire en format "form-data"
1338#
1339# Entrée :
1340#   - paramètres :
1341#	- formtab : tableau de champs, cf get-data
1342#	- entete : l'en-tête de la sous-partie
1343#	- corps : le corps de la sous-partie
1344# Sortie :
1345#   - valeur de retour : 1 si ok, 0 si erreur
1346#
1347# Notes :
1348#   - le format de l'en-tête de la sous-partie est :
1349#	Content-Disposition: form-data; name="<champ formulaire>"; filename="..."
1350#	Content-Type: image/gif
1351#   - le corps est le contenu du fichier.
1352#   - si c'est une variable classique de formulaire, il n'y a pas de filename=
1353#
1354# Historique
1355#   2003/06/01 : pda : commentaires
1356#
1357
1358proc ::webapp::get-mime-part {formtab entete corps} {
1359    upvar $formtab tab
1360
1361    set hdrre {^([^: \t]+):[ \t]*(.*)}
1362    set subhdrre {^([^= \t]+)[ \t]*=[ \t]*(.*)}
1363    set unquotere {^"([^"]*)"$}
1364
1365    #
1366    # Traitement de l'en-tête
1367    #
1368
1369    regsub -all -- "\r\n" $entete "\n" entete
1370    foreach ligne [split $entete "\n"] {
1371	#
1372	# Première partie : séparer "nom: valeur" (ex: Content-Disposition: ...)
1373	#
1374	if {! [regexp $hdrre $ligne bidon hname hval]} then {
1375	    return 0
1376	}
1377	# nom du champ d'en-tête
1378	set hname [string tolower $hname]
1379
1380	# la valeur peut elle-même être de la forme "val;clef=val;clef=val..."
1381	set hval [split $hval ";"]
1382
1383	set subhdrlist {}
1384	lappend subhdrlist VALEUR
1385	lappend subhdrlist [lindex $hval 0]
1386
1387	#
1388	# Parcourir toutes les sous-valeurs de la ligne d'en-tête
1389	#
1390
1391	foreach hv [lrange $hval 1 end] {
1392	    if {! [regexp $subhdrre [string trim $hv] bidon clef val]} then {
1393		lappend tab(_error) "Invalid form-data sub-header name '$hname'"
1394		return 0
1395	    }
1396	    if {[regexp $unquotere $val bidon v]} then {
1397		set val $v
1398	    }
1399	    lappend subhdrlist [string tolower $clef]
1400	    lappend subhdrlist $val
1401	}
1402	array set sh $subhdrlist
1403
1404	#
1405	# Une fois la ligne d'en-tête complètement parcourue, regarder
1406	# quels sont les associations "clef/valeur" obtenues.
1407	# Ces associations sont dans le tableau sh()
1408	#	sh(VALEUR) :	valeur du champ d'en-tête
1409	#	sh(name) :	nom de la variable du formulaire
1410	#	sh(filename) :	nom du fichier fourni par le client
1411	#
1412
1413	switch -- $hname {
1414	    content-disposition {
1415		if {! [string equal -nocase $sh(VALEUR) "form-data"]} then {
1416		    lappend tab(_error) "Invalid Content-Disposition header"
1417		    return 0
1418		}
1419		if {! [info exists sh(name)]} then {
1420		    lappend tab(_error) "No 'name' attribute in form"
1421		    return 0
1422		}
1423		set h(name) $sh(name)
1424		if {[info exists sh(filename)]} then {
1425		    set h(filename) $sh(filename)
1426		}
1427	    }
1428	    content-type {
1429		set h(contenttype) $sh(VALEUR)
1430	    }
1431	    default {
1432		lappend tab(_error) "Invalid form-data sub-header name '$hname'"
1433		return 0
1434	    }
1435	}
1436	unset sh
1437    }
1438
1439    #
1440    # Traitement du corps
1441    #
1442
1443    if {! [info exists h(name)]} then {
1444	lappend tab(_error) "No 'name' attribute in form"
1445	return 0
1446    }
1447    set name $h(name)
1448
1449    #
1450    # Si c'est un fichier, le placer dans une liste de la forme
1451    #		{file <type> <filename> <content>}
1452    # Sinon, nettoyer les \r\n
1453    #
1454
1455    if {[info exists h(filename)]} then {
1456	if {! [info exists h(contenttype)]} then {
1457	    set h(contenttype) application/octet-stream
1458	}
1459
1460	lappend tab($name) [list "file" $sh(filename) $h(contentype) $corps]
1461
1462    } else {
1463	#
1464	# Variable classique (i.e. pas un fichier)
1465	#
1466	regsub -all -- "\r\n" $corps "\n" corps
1467	lappend tab($name) $corps
1468    }
1469
1470    return 1
1471}
1472
1473proc ::webapp::form-data {formtab contenttype} {
1474    global env
1475    upvar $formtab tab
1476
1477    #
1478    # Méthode pour récupérer les champs des formulaires
1479    # spécifiée dans la RFC 1867, notamment pour gérer
1480    # les fichiers.
1481    #
1482
1483    if {! [info exists env(CONTENT_LENGTH)]} then {
1484	lappend tab(_error) "non existant CONTENT_LENGTH"
1485	return 0
1486    }
1487
1488    #
1489    # Extraire le délimiteur
1490    #
1491
1492    set boundary ""
1493    foreach element [split $contenttype ";"] {
1494	if {[regexp {boundary=(.*)} $element bidon boundary]} then {
1495	    break
1496	}
1497    }
1498    if {[string equal $boundary ""]} then {
1499	lappend tab(_error) "boundary not found in CONTENT_TYPE"
1500	return 0
1501    }
1502    set boundary "--$boundary"
1503
1504    #
1505    # Lire les données du formulaire et les mettre en mémoire
1506    #
1507
1508    fconfigure stdin -translation binary
1509    set line [read stdin $env(CONTENT_LENGTH)]
1510
1511    set fd [open /tmp/g.log w]
1512    fconfigure $fd -translation binary
1513    puts $fd $line
1514    close $fd
1515
1516    #
1517    # Rechercher le premier délimiteur
1518    #
1519
1520    set offset [string first $boundary $line 0]
1521    if {$offset == -1} then {
1522	lappend tab(_error) "Invalid form-data encoding (no first boundary)"
1523	return 0
1524    }
1525    set blen [string length $boundary]
1526
1527    incr offset $blen
1528
1529    #
1530    # Invariants de boucle
1531    #  - offset = indice juste après le délimiteur (qui correspond soit à
1532    #			"\r\n", soit à "--\r\n" si c'est le dernier)
1533    #  - retval = 1 si aucune erreur ne s'est produite
1534    #
1535    #
1536
1537    set retval 1
1538    while {[set next [string first $boundary $line $offset]] != -1} {
1539	# - next = indice du délimiteur suivant
1540
1541	#
1542	# Arrêt si le premier délimiteur correspond à une fin
1543	# d'arguments. Ce cas ne devrait jamais arriver, mais
1544	# il vaut mieux prévoir l'impossible...
1545	#
1546
1547	if {[string equal [string range $line $offset [expr $offset+1]] "--"]} then {
1548	    break
1549	}
1550
1551	# on saute le \r\n
1552	incr offset 2
1553
1554	#
1555	# Séparation de l'en-tête et du corps
1556	#
1557
1558	set sephdr [string first "\r\n\r\n" $line $offset]
1559	set entete [string range $line $offset [expr $sephdr-1]]
1560
1561	set r [::webapp::get-mime-part tab \
1562			[string range $line $offset [expr $sephdr-1]] \
1563			[string range $line [expr $sephdr+4] [expr $next-3]] \
1564		    ]
1565	if {$r == 0} then {
1566	    lappend tab(_error) "Invalid form-data encoding of subpart"
1567	    set retval 0
1568	}
1569
1570	set offset [expr $next + $blen]
1571    }
1572
1573    return $retval
1574}
1575
1576proc ::webapp::recuperer-form {formtab param} {
1577    global env
1578    upvar $formtab tab
1579
1580    if {! [info exists env(REQUEST_METHOD)]} then {
1581	lappend tab(_error) "non existant REQUEST_METHOD"
1582	return 0
1583    }
1584    if {! [string equal $env(REQUEST_METHOD) "POST"]} then {
1585	lappend tab(_error) "invalid method '$env(REQUEST_METHOD)'"
1586	return 0
1587    }
1588
1589    #
1590    # Traitement de content-type
1591    #
1592
1593    if {[info exists env(CONTENT_TYPE)]} then {
1594	set type $env(CONTENT_TYPE)
1595    } else {
1596	#
1597	# Cas particulier du browser de KDE 1 : si
1598	# CONTENT_TYPE est vide, c'est implicitement
1599	# "application/x-www-form-urlencoded".
1600	#
1601	set type application/x-www-form-urlencoded
1602    }
1603
1604    switch -glob -- $type {
1605	application/x-www-form-urlencoded	{
1606	    set r [::webapp::x-www-form-urlencoded tab]
1607	}
1608	multipart/form-data* {
1609	    set r [::webapp::form-data tab $type]
1610	}
1611	default {
1612	    lappend tab(_error) "invalid CONTENT_TYPE '$env(CONTENT_TYPE)'"
1613	    set r 0
1614	}
1615    }
1616
1617    #
1618    # On a lu quelque chose
1619    #
1620
1621    return $r
1622}
1623
1624#
1625# Convertit une chaîne (données d'un formulaire) en caractères
1626# "normaux"
1627#
1628# Entrée :
1629#   - paramètres :
1630#	- str : la chaîne à convertir
1631# Sortie :
1632#   - valeur de retour : la chaîne convertie
1633#
1634# Historique
1635#   1994/08/xx : pda : conception et codage
1636#   1999/02/25 : pda : documentation
1637#   2001/02/28 : pda : remplacement des \r\n par \n
1638#   2010/10/28 : pda : simplification du décodage, merci Tcl
1639#
1640
1641proc ::webapp::unpost-string {str} {
1642    #
1643    # Remplace tous les espaces
1644    #
1645    regsub -all "\\+" $str " " str
1646
1647    #
1648    # Remplace tous les %xx par le caractère correspondant
1649    #
1650
1651    set pos 0
1652    while {[set pos [string first "%" $str $pos]] > -1} {
1653        set code [scan [string range $str $pos+1 $pos+2] "%x"]
1654        set str [string replace $str $pos $pos+2 [format "%c" $code]]
1655	incr pos
1656    }
1657
1658    set new [encoding convertfrom utf-8 $str]
1659
1660    #
1661    # Nettoyage des mauvais caractères de fin de ligne
1662    #
1663    regsub -all -- "\r\n" $new "\n" new
1664    regsub -all -- "\r" $new "\n" new
1665
1666    return $new
1667}
1668
1669#
1670# Convertit une chaîne contenant éventuellement des caractères spéciaux
1671# HTML en chaîne dans laquelle les caractères spéciaux sont remplacés
1672# par des caractères "%.."
1673#
1674# Entrée :
1675#   - paramètres :
1676#	- str : la chaîne à convertir
1677# Sortie :
1678#   - valeur de retour : la chaîne convertie
1679#
1680# Historique
1681#   1999/11/01 : pda : conception
1682#
1683
1684proc ::webapp::post-string {str} {
1685    #
1686    # Remplace tous les caractères spéciaux
1687    #
1688    regsub -all {%}  $str "%25" str
1689    regsub -all {\+} $str "%2B" str
1690    regsub -all {\&} $str "%26" str
1691    regsub -all "\n" $str "%0A" str
1692    regsub -all "\r" $str "%0D" str
1693    regsub -all {\<} $str "%3C" str
1694    regsub -all {=}  $str "%3D" str
1695    regsub -all {\>} $str "%3E" str
1696    regsub -all {\?} $str "%3F" str
1697    regsub -all {"}  $str "%22" str
1698    regsub -all {"}  $str "%22" str
1699    regsub -all { }  $str "%20" str
1700
1701    return $str
1702}
1703
1704#
1705# Convertit une chaîne contenant éventuellement des caractères spéciaux
1706# HTML en chaîne dans laquelle les caractères spéciaux sont remplacés
1707# par des caractères "&...;"
1708#
1709# Entrée :
1710#   - paramètres :
1711#	- str : la chaîne à convertir
1712# Sortie :
1713#   - valeur de retour : la chaîne convertie
1714#
1715# Historique
1716#   1999/11/02 : pda : conception
1717#
1718
1719proc ::webapp::html-string {str} {
1720    #
1721    # Remplace tous les caractères spéciaux
1722    #
1723    regsub -all {\&} $str {\&amp;} str
1724    regsub -all {\<} $str {\&lt;} str
1725    regsub -all {\>} $str {\&gt;} str
1726    regsub -all {"}  $str {\&quot;} str
1727
1728    return $str
1729}
1730
1731#
1732# Import form variables in individual Tcl variables
1733#
1734# Entrée :
1735#   - paramètres :
1736#	- _ftab : tableau, passé par référence, contenant les valeurs
1737#	    des paramètres fournis au formulaire (see get-data)
1738#	- fspec : optional form specification (see get-data)
1739# Sortie :
1740#   - variables nommées par formtab : initialisées
1741#   - valeur de retour : none
1742#
1743# Historique :
1744#   2006/08/29 : pda : conception et codage
1745#   2010/12/16 : pda : add fspec
1746#
1747
1748proc ::webapp::import-vars {_ftab {fspec {}}} {
1749    upvar $_ftab ftab
1750
1751    if {[llength $fspec] == 0} then {
1752	foreach varname [array names ftab] {
1753	    upvar $varname var
1754	    set var $ftab($varname)
1755	}
1756    } else {
1757	# keep max number of occurrence to make a single value or a list
1758	# while at here, keep a log of found specifiers
1759	foreach s $fspec {
1760	    lassign $s re min max def
1761	    set m($re) $max
1762	    set found($re) 0
1763	}
1764	foreach varname [array names ftab] {
1765	    foreach s $fspec {
1766		lassign $s re min max def
1767		if {[regexp "^$re\$" $varname]} then {
1768		    set found($re) 1
1769		    upvar $varname var
1770		    set val $ftab($varname)
1771		    if {$m($re) <= 1} then {
1772			set val [string trim [lindex $val 0]]
1773		    }
1774		    set var $val
1775		    break
1776		}
1777	    }
1778	}
1779	foreach s $fspec {
1780	    lassign $s re min max def
1781	    if {! $found($re)} then {
1782		upvar $re var
1783		set var {}
1784	    }
1785	}
1786    }
1787}
1788
1789##############################################################################
1790# Mail et adresses électroniques
1791##############################################################################
1792
1793#
1794# Vérifie si une adresse électronique est valide,
1795# c'est à dire si elle vérifie les conditions suivantes :
1796# - présence de "@"
1797# - absence d'espace et de tabulations
1798#
1799# Entrée :
1800#   - paramètres :
1801#	- email : adresse électronique telle que saisie par l'utilisateur
1802# Sortie :
1803#   - valeur de retour : 0 (adresse incorrecte) ou 1 (adresse correcte)
1804#
1805# Historique
1806#   1994/08/xx : pda : conception et codage
1807#   1999/02/25 : pda : documentation
1808#
1809
1810proc ::webapp::valid-email {email} {
1811    set email [string trim $email]
1812
1813    if {[string first "@" $email] == -1} then { return 0 }
1814    if {[string first " " $email] != -1} then { return 0 }
1815    if {[string first "\t" $email] != -1} then { return 0 }
1816    return 1
1817}
1818
1819#
1820# Envoi d'un mail
1821#
1822# Entrée :
1823#   - paramètres :
1824#	- from : l'émetteur
1825#	- replyto : le destinataire des réponses
1826#	- to : le ou les destinataires
1827#	- cc : le ou les destinataires, si besoin est
1828#	- bcc : destinataire caché, si besoin est
1829#	- subject : le sujet
1830#	- texte : le texte
1831#	- type : le type du mail, par défaut 'text/plain; charset="utf-8"'
1832#
1833# Sortie :
1834#   - valeur de retour : aucune
1835#
1836# Historique :
1837#   2003/09/29 : pda : conception et codage
1838#   2009/02/23 : pda : ajout paramètre optionnel type
1839#
1840
1841proc ::webapp::mail {from replyto to cc bcc subject texte {type {}}} {
1842    set fd [open "|$::webapp::sendmail" "w"]
1843
1844    set to [join $to ", "]
1845    puts $fd "From: $from"
1846    puts $fd "To: $to"
1847
1848    if {! [string equal $cc ""]} then {
1849	puts $fd "Cc: $cc"
1850    }
1851    if {! [string equal $bcc ""]} then {
1852	puts $fd "Bcc: $bcc"
1853    }
1854    if {! [string equal $replyto ""]} then {
1855	puts $fd "Reply-to: $replyto"
1856    }
1857    if {[string equal $type ""]} then {
1858	set type {text/plain; charset="utf-8"}
1859    }
1860    if {! [string is ascii $subject]} then {
1861	set subject [::mime::word_encode "utf-8" "quoted-printable" $subject]
1862    }
1863    puts $fd "Subject: $subject"
1864    puts $fd "Mime-Version: 1.0"
1865    puts $fd "Content-Type: $type"
1866    puts $fd "Content-Transfer-Encoding: 8bit"
1867    puts $fd ""
1868    puts $fd $texte
1869    close $fd
1870}
1871
1872##############################################################################
1873# Génération d'une page HTML par substitution dans une page existante
1874##############################################################################
1875
1876#
1877# Substitue, dans un fichier, des motifs par des valeurs calculées
1878# par le script CGI.
1879#
1880# Entrée :
1881#   - paramètres :
1882#	- fichier : le nom du fichier servant de base pour la substitution
1883#	- subst : liste de susbtitutions, de la forme
1884#		{{motif valeur} {motif valeur} ...}
1885#	- encoding: name of encoding
1886# Sortie :
1887#   - valeur de retour : le fichier susbtitué
1888#
1889# Historique
1890#   1999/03/25 : pda : conception et codage
1891#   1999/11/02 : pda : suppression de & comme caractère spécial
1892#   2002/05/12 : pda : suppression de \ comme caractère spécial
1893#   2010/10/16 : pda : add encoding parameter
1894#   2010/12/22 : pda/jean : encoding defaults to utf-8
1895#
1896
1897proc ::webapp::file-subst {fichier subst {encoding utf-8}} {
1898    set fd [open $fichier r]
1899    if {$encoding ne ""} then {
1900	fconfigure $fd -encoding $encoding
1901    }
1902    set string [read $fd]
1903    close $fd
1904
1905    foreach l $subst {
1906	set motif  [lindex $l 0]
1907	set valeur [lindex $l 1]
1908
1909	regsub -all {\\} $valeur {\\&} valeur
1910	regsub -all {\&} $valeur {\\&} valeur
1911
1912	regsub -all -- $motif $string $valeur string
1913    }
1914    return $string
1915}
1916
1917##############################################################################
1918# Gestion des sessions
1919##############################################################################
1920
1921#
1922# Récupère une chaîne aléatoire (ou pseudo-aléatoire)
1923#
1924# Entrée :
1925#   - paramètres : -
1926# Sortie :
1927#   - valeur de retour : une chaîne de 20 chiffres
1928#
1929# Historique
1930#   1999/07/14 : pda : conception
1931#
1932
1933proc ::webapp::random {} {
1934    set rand ""
1935
1936    append rand [format "%03d" [expr [clock clicks] % 1000]]
1937    # rand contains now 3 digits
1938
1939    append rand [format "%05d" [pid]]
1940    # rand contains now 8 digits
1941
1942    # %d = day of month 01..31
1943    # %H = hour 00..23
1944    # %j = day of the year 001..366
1945    # %M = minute 00..59
1946    # %S = second 00..59
1947    # %w = weekday 0..6
1948    append rand [clock format [clock seconds] -format "%d%H%j%M%S%w"]
1949    # rand contains now 20 digits
1950
1951    return $rand
1952}
1953
1954##############################################################################
1955# Sortie d'une page Web ou autre
1956##############################################################################
1957
1958#
1959# Sort une page Web ou autre
1960#
1961# Entrée :
1962#   - paramètres :
1963#	- type : le type de sortie, html ou pdf
1964#	- page : la page (en html si html, en latex si pdf)
1965#	- fichier : nom de fichier à renvoyer
1966# Sortie :
1967#   - envoi direct sur la sortie standard
1968#
1969# Historique
1970#   2002/05/20 : pda : conception
1971#   2002/06/21 : pda : ajout de types
1972#   2002/10/24 : pda : ajout de la sortie csv
1973#   2008/02/27 : jean/zamboni : gestion des extensions de nom de fichiers
1974#
1975
1976proc ::webapp::send {type page {fichier "output"}} {
1977
1978    #
1979    # Détermine l'extension du fichier
1980    #
1981    switch -- $type {
1982	rawpdf	{ set ext "pdf" }
1983	jpeg 	{ set ext "jpg" }
1984	default { set ext $type }
1985    }
1986
1987    #
1988    # on rajoute une extension au nom de fichier si necessaire
1989    #
1990    if {! [regexp "\.$ext\$" $fichier] } then {
1991	append fichier "." $ext
1992    }
1993
1994    switch -- $type {
1995	html 	{ ::webapp::sortie-html $page }
1996	csv	{ ::webapp::sortie-csv $page $fichier }
1997	png 	{ ::webapp::sortie-bin image/png $page $fichier }
1998	gif 	{ ::webapp::sortie-bin image/gif $page $fichier }
1999	jpeg 	{ ::webapp::sortie-bin image/jpeg $page $fichier }
2000	rawpdf 	{ ::webapp::sortie-bin application/pdf $page $fichier }
2001	pdf 	{ ::webapp::sortie-latex $page $fichier }
2002    }
2003}
2004
2005#
2006# Sort une page Web ou autre
2007#
2008# Entrée :
2009#   - paramètres :
2010#	- page : la page HTML, sans le content-type
2011# Sortie :
2012#   - envoi direct sur la sortie standard
2013#
2014# Historique
2015#   2001/10/20 : pda : conception et codage
2016#   2014/03/28 : pda/jean : add cookies
2017#
2018
2019proc ::webapp::sortie-html {page} {
2020    fconfigure stdout -encoding utf-8
2021    puts stdout "Content-type: text/html; charset=utf-8"
2022    http-send-cookies
2023    puts stdout ""
2024    puts stdout $page
2025}
2026
2027#
2028# Sort un fichier CSV
2029#
2030# Entrée :
2031#   - paramètres :
2032#	- page : le fichier CSV, sans le content-type
2033#	- fichier : nom de fichier à renvoyer
2034# Sortie :
2035#   - envoi direct sur la sortie standard
2036#
2037# Historique
2038#   2002/10/24 : pda : conception et codage
2039#   2008/02/27 : jean/zamboni : Content-type et filename
2040#   2014/03/28 : pda/jean : add cookies
2041#
2042
2043proc ::webapp::sortie-csv {page fichier} {
2044    fconfigure stdout -encoding utf-8
2045    puts stdout "Content-type: text/csv; charset=utf-8"
2046    puts stdout "Content-Disposition: attachment; filename=$fichier"
2047    http-send-cookies
2048    puts stdout ""
2049    puts stdout $page
2050}
2051
2052#
2053# Sort un document binaire
2054#
2055# Entrée :
2056#   - paramètres :
2057#	- type : type MIME
2058#	- page : le fichier
2059#	- fichier : nom de fichier à renvoyer
2060# Sortie :
2061#   - envoi direct sur la sortie standard
2062#
2063# Historique
2064#   2002/05/21 : pda : conception et codage
2065#   2008/02/27 : jean/zamboni : ajout filename
2066#   2014/03/28 : pda/jean : add cookies
2067#
2068
2069proc ::webapp::sortie-bin {type page fichier} {
2070    puts stdout "Content-type: $type"
2071    puts stdout "Content-Disposition: attachment; filename=$fichier"
2072    http-send-cookies
2073    puts stdout ""
2074    flush stdout
2075    fconfigure stdout -translation binary
2076    puts -nonewline stdout $page
2077}
2078
2079
2080#
2081# Sort un document latex compilé en pdf
2082#
2083# Entrée :
2084#   - paramètres :
2085#	- page : le source latex, prêt à être compilé
2086#	- fichier : nom de fichier à renvoyer
2087#   - variable globale debuginfos : valeur latexfiles
2088# Sortie :
2089#   - envoi direct sur la sortie standard
2090#
2091# Historique
2092#   2002/05/11 : pda : conception et codage
2093#   2002/05/12 : pda : ajout de debuginfos
2094#   2008/02/27 : jean/zamboni : ajout filename
2095#   2012/01/09 : pda : encoding to utf8
2096#
2097
2098proc ::webapp::sortie-latex {page fichier} {
2099    global errorInfo
2100
2101    if {[lsearch $::webapp::debuginfos latexsource] != -1} then {
2102	::webapp::sortie-html \
2103	    "<PRE>$page</PRE>"
2104	return
2105    }
2106
2107    #
2108    # Le changement de répertoire est nécessaire car latex dépose
2109    # des fichiers .aux, .log et .pdf dans le répertoire courant.
2110    #
2111
2112    cd $::webapp::tmpdir
2113
2114    #
2115    # Nommage des fichiers utilisés. Le répertoire est absolu,
2116    # c'est plus clair dans les messages d'erreur.
2117    #
2118
2119    set prefix $::webapp::tmpdir/arrgen[pid]
2120    set texfile "${prefix}.tex"
2121    set pdffile "${prefix}.pdf"
2122    set auxfile "${prefix}.aux"
2123    set logfile "${prefix}.log"
2124
2125    #
2126    # Envoi du source latex dans le fichier
2127    #
2128
2129    if {[catch {set fd [open $texfile "w"]} m]} then {
2130	::webapp::sortie-html \
2131	    "Impossible de créer '$texfile': <PRE>$errorInfo</PRE>"
2132	return
2133    }
2134    fconfigure $fd -encoding utf-8
2135    puts $fd $page
2136    close $fd
2137
2138    #
2139    # Génération du fichier pdf
2140    #
2141
2142    if {[catch {set log [exec $::webapp::pdflatex $texfile]} msg]} then {
2143	::webapp::sortie-html \
2144	    "Impossible de générer '$pdffile': <PRE>$errorInfo</PRE>"
2145	return
2146    }
2147
2148    #
2149    # Sortie du résultat
2150    #
2151
2152    if {[catch {set fd [open $pdffile "r"]} m]} then {
2153	::webapp::sortie-html \
2154	    "Impossible de lire '$pdffile': <PRE>$errorInfo</PRE>"
2155	return
2156    }
2157    fconfigure $fd -translation binary
2158    set pdf [read $fd]
2159    close $fd
2160
2161    puts stdout "Content-Type: application/pdf"
2162    puts stdout "Content-Disposition: attachment; filename=$fichier"
2163    http-send-cookies
2164    puts stdout ""
2165    flush stdout
2166    fconfigure stdout -translation binary
2167    puts -nonewline stdout $pdf
2168
2169    #
2170    # Effacement des fichiers temporaires
2171    #
2172
2173    if {[lsearch $::webapp::debuginfos latexfiles] == -1} then {
2174	file delete -force -- $texfile $pdffile $auxfile $logfile
2175    }
2176}
2177
2178#
2179# Sort une redirection
2180#
2181# Entrée :
2182#   - paramètres :
2183#	- url : redirect url (or relative path)
2184# Sortie :
2185#   - envoi direct sur la sortie standard
2186#
2187# Historique
2188#   2015/02/18 : pda/jean : creation
2189#
2190
2191proc ::webapp::redirect {url} {
2192    fconfigure stdout -encoding utf-8
2193    puts stdout "Location: $url"
2194    http-send-cookies
2195    puts stdout ""
2196}
2197
2198##############################################################################
2199# Sortie des erreurs dans une belle page Web
2200##############################################################################
2201
2202#
2203# Sortie des erreurs dans une belle page Web
2204#
2205# Entrée :
2206#   - paramètres :
2207#	- page : fichier contenant la page HTML à trous
2208#	- msg : le message d'erreur
2209# Sortie : pas de sortie, la procédure fait un exit.
2210#
2211# Historique
2212#   2000/07/26 : pda     : conception
2213#   2000/07/27 : pda     : documentation
2214#   2001/10/20 : pda     : utilisation de la procédure de sortie
2215#   2002/12/26 : pda     : mise en package
2216#   2003/12/11 : pda     : ajout du traitement de \n
2217#
2218
2219proc ::webapp::error-exit {page msg} {
2220    set msg [::webapp::html-string $msg]
2221    regsub -all "\n" $msg "<br>" msg
2222    ::webapp::send html [::webapp::file-subst $page \
2223				    [list [list %MESSAGE% $msg] \
2224					] \
2225				]
2226    exit 0
2227}
2228
2229##############################################################################
2230# Des fois, il faut bien avoir recours aux dernières extrémités...
2231##############################################################################
2232
2233#
2234# Affiche tous les paramètres fournis au script CGI.
2235#
2236# Entrée : tout l'environnement d'un script CGI
2237# Sortie :
2238#   - envoi direct
2239#
2240# Historique
2241#   1999/03/25 : pda : conception et codage
2242#
2243
2244proc ::webapp::cgidebug {} {
2245    global env argv
2246
2247    puts "Content-type: text/html"
2248    http-send-cookies
2249    puts ""
2250
2251    puts "<TITLE>Debug information</TITLE>"
2252    puts "<H1>Debug information</H1>"
2253
2254    set pwd [exec pwd]
2255    puts "Working directory = $pwd <P>"
2256
2257    puts "Parameters : <P>"
2258    set n 0
2259    puts "<UL>"
2260    foreach i $argv {
2261	incr n
2262	puts "<LI> arg $n = /$i/"
2263    }
2264    puts "</UL>"
2265
2266    puts "Environment : <P>"
2267    puts "<UL>"
2268    foreach i [lsort [array names env]] {
2269	puts "<LI> $i=$env($i)"
2270    }
2271    puts "</UL>"
2272
2273    if {[info exists env(CONTENT_LENGTH)]} then {
2274	puts "Standard input : <P>"
2275	puts "<CODE>"
2276	puts [read stdin $env(CONTENT_LENGTH)]
2277	puts "</CODE>"
2278    }
2279}
2280
2281##############################################################################
2282# Protéger l'accès à des applications
2283##############################################################################
2284
2285#
2286# Teste l'existence d'un fichier et interdit l'accès à
2287# l'application si le fichier existe.
2288#
2289# Entrée :
2290#   - paramètres :
2291#	- ftest : fichier à tester, contenant le message d'interdiction
2292#	- lusers : liste d'utilisateurs autorisés à accéder quand même
2293#	- ferr : fichier HTML à trou (%MESSAGE% = message d'interdiction)
2294#   - variables d'environnement :
2295#	- REMOTE_USER : une chaîne de la forme "login""
2296# Sortie :
2297#   - envoi direct, ou rien du tout
2298#
2299# Historique
2300#   1999/03/25 : pda : conception et codage
2301#   1999/06/21 : pda : fin de la conception
2302#
2303
2304proc ::webapp::nologin {ftest lusers ferr} {
2305    set user [::webapp::user]
2306    if {[file exists $ftest]} then {
2307	if {[string equal $user ""] || [lsearch -exact $lusers $user] == -1} then {
2308	    set fd [open $ftest r]
2309	    set message [read $fd]
2310	    close $fd
2311
2312	    ::webapp::send html [::webapp::file-subst $ferr \
2313					[list \
2314						[list %MESSAGE% $message] \
2315					    ] \
2316				    ]
2317	    exit 0
2318	}
2319    }
2320}
2321
2322##############################################################################
2323# Une interface agréable pour la programmation des scripts CGI
2324##############################################################################
2325
2326proc ::webapp::cgi-env {} {
2327}
2328
2329proc ::webapp::cgi-get {} {
2330}
2331
2332proc ::webapp::cgi-err {msg debug} {
2333    global argv
2334
2335    set script [::webapp::script-name]
2336    set date   [clock format [clock seconds]]
2337
2338    set page ""
2339    append page "<HTML>\n"
2340    append page "<HEAD><TITLE>Error !</TITLE></HEAD>\n"
2341    append page "<BODY TEXT=#000000 BGCOLOR=#FFFFFF>\n"
2342    append page "<FONT FACE=\"Arial,Helvetica\">\n"
2343    append page "<H1>Internal error!</H1>\n"
2344
2345    if {$debug} then {
2346	set pwd    [exec pwd]
2347
2348	append page "Error detected in script '$script'\n"
2349	append page "on '$date'&nbsp;:\n"
2350	append page "<HR>\n"
2351	append page "<PRE>[::webapp::html-string $msg]</PRE>\n"
2352	append page "<HR>\n"
2353
2354	append page "<H2>Context</H2>\n"
2355	append page "Directory = $pwd<P>\n"
2356
2357	append page "Parameters&nbsp;:<BR>\n"
2358	set n 0
2359	append page "<UL>\n"
2360	foreach i $argv {
2361	    incr n
2362	    append page "<LI> arg $n = /[::webapp::html-string $i]/\n"
2363	}
2364	append page "</UL>\n"
2365
2366	append page "Environment&nbsp;:<BR>\n"
2367	append page "<UL>\n"
2368	foreach i [lsort [array names env]] {
2369	    append page "<LI> $i=[::webapp::html-string $env($i)]\n"
2370	}
2371	append page "</UL>\n"
2372
2373	if {[info exists env(CONTENT_LENGTH)]} then {
2374	    append page "Standard input&nbsp;: <P>\n"
2375	    append page "<CODE>\n"
2376	    append page [::webapp::html-string [read stdin $env(CONTENT_LENGTH)]]
2377	    append page "</CODE>\n"
2378	}
2379    } else {
2380	append page "Error detected in script&nbsp;:\n"
2381	append page "<UL>\n"
2382	append page "<LI> on '$date'\n"
2383	append page "<LI> in '$script'\n"
2384	append page "</UL>\n"
2385	append page "Please contact your Netmagis administrator\n"
2386	append page "and send her/him a copy of this message.\n"
2387
2388	puts stderr "\[$date\] webapp/$script: $msg"
2389    }
2390    append page "</BODY></HTML>\n"
2391
2392    ::webapp::send html $page
2393}
2394
2395#
2396# Lance l'exécution d'un script CGI
2397#
2398# Entrée :
2399#   - tout l'environnement d'un script CGI
2400#   - paramètres :
2401#	- script : nom du script à exécuter, avec paramètres éventuels
2402#	- debug : 1 s'il faut sortir l'environnement, ou 0 pour un simple message
2403# Sortie :
2404#   - envoi direct
2405#
2406# Historique
2407#   2001/06/20 : pda : conception
2408#
2409
2410proc ::webapp::cgi-exec {script {debug 0}} {
2411    global errorInfo
2412
2413    ::webapp::cgi-env
2414    if [catch $script msg] then {
2415	# on n'utilise pas msg, car errorInfo le contient déjà
2416	::webapp::cgi-err $errorInfo $debug
2417    }
2418    exit 0
2419}
2420
2421#
2422# Classe "utilisateur dans la base d'authentification"
2423#
2424# Représente les attributs d'un utilisateur tel qu'il est stocké
2425# dans la base d'authentification (PostgreSQL ou LDAP) sous une
2426# forme unifiée.
2427#
2428# Options :
2429#   aucune
2430#
2431# Méthodes
2432#   get	    : récupère la valeur (unique) d'un attribut
2433#   set	    : modifie la valeur d'un attribut (en mémoire uniquement).
2434#	      C'est une méthode utilisée uniquement par la classe authbase
2435#   exists  : indique si l'utilisateur a été trouvé dans la base.
2436#
2437# Historique
2438#   2007/10/05 : pda/jean : intégration et documentation
2439#
2440
2441snit::type ::webapp::authuser {
2442    variable exists 0
2443    variable attrvals -array {}
2444
2445    method exists {{value {}}} {
2446	if {$value ne ""} then {
2447	    set exists $value
2448	}
2449	return $exists
2450    }
2451
2452    method get {attr} {
2453	if {[info exists attrvals($attr)]} then {
2454	    set v $attrvals($attr)
2455	} else {
2456	    set v ""
2457	}
2458	return $v
2459    }
2460
2461    method set {attr val} {
2462	set attrvals($attr) $val
2463    }
2464}
2465
2466#
2467# Classe "base d'authentification"
2468#
2469# Représente une base d'authentification et donne les moyens
2470# de récupérer les attributs d'un utilisateur
2471#
2472# Options :
2473#   method  : "ldap" ou "postgresql"
2474#   db	    : paramètres d'accès à la base d'authentification (cf. ci-dessous)
2475#   attrmap : traduction d'attribut
2476#
2477# Méthodes
2478#   getuser : recherche l'utilisateur par son login et récupère ses attributs
2479#
2480# Historique
2481#   2007/10/05 : pda/jean : intégration et documentation
2482#
2483
2484snit::type ::webapp::authbase {
2485
2486    # Option method: ldap, postgresql, opened-postgresql
2487    option -method  -default "none"
2488
2489    # Option db :
2490    #   pour ldap:
2491    #	  url ...
2492    #	  [ binddn ... ]
2493    #	  [ bindpw ... ]
2494    #	  base ...
2495    #	  searchuid ... (filtre avec un %s pour le login)
2496    #   pour postgresql:
2497    #	  host=...
2498    #	  dbname=...
2499    #	  user=...
2500    #	  password=...
2501    #	pour opened-postgresql:
2502    #	  handle
2503    option -db      -default {}
2504
2505    # Option attrmap :
2506    # liste de couples
2507    #	<nom dans ce module> <nom dans la base>
2508    option -attrmap -default {
2509	login     login
2510	password  password
2511	lastname  lastname
2512	firstname firstname
2513	mail      mail
2514	phone     phone
2515	mobile    mobile
2516	fax       fax
2517	addr      addr
2518    }
2519
2520    variable connected "no"
2521    variable handle
2522
2523    destructor {
2524	if {$connected} then {
2525	    Disconnect $selfns
2526	}
2527    }
2528
2529    method getuser {login u} {
2530	if {! $connected} then {
2531	    Connect $selfns
2532	}
2533
2534	$u exists 0
2535	set n 0
2536
2537	switch $options(-method) {
2538	    opened-postgresql -
2539	    postgresql {
2540		set qlogin [::pgsql::quote $login]
2541		set sql "SELECT * FROM pgauth.user WHERE login = '$qlogin'"
2542		set av {}
2543		pg_select $handle $sql tab {
2544		    set av [array get tab]
2545		    incr n
2546		}
2547	    }
2548	    ldap {
2549		array set dbopt $options(-db)
2550		set base   $dbopt(base)
2551		set search $dbopt(searchuid)
2552
2553		# XXXXXXXXX  Il faut quoter le login
2554		set filter [format $search $login]
2555
2556		set e [::ldapx::entry create %AUTO%]
2557		set n [$handle read $base $filter $e]
2558
2559		set av {}
2560		if {$n == 1} then {
2561		    #
2562		    # On ne garde que la première valeur des champs multivalués
2563		    #
2564
2565		    array set x [$e getall]
2566		    foreach i [array names x] {
2567			set x($i) [lindex $x($i) 0]
2568		    }
2569		    set av [array get x]
2570		}
2571
2572		$e destroy
2573	    }
2574	    default {
2575		error "Auth method '$options(-method)' not supported"
2576	    }
2577	}
2578
2579	if {$av ne ""} then {
2580	    $u exists 1
2581	    array set t $av
2582	    foreach {cmod cbase} [string tolower $options(-attrmap)] {
2583		set v {}
2584		foreach c $cbase {
2585		    if {[info exists t($c)]} then {
2586			lappend v $t($c)
2587		    }
2588		    $u set $cmod [join $v ", "]
2589		}
2590	    }
2591	}
2592
2593	return $n
2594    }
2595
2596    proc Connect {selfns} {
2597	set db $options(-db)
2598	switch $options(-method) {
2599	    opened-postgresql {
2600		set handle $db
2601	    }
2602	    postgresql {
2603		if {[catch {set handle [pg_connect -conninfo $db]} msg]} then {
2604		    error $msg
2605		}
2606	    }
2607	    ldap {
2608		array set dbopt $db
2609
2610		if {! [info exists dbopt(url)]} then {
2611		    error "url not configured for LDAP method"
2612		} else {
2613		    set url $dbopt(url)
2614		}
2615		if {[info exists dbopt(binddn)] && [info exists dbopt(bindpw)]} then {
2616		    set binddn $dbopt(binddn)
2617		    set bindpw $dbopt(bindpw)
2618		} else {
2619		    set binddn ""
2620		    set bindpw ""
2621		}
2622
2623		set handle [::ldapx::ldap create %AUTO%]
2624		if {! [$handle connect $url $binddn $bindpw]} then {
2625		    error [$handle error]
2626		}
2627	    }
2628	    none {
2629		error "Auth method not configured"
2630	    }
2631	    default {
2632		error "Auth method '$options(-method)' not supported"
2633	    }
2634	}
2635	set connected 1
2636    }
2637
2638    proc Disconnect {selfns} {
2639	switch $options(-method) {
2640	    opened-postgresql {
2641		# nothing
2642	    }
2643	    postgresql {
2644		if {[catch {pg_disconnect $handle} msg]} then {
2645		    error $msg
2646		}
2647	    }
2648	    ldap {
2649		if {! [$handle disconnect]} then {
2650		    error [$handle error]
2651		}
2652		$handle destroy
2653	    }
2654	    default {
2655		error "Auth method '$options(-method)' not supported"
2656	    }
2657	}
2658	set connected 0
2659    }
2660}
2661
2662##############################################################################
2663# Cookie management
2664##############################################################################
2665
2666# Input:
2667#   - name: cookie name (printable ascii chars, excluding [,; =])
2668#   - val: cookie value (printable ascii chars, excluding [,; ])
2669#   - expire: unix timestamp, or 0 if no expiration date
2670#   - path:
2671#   - domain:
2672#   - secure:
2673#   - httponly:
2674# Output: none
2675#
2676# History:
2677#   2014/03/28 : pda/jean : design
2678
2679proc ::webapp::set-cookie {name val expire path domain secure httponly} {
2680    global wcooktab
2681
2682    set l {}
2683
2684    lappend l "$name=$val"
2685    if {$expire > 0} then {
2686	# Wdy, DD Mon YYYY HH:MM:SS GMT
2687	set max [clock format $expire -gmt yes -format "%a, %d %b %Y %T GMT"]
2688	lappend "Expires=$max"
2689    }
2690    if {$path ne ""} then {
2691	lappend "Path=$path"
2692    }
2693    if {$domain ne ""} then {
2694	lappend "Domain=$domain"
2695    }
2696    if {$secure} then {
2697	lappend "Secure"
2698    }
2699    if {$httponly} then {
2700	lappend "HttpOnly"
2701    }
2702
2703    set wcooktab($name) [join $l "; "]
2704}
2705
2706#
2707# Send cookies to the browser as part of HTTP protocol
2708#
2709# Input:
2710#   - global parameter wcooktab(): all cookies to return
2711# Output: none
2712#
2713# History:
2714#   2014/03/28 : pda/jean : design
2715#
2716
2717proc ::webapp::http-send-cookies {} {
2718    global wcooktab
2719
2720    foreach name [array names wcooktab] {
2721	puts stdout "Set-Cookie: $wcooktab($name)"
2722    }
2723}
2724
2725#
2726# Get a cookie (as returned by the browser) by its name
2727#
2728# Input:
2729#   - name: name of the cookie to get
2730# Output:
2731#   - return value: value of cookie or ""
2732#
2733# History:
2734#   2014/04/11 : pda/jean : design
2735#
2736
2737set ::webapp::gotcookies 0
2738
2739proc ::webapp::get-cookie {name} {
2740    global ::webapp::gotcookies
2741    global rcooktab
2742    global env
2743
2744    if {! $::webapp::gotcookies} then {
2745	if {[info exists env(HTTP_COOKIE)]} then {
2746	    foreach nv [split $env(HTTP_COOKIE) ";"] {
2747		if {[regexp {^\s*([^=]+)=(.*)} $nv bidon n v]} then {
2748		    set rcooktab($n) $v
2749		}
2750	    }
2751	}
2752	set ::webapp::gotcookies 1
2753    }
2754
2755    if {[info exists rcooktab($name)]} then {
2756	set v $rcooktab($name)
2757    } else {
2758	set v ""
2759    }
2760
2761    return $v
2762}
2763
2764
2765##############################################################################
2766# Log management
2767##############################################################################
2768
2769#
2770# Classe "systeme de log"
2771#
2772# Représente l'acces a un support de journaux
2773#
2774# Options :
2775#   method  : "postgresql", "file", "syslog"
2776#   medium  : paramètres
2777#   subsys  : nom générique de l'application
2778#
2779# Méthodes
2780#   log     : écrit un événement dans le journal
2781#
2782# Historique
2783#   2007/10/23 : pda/jean : intégration et documentation
2784#
2785
2786snit::type ::webapp::log {
2787
2788    # method: postgresql, file, syslog
2789    option -method  -default "none"
2790
2791    # medium for postgresql :
2792    #	host ...
2793    #	dbname ...
2794    #	table ...
2795    #	user ...
2796    #	password ...
2797    #   (table must contain the columns : date, subsys, event, login, ip, msg)
2798    # medium for opened-postgresql
2799    #   dbfd ...
2800    #   table ...
2801    # medium for file :
2802    #   file ...
2803    # medium for syslog :
2804    #   host ...
2805    #   facility ...
2806    #   priority ...
2807    option -medium      -default {}
2808
2809    # subsystem
2810    option -subsys -default "none"
2811
2812    variable handle ""
2813    variable table "log"
2814
2815    constructor {args} {
2816	$self configurelist $args
2817
2818	switch $options(-method) {
2819	    none {
2820		error "Wrong # args: should be -method ... -medium ..."
2821	    }
2822	    postgresql {
2823		array set x $options(-medium)
2824		set db {}
2825		foreach c {host dbname user password} {
2826		    if {[info exists x($c)]} then {
2827			lappend db "$c=$x($c)"
2828		    }
2829		}
2830		set db [join $db " "]
2831		if {[catch {set handle [pg_connect -conninfo $db]} msg]} then {
2832		    error "Cannot connect: $msg"
2833		}
2834		if {[info exists x(table)]} then {
2835		    set table $x(table)
2836		}
2837	    }
2838	    opened-postgresql {
2839		array set x $options(-medium)
2840		if {! [info exists x(db)]} then {
2841		    error "db is a mandatory parameter"
2842		}
2843		set handle $x(db)
2844		if {[info exists x(table)]} then {
2845		    set table $x(table)
2846		}
2847	    }
2848	    file {
2849		# XXX
2850	    }
2851	    syslog {
2852		# XXX
2853	    }
2854	    default {
2855		error "Unknown method '$options(-method)'"
2856	    }
2857	}
2858    }
2859
2860    destructor {
2861	switch $options(-method) {
2862	    opened-postgresql -
2863	    postgresql {
2864		pg_disconnect $handle
2865	    }
2866	    file {
2867	    }
2868	    syslog {
2869	    }
2870	    default {
2871		error "Unknown method '$options(-method)'"
2872	    }
2873	}
2874    }
2875
2876    method log {date event login ip msg} {
2877
2878	switch $options(-method) {
2879	    opened-postgresql -
2880	    postgresql {
2881		foreach c {event login ip msg} {
2882		    if {[string equal [set $c] ""]} then {
2883			set t($c) NULL
2884		    } else {
2885			set t($c) "'[::pgsql::quote [set $c]]'"
2886		    }
2887		}
2888		if {[string equal $date ""]} then {
2889		    set datecol ""
2890		    set dateval ""
2891		} else {
2892		    set datecol "date,"
2893		    if {[regexp {^\d+$} $date]} then {
2894			set dateval "to_timestamp($date),"
2895		    } else {
2896			set dateval "'[::pgsql::quote $date]',"
2897		    }
2898		}
2899		set t(subsys) "'[::pgsql::quote $options(-subsys)]'"
2900		set sql "INSERT INTO $table
2901				($datecol subsys, event, login, ip, msg)
2902			    VALUES (
2903				$dateval $t(subsys), $t(event), $t(login),
2904				    $t(ip), $t(msg))"
2905		if {! [::pgsql::execsql $handle $sql m]} then {
2906		    error "Cannot write log ($m)"
2907		}
2908	    }
2909	    file {
2910	    }
2911	    syslog {
2912	    }
2913	    default {
2914		error "Unknown method '$options(-method)'"
2915	    }
2916	}
2917    }
2918}
2919