Subversion Repositories eFlore/Applications.del

Rev

Rev 1521 | Go to most recent revision | Only display areas with differences | Ignore whitespace | Details | Blame | Last modification | View Log | RSS feed

Rev 1521 Rev 1595
1
<?php
1
<?php
2
/**
2
/**
3
 * DEL (Détermination en ligne [Pictoflora/Identiplante]) Toolkit
3
 * DEL (Détermination en ligne [Pictoflora/Identiplante]) Toolkit
4
 * Quelques fonctions utiles, utilisées et/ou utilisables aussi bien par images/*, observations/*
4
 * Quelques fonctions utiles, utilisées et/ou utilisables aussi bien par images/*, observations/*
5
 * et probablement d'autres, comme determination/*.
5
 * et probablement d'autres, comme determination/*.
6
 *
6
 *
7
 * Les domaines des fonctions tournent autour de 4 aspects:
7
 * Les domaines des fonctions tournent autour de 4 aspects:
8
 * - gestions des paramètres d'entrée utilisateurs, valeurs par défaut et sanitization
8
 * - gestions des paramètres d'entrée utilisateurs, valeurs par défaut et sanitization
9
 * - génération de SQL
9
 * - génération de SQL
10
 * - processing de tableau de pattern d'utilisation SQL assez commun
10
 * - processing de tableau de pattern d'utilisation SQL assez commun
11
 * - formattage basique de sortie (JSON)
11
 * - formattage basique de sortie (JSON)
12
 * + quelques helpers basiques
12
 * + quelques helpers basiques
13
 *
13
 *
14
 * @category	php 5.2
14
 * @category	php 5.2
15
 * @package		del
15
 * @package		del
16
 * @author		Raphaël Droz <raphael@tela-botanica.org>
16
 * @author		Raphaël Droz <raphael@tela-botanica.org>
17
 * @copyright	Copyright (c) 2013 Tela Botanica (accueil@tela-botanica.org)
17
 * @copyright	Copyright (c) 2013 Tela Botanica (accueil@tela-botanica.org)
18
 * @license	http://www.cecill.info/licences/Licence_CeCILL_V2-fr.txt Licence CECILL
18
 * @license	http://www.cecill.info/licences/Licence_CeCILL_V2-fr.txt Licence CECILL
19
 * @license	http://www.gnu.org/licenses/gpl.html Licence GNU-GPL
19
 * @license	http://www.gnu.org/licenses/gpl.html Licence GNU-GPL
20
 */
20
 */
21
 
21
 
22
 
22
 
23
define('_LISTE_OBS_MAX_RESULT_LIMIT', 1000);
23
define('_LISTE_OBS_MAX_RESULT_LIMIT', 1000);
24
define('_LISTE_OBS_MAX_ID_OBS', 10e7);
24
define('_LISTE_OBS_MAX_ID_OBS', 10e7);
25
// SELECT MAX(num_taxonomique) FROM bdtfx_v2_00;
25
// SELECT MAX(num_taxonomique) FROM bdtfx_v2_00;
26
define('_LISTE_OBS_MAX_BDTFX_NT', 1000000); // 44378 + 1000
26
define('_LISTE_OBS_MAX_BDTFX_NT', 1000000); // 44378 + 1000
27
// SELECT MAX(num_nom) FROM bdtfx_v2_00;
27
// SELECT MAX(num_nom) FROM bdtfx_v2_00;
28
define('_LISTE_OBS_MAX_BDTFX_NN', 1000000); // 120816 + 10000
28
define('_LISTE_OBS_MAX_BDTFX_NN', 1000000); // 120816 + 10000
29
 
29
 
30
class DelTk {
30
class DelTk {
31
    static $parametres_autorises = array(
31
    static $parametres_autorises = array(
32
        'masque', 'masque.famille', 'masque.nn', 'masque.referentiel', // taxon
32
        'masque', 'masque.famille', 'masque.nn', 'masque.referentiel', // taxon
33
        'masque.genre', 'masque.espece', 'masque.ns', // nom_sel
33
        'masque.genre', 'masque.espece', 'masque.ns', // nom_sel
34
        'masque.commune', 'masque.departement', 'masque.id_zone_geo', // loc
34
        'masque.commune', 'masque.departement', 'masque.id_zone_geo', // loc
35
        'masque.auteur', 'masque.date', 'masque.tag', 'masque.type', // autres
35
        'masque.auteur', 'masque.date', 'masque.tag', 'masque.type', // autres
36
        // tri, offset
36
        // tri, offset
37
        'navigation.depart', 'navigation.limite',
37
        'navigation.depart', 'navigation.limite',
38
        'tri', 'ordre', // TODO: 'total=[yes]', 'fields=[x,y,...]'
38
        'tri', 'ordre', // TODO: 'total=[yes]', 'fields=[x,y,...]'
39
        // TODO: masque.annee, masque.insee (!= departement)
39
        // TODO: masque.annee, masque.insee (!= departement)
40
    );
40
    );
41
 
41
 
42
    static $default_params = array(
42
    static $default_params = array(
43
        'navigation.depart' => 0, 'navigation.limite' => 10,
43
        'navigation.depart' => 0, 'navigation.limite' => 10,
44
        'tri' => 'date_transmission', 'ordre' => 'desc');
44
        'tri' => 'date_transmission', 'ordre' => 'desc');
45
 
45
 
46
 
46
 
47
    // input filtering
47
    // input filtering
48
 
48
 
49
 
49
 
50
    /* Construit un (vulgaire) abstract syntax tree:
50
    /* Construit un (vulgaire) abstract syntax tree:
51
       "AND" => [ "tag1", "tag2" ]
51
       "AND" => [ "tag1", "tag2" ]
52
       Idéalement (avec un parser simple comme proposé par http://hoa-project.net/Literature/Hack/Compiler.html#Langage_PP)
52
       Idéalement (avec un parser simple comme proposé par http://hoa-project.net/Literature/Hack/Compiler.html#Langage_PP)
53
       nous aurions:
53
       nous aurions:
54
       "AND" => [ "tag1", "tag2", "OR" => [ "tag3", "tag4" ] ]
54
       "AND" => [ "tag1", "tag2", "OR" => [ "tag3", "tag4" ] ]
55
 
55
 
56
       Ici nous devons traiter les cas suivants:
56
       Ici nous devons traiter les cas suivants:
57
       tags séparés par des "ET/AND OU/OR", séparés par des espaces ou des virgules.
57
       tags séparés par des "ET/AND OU/OR", séparés par des espaces ou des virgules.
58
       Mais la chaîne peut aussi avoir été issue du "masque général" (la barre de recherche générique).
58
       Mais la chaîne peut aussi avoir été issue du "masque général" (la barre de recherche générique).
59
       ce qui implique des comportement par défaut différents afin de préserver la compatibilité.
59
       ce qui implique des comportement par défaut différents afin de préserver la compatibilité.
60
 
60
 
61
       Théorie:
61
       Théorie:
62
       1) tags passés par "champ tag":
62
       1) tags passés par "champ tag":
63
       - support du ET/OU, et explode par virgule.
63
       - support du ET/OU, et explode par virgule.
64
       - si pas d'opérande détectée: "OU"
64
       - si pas d'opérande détectée: "OU"
65
 
65
 
66
       2) tags passés par "recherche générale":
66
       2) tags passés par "recherche générale":
67
       - support du ET/OU, et explode par whitespace.
67
       - support du ET/OU, et explode par whitespace.
68
       - si pas d'opérande détectée: "ET"
68
       - si pas d'opérande détectée: "ET"
69
 
69
 
70
       La présence de $additional_sep s'explique car ET/OU sous-entendent une séparation par des espaces.
70
       La présence de $additional_sep s'explique car ET/OU sous-entendent une séparation par des espaces.
71
       Mais ce n'est pas toujours pertinent car: 1) la compatibilité suggère de considérer parfois
71
       Mais ce n'est pas toujours pertinent car: 1) la compatibilité suggère de considérer parfois
72
       la virgule comme séparateur et 2) les tags *peuvent* contenir des espaces. Par conséquent:
72
       la virgule comme séparateur et 2) les tags *peuvent* contenir des espaces. Par conséquent:
73
       * a,b,c => "a" $default_op "b" $default_op "c"
73
       * a,b,c => "a" $default_op "b" $default_op "c"
74
       * a,b AND c => "a" AND "b" AND "c"
74
       * a,b AND c => "a" AND "b" AND "c"
75
       * a OR b AND c,d => "a" AND "b" AND "c" AND "d"
75
       * a OR b AND c,d => "a" AND "b" AND "c" AND "d"
76
       C'est à dire par ordre décroissant de priorité:
76
       C'est à dire par ordre décroissant de priorité:
77
       1) opérande contenu dans la chaîne
77
       1) opérande contenu dans la chaîne
78
       2) opérande par défaut
78
       2) opérande par défaut
79
       3) les séparateurs présents sont substitués par l'opérande déterminée par 1) ou 2)
79
       3) les séparateurs présents sont substitués par l'opérande déterminée par 1) ou 2)
80
 
80
 
81
       // TODO: support des parenthèses, imbrications & co: "(", ")"
81
       // TODO: support des parenthèses, imbrications & co: "(", ")"
82
       // http://codehackit.blogspot.fr/2011/08/expression-parser-in-php.html
82
       // http://codehackit.blogspot.fr/2011/08/expression-parser-in-php.html
83
       // http://blog.angeloff.name/post/2012/08/05/php-recursive-patterns/
83
       // http://blog.angeloff.name/post/2012/08/05/php-recursive-patterns/
84
 
84
 
85
       @param $str: la chaîne à "parser"
85
       @param $str: la chaîne à "parser"
86
       @param $default_op: "AND" ou "OR"
86
       @param $default_op: "AND" ou "OR"
87
       @param $additional_sep: séparateur de mots:
87
       @param $additional_sep: séparateur de mots:
88
    */
88
    */
89
    static function buildTagsAST($str = NULL, $default_op, $additional_sep = ',') {
89
    static function buildTagsAST($str = NULL, $default_op, $additional_sep = ',') {
90
	if(!$str) return;
90
	if(!$str) return;
91
	$words = preg_split('/ (OR|AND|ET|OU) /', $str, -1, PREG_SPLIT_NO_EMPTY);
91
	$words = preg_split('/ (OR|AND|ET|OU) /', $str, -1, PREG_SPLIT_NO_EMPTY);
92
 
92
 
93
	if(preg_match('/\b(ET|AND)\b/', $str)) $op = 'AND';
93
	if(preg_match('/\b(ET|AND)\b/', $str)) $op = 'AND';
94
	elseif(preg_match('/\b(OU|OR)\b/', $str)) $op = 'OR';
94
	elseif(preg_match('/\b(OU|OR)\b/', $str)) $op = 'OR';
95
	else $op = $default_op;
95
	else $op = $default_op;
96
 
96
 
97
	if($additional_sep) {
97
	if($additional_sep) {
98
	    array_walk($words,
98
	    array_walk($words,
99
		       create_function('&$v, $k, $sep', '$v = preg_split("/".$sep."/", $v, -1, PREG_SPLIT_NO_EMPTY);'),
99
		       create_function('&$v, $k, $sep', '$v = preg_split("/".$sep."/", $v, -1, PREG_SPLIT_NO_EMPTY);'),
100
		       $additional_sep);
100
		       $additional_sep);
101
	}
101
	}
102
	$words = DelTk::array_flatten($words);
102
	$words = DelTk::array_flatten($words);
103
	$words = array_map('trim', $words);
103
	$words = array_map('trim', $words);
104
	return array($op => array_filter($words));
104
	return array($op => array_filter($words));
105
    }
105
    }
106
 
106
 
107
 
107
 
108
    static function array_flatten($arr) {
108
    static function array_flatten($arr) {
109
	$arr = array_values($arr);
109
	$arr = array_values($arr);
110
	while (list($k,$v)=each($arr)) {
110
	while (list($k,$v)=each($arr)) {
111
	    if (is_array($v)) {
111
	    if (is_array($v)) {
112
		array_splice($arr,$k,1,$v);
112
		array_splice($arr,$k,1,$v);
113
		next($arr);
113
		next($arr);
114
	    }
114
	    }
115
	}
115
	}
116
	return $arr;
116
	return $arr;
117
    }
117
    }
118
 
118
 
119
    // supprime l'index du tableau des paramètres si sa valeur ne correspond pas
119
    // supprime l'index du tableau des paramètres si sa valeur ne correspond pas
120
    // au spectre passé par $values.
120
    // au spectre passé par $values.
121
    static function unsetIfInvalid(&$var, $index, $values) {
121
    static function unsetIfInvalid(&$var, $index, $values) {
122
	if(array_key_exists($index, $var)) {
122
	if(array_key_exists($index, $var)) {
123
	    if(!in_array($var[$index], $values)) unset($var[$index]);
123
	    if(!in_array($var[$index], $values)) unset($var[$index]);
124
	    else return $var[$index];
124
	    else return $var[$index];
125
	}
125
	}
126
	return NULL;
126
	return NULL;
127
    }
127
    }
128
 
128
 
129
 
129
 
130
 
130
 
131
 
131
 
132
    /* Filtre et valide les paramètres reconnus. Effectue *toute* la sanitization *sauf* l'escape-string
132
    /* Filtre et valide les paramètres reconnus. Effectue *toute* la sanitization *sauf* l'escape-string
133
       Cette fonction est appelée:
133
       Cette fonction est appelée:
134
       - une fois sur les champs de recherche avancées
134
       - une fois sur les champs de recherche avancées
135
       - une fois sur le masque général si celui-ci à été spécifié. Dans ce cas,
135
       - une fois sur le masque général si celui-ci à été spécifié. Dans ce cas,
136
       la chaîne générale saisie est utilisée comme valeur pour chacun des champs particuliers
136
       la chaîne générale saisie est utilisée comme valeur pour chacun des champs particuliers
137
       avec les traitements particuliers qui s'imposent
137
       avec les traitements particuliers qui s'imposent
138
       Par exemple: si l'on cherche "Languedoc", cela impliquera:
138
       Par exemple: si l'on cherche "Languedoc", cela impliquera:
139
       WHERE (nom_sel like "Languedoc" OR nom_ret ... OR ...) mais pas masque.date ou masque.departement
139
       WHERE (nom_sel like "Languedoc" OR nom_ret ... OR ...) mais pas masque.date ou masque.departement
140
       qui s'assure d'un pattern particulier */
140
       qui s'assure d'un pattern particulier */
141
    static function requestFilterParams(Array $params, $parametres_autorises = NULL, Conteneur $c = NULL /* pour la récup des départements */ ) {
141
    static function requestFilterParams(Array $params, $parametres_autorises = NULL, Conteneur $c = NULL /* pour la récup des départements */ ) {
142
	if($parametres_autorises) { // filtrage de toute clef inconnue
142
	if($parametres_autorises) { // filtrage de toute clef inconnue
143
	    $params = array_intersect_key($params, array_flip($parametres_autorises));
143
	    $params = array_intersect_key($params, array_flip($parametres_autorises));
144
	}
144
	}
145
 
145
 
146
	$p['tri'] = DelTK::unsetIfInvalid($params, 'tri', array('date_observation'));
146
	$p['tri'] = DelTK::unsetIfInvalid($params, 'tri', array('date_observation'));
147
	$p['ordre'] = DelTK::unsetIfInvalid($params, 'ordre', array('asc','desc'));
147
	$p['ordre'] = DelTK::unsetIfInvalid($params, 'ordre', array('asc','desc'));
148
	$p['masque.referentiel'] = DelTK::unsetIfInvalid($params, 'masque.referentiel', array('bdtfx','bdtxa','isfan'));
148
	$p['masque.referentiel'] = DelTK::unsetIfInvalid($params, 'masque.referentiel', array('bdtfx','bdtxa','isfan'));
149
 
149
 
150
	// TODO: use filter_input(INPUT_GET);
150
	// TODO: use filter_input(INPUT_GET);
151
	// renvoie FALSE ou NULL si absent ou invalide
151
	// renvoie FALSE ou NULL si absent ou invalide
152
	$p['navigation.limite'] = filter_var(@$params['navigation.limite'],
152
	$p['navigation.limite'] = filter_var(@$params['navigation.limite'],
153
					     FILTER_VALIDATE_INT,
153
					     FILTER_VALIDATE_INT,
154
					     array('options' => array('default' => NULL,
154
					     array('options' => array('default' => NULL,
155
								      'min_range' => 1,
155
								      'min_range' => 1,
156
								      'max_range' => _LISTE_OBS_MAX_RESULT_LIMIT)));
156
								      'max_range' => _LISTE_OBS_MAX_RESULT_LIMIT)));
157
	$p['navigation.depart'] = filter_var(@$params['navigation.depart'],
157
	$p['navigation.depart'] = filter_var(@$params['navigation.depart'],
158
					     FILTER_VALIDATE_INT,
158
					     FILTER_VALIDATE_INT,
159
					     array('options' => array('default' => NULL,
159
					     array('options' => array('default' => NULL,
160
								      'min_range' => 0,
160
								      'min_range' => 0,
161
								      'max_range' => _LISTE_OBS_MAX_ID_OBS)));
161
								      'max_range' => _LISTE_OBS_MAX_ID_OBS)));
162
	if(isset($params['masque.departement'])) {
162
	if (isset($params['masque.departement'])) {
163
	    // STRING: 0 -> 95, 971 -> 976, 2A + 2B (./services/configurations/config_departements_bruts.ini)
163
		// STRING: 0 -> 95, 971 -> 976, 2A + 2B (./services/configurations/config_departements_bruts.ini)
164
	    // accept leading 0 ?
164
		// accept leading 0 ?
165
	    // TODO; filter patterns like 555.
165
		// TODO; filter patterns like 555.
166
	    if(preg_match(';^(\d{2}|\d{3}|2a|2b)$;i', $params['masque.departement'])) {
166
		if (preg_match(';^(\d{2}|\d{3}|2a|2b)$;i', $params['masque.departement']) != 0) {
-
 
167
			if(is_numeric($params['masque.departement'])) {
167
		$p['masque.departement'] = $params['masque.departement'];
168
				$p['masque.departement'] = str_pad($params['masque.departement'], 5, '_');
168
	    }
169
			} else {
169
	    // cf configurations/config_departements_bruts.ini
170
				$p['masque.departement'] = $params['masque.departement'];
-
 
171
			}
170
	    elseif( !is_null($c) && ( $x = $c->getParametre(
172
		} else {
171
		strtolower(str_replace(' ','-',iconv("UTF-8", "ASCII//TRANSLIT", $params['masque.departement'])))
173
			$dpt_chaine = strtolower(str_replace(' ','-',iconv("UTF-8", "ASCII//TRANSLIT", $params['masque.departement'])));
172
	    ))) {
174
			$c->chargerConfiguration('config_departements_bruts.ini');
173
		$p['masque.departement'] = sprintf("INSEE-C:%02d___", $x);
175
			$p['masque.departement'] = str_pad($c->getParametre($dpt_chaine), 5, '_');
174
	    }
176
		}
175
	}
177
	}
176
 
178
 
177
	if(isset($params['masque.date'])) {
179
	if(isset($params['masque.date'])) {
178
	    // une année, TODO: masque.annee
180
	    // une année, TODO: masque.annee
179
	    if(is_numeric($params['masque.date'])) {
181
	    if(is_numeric($params['masque.date'])) {
180
		$p['masque.date'] = $params['masque.date'];
182
		$p['masque.date'] = $params['masque.date'];
181
	    }
183
	    }
182
	    elseif(strpos($params['masque.date'], '/' !== false) &&
184
	    elseif(strpos($params['masque.date'], '/' !== false) &&
183
		   ($x = strtotime(str_replace('/','-',$params['masque.date'])))) {
185
		   ($x = strtotime(str_replace('/','-',$params['masque.date'])))) {
184
		$p['masque.date'] = $x;
186
		$p['masque.date'] = $x;
185
	    }
187
	    }
186
	    elseif(strpos($params['masque.date'], '-' !== false) &&
188
	    elseif(strpos($params['masque.date'], '-' !== false) &&
187
		   ($x = strtotime($params['masque.date'])) ) {
189
		   ($x = strtotime($params['masque.date'])) ) {
188
		$p['masque.date'] = $x;
190
		$p['masque.date'] = $x;
189
	    }
191
	    }
190
	}
192
	}
191
 
193
 
192
	$p['masque.nn'] = filter_var(@$params['masque.nn'],
194
	$p['masque.nn'] = filter_var(@$params['masque.nn'],
193
				     FILTER_VALIDATE_INT,
195
				     FILTER_VALIDATE_INT,
194
				     array('options' => array('default' => NULL,
196
				     array('options' => array('default' => NULL,
195
							      'min_range' => 0,
197
							      'min_range' => 0,
196
							      'max_range' => _LISTE_OBS_MAX_BDTFX_NN)));
198
							      'max_range' => _LISTE_OBS_MAX_BDTFX_NN)));
197
 
199
 
198
	$p['masque.nt'] = filter_var(@$params['masque.nt'],
200
	$p['masque.nt'] = filter_var(@$params['masque.nt'],
199
				     FILTER_VALIDATE_INT,
201
				     FILTER_VALIDATE_INT,
200
				     array('options' => array('default' => NULL,
202
				     array('options' => array('default' => NULL,
201
							      'min_range' => 0,
203
							      'min_range' => 0,
202
							      'max_range' => _LISTE_OBS_MAX_BDTFX_NT)));
204
							      'max_range' => _LISTE_OBS_MAX_BDTFX_NT)));
203
 
205
 
204
 
206
 
205
	// TODO: should we really trim() ?
207
	// TODO: should we really trim() ?
206
 
208
 
207
	if(isset($params['masque.ns'])) $p['masque.ns'] = trim($params['masque.ns']);
209
	if(isset($params['masque.ns'])) $p['masque.ns'] = trim($params['masque.ns']);
208
	// if(isset($params['masque.texte'])) $p['masque.texte'] = trim($params['masque.texte']);
210
	// if(isset($params['masque.texte'])) $p['masque.texte'] = trim($params['masque.texte']);
209
 
211
 
210
	if(isset($params['masque.famille'])) {
212
	if(isset($params['masque.famille'])) {
211
	    // mysql -N<<<"SELECT DISTINCT famille FROM bdtfx_v1_02;"|sed -r "s/(.)/\1\n/g"|sort -u|tr -d "\n"
213
	    // mysql -N<<<"SELECT DISTINCT famille FROM bdtfx_v1_02;"|sed -r "s/(.)/\1\n/g"|sort -u|tr -d "\n"
212
	    $p['masque.famille'] = preg_replace('/[^a-zA-Z %_]/', '', iconv("UTF-8",
214
	    $p['masque.famille'] = preg_replace('/[^a-zA-Z %_]/', '', iconv("UTF-8",
213
									    "ASCII//TRANSLIT",
215
									    "ASCII//TRANSLIT",
214
									    $params['masque.famille']));
216
									    $params['masque.famille']));
215
	}
217
	}
216
 
218
 
217
	// masque.genre est un alias pour masque.ns (nom_sel), mais permet de rajouter une clause supplémentaire
219
	// masque.genre est un alias pour masque.ns (nom_sel), mais permet de rajouter une clause supplémentaire
218
	// sur nom_sel. Précédemment: WHERE nom_sel LIKE '%<masque.genre>% %'.
220
	// sur nom_sel. Précédemment: WHERE nom_sel LIKE '%<masque.genre>% %'.
219
	// Désormais masque.genre doit être intégralement spécifié, les caractères '%' et '_' seront interprétés.
221
	// Désormais masque.genre doit être intégralement spécifié, les caractères '%' et '_' seront interprétés.
220
	// Attention toutefois car la table del_observation intègre des nom_sel contenant '_'
222
	// Attention toutefois car la table del_observation intègre des nom_sel contenant '_'
221
	if(isset($params['masque.genre'])) $p['masque.genre'] = trim($params['masque.genre']);
223
	if(isset($params['masque.genre'])) $p['masque.genre'] = trim($params['masque.genre']);
222
	if(isset($params['masque.ns'])) $p['masque.ns'] = trim($params['masque.ns']);
224
	if(isset($params['masque.ns'])) $p['masque.ns'] = trim($params['masque.ns']);
223
	// masque.espece n'était pas déclaré dans la "where" mais utilisé via config + switch//default
225
	// masque.espece n'était pas déclaré dans la "where" mais utilisé via config + switch//default
224
	if(isset($params['masque.espece'])) $p['masque.espece'] = trim($params['masque.espece']);
226
	if(isset($params['masque.espece'])) $p['masque.espece'] = trim($params['masque.espece']);
225
 
227
 
226
	// idem pour id_zone_geo qui mappait à ce_zone_geo:
228
	// idem pour id_zone_geo qui mappait à ce_zone_geo:
227
	if(isset($params['masque.id_zone_geo']) && preg_match(';^(INSEE-C:\d{5}|\d{2})$;', $params['masque.id_zone_geo'])) {
229
	if(isset($params['masque.id_zone_geo']) && preg_match(';^(INSEE-C:\d{5}|\d{2})$;', $params['masque.id_zone_geo'])) {
228
	    $p['masque.id_zone_geo'] = $params['masque.id_zone_geo'];
230
	    $p['masque.id_zone_geo'] = $params['masque.id_zone_geo'];
229
	}
231
	}
230
 
232
 
231
	// masque.commune (zone_geo)
233
	// masque.commune (zone_geo)
232
	// TODO: que faire avec des '%' en INPUT ?
234
	// TODO: que faire avec des '%' en INPUT ?
233
	// Le masque doit *permettre* une regexp et non l'imposer. Charge au client de faire son travail
235
	// Le masque doit *permettre* une regexp et non l'imposer. Charge au client de faire son travail
234
	if(isset($params['masque.commune'])) $p['masque.commune'] = str_replace(array('-',' '), '_', $params['masque.commune']);
236
	if(isset($params['masque.commune'])) $p['masque.commune'] = str_replace(array('-',' '), '_', $params['masque.commune']);
235
 
237
 
236
	// masque.auteur: peut-être un id, un courriel, ou un nom ou prénom, ...
238
	// masque.auteur: peut-être un id, un courriel, ou un nom ou prénom, ...
237
	if(isset($params['masque.auteur'])) $p['masque.auteur'] = trim($params['masque.auteur']);
239
	if(isset($params['masque.auteur'])) $p['masque.auteur'] = trim($params['masque.auteur']);
238
	// sera trimmé plus tard, cf sqlAddConstraint
240
	// sera trimmé plus tard, cf sqlAddConstraint
239
	if(isset($params['masque'])) $p['masque'] = trim($params['masque']);
241
	if(isset($params['masque'])) $p['masque'] = trim($params['masque']);
240
 
242
 
241
	// masque.tag, idem que pour masque.genre et masque.commune
243
	// masque.tag, idem que pour masque.genre et masque.commune
242
	if(isset($params['masque.tag'])) {
244
	if(isset($params['masque.tag'])) {
243
	    $x = explode(',',$params['masque.tag']);
245
	    $x = explode(',',$params['masque.tag']);
244
	    $x = array_map('trim', $x);
246
	    $x = array_map('trim', $x);
245
	    $p['masque.tag'] = implode('|', array_filter($x));
247
	    $p['masque.tag'] = implode('|', array_filter($x));
246
	}
248
	}
247
 
249
 
248
	// masque.type: ['adeterminer', 'aconfirmer', 'endiscussion', 'validees']
250
	// masque.type: ['adeterminer', 'aconfirmer', 'endiscussion', 'validees']
249
	if(isset($params['masque.type'])) {
251
	if(isset($params['masque.type'])) {
250
	    $p['masque.type'] = array_flip(array_intersect(array_filter(explode(';', $params['masque.type'])),
252
	    $p['masque.type'] = array_flip(array_intersect(array_filter(explode(';', $params['masque.type'])),
251
							   array('adeterminer', 'aconfirmer', 'endiscussion', 'validees')));
253
							   array('adeterminer', 'aconfirmer', 'endiscussion', 'validees')));
252
	}
254
	}
253
 
255
 
254
 
256
 
255
	// TODO: masque (général)
257
	// TODO: masque (général)
256
 
258
 
257
 
259
 
258
	// on filtre les NULL, FALSE et '', mais pas les 0, d'où le callback()
260
	// on filtre les NULL, FALSE et '', mais pas les 0, d'où le callback()
259
	// TODO: PHP-5.3
261
	// TODO: PHP-5.3
260
	return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
262
	return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
261
    }
263
    }
262
 
264
 
263
 
265
 
264
 
266
 
265
    // SQL helpers
267
    // SQL helpers
266
 
268
 
267
    /* Lorsque l'on concatène des champs, un seul NULL prend le dessus,
269
    /* Lorsque l'on concatène des champs, un seul NULL prend le dessus,
268
       Il faut donc utiliser la syntaxe IFNULL(%s, "").
270
       Il faut donc utiliser la syntaxe IFNULL(%s, "").
269
       (Cette fonction effectue aussi l'implode() "final" */
271
       (Cette fonction effectue aussi l'implode() "final" */
270
    static function sqlAddIfNullPourConcat($tab) {
272
    static function sqlAddIfNullPourConcat($tab) {
271
	// XXX: PHP-5.3
273
	// XXX: PHP-5.3
272
	return implode(',',array_map(create_function('$a', 'return "IFNULL($a, \"\")";'), $tab));
274
	return implode(',',array_map(create_function('$a', 'return "IFNULL($a, \"\")";'), $tab));
273
    }
275
    }
274
 
276
 
275
 
277
 
276
 
278
 
277
    /* Converti un tableau associatif et un préfix optionnel en une chaîne de champs adéquate
279
    /* Converti un tableau associatif et un préfix optionnel en une chaîne de champs adéquate
278
       à un SELECT MySQL.
280
       à un SELECT MySQL.
279
       $select (optionnel) restreint les champs mappés aux valeurs de $select.
281
       $select (optionnel) restreint les champs mappés aux valeurs de $select.
280
       Si $select n'est pas fourni, toutes les clefs présentes dans $map seront présentes dans
282
       Si $select n'est pas fourni, toutes les clefs présentes dans $map seront présentes dans
281
       le SELECT en sortie */
283
       le SELECT en sortie */
282
    static function sqlFieldsToAlias($map, $select = NULL, $prefix = NULL) {
284
    static function sqlFieldsToAlias($map, $select = NULL, $prefix = NULL) {
283
	if($select) $arr = array_intersect_key($map, array_flip($select));
285
	if($select) $arr = array_intersect_key($map, array_flip($select));
284
	else $arr = $map;
286
	else $arr = $map;
285
	$keys = array_keys($arr);
287
	$keys = array_keys($arr);
286
 
288
 
287
	if($prefix) array_walk($keys, create_function('&$val, $k, $prefix', '$val = sprintf("%s.`%s`", $prefix, $val);'), $prefix);
289
	if($prefix) array_walk($keys, create_function('&$val, $k, $prefix', '$val = sprintf("%s.`%s`", $prefix, $val);'), $prefix);
288
	else array_walk($keys, create_function('&$val, $k', '$val = sprintf("`%s`", $val);'));
290
	else array_walk($keys, create_function('&$val, $k', '$val = sprintf("`%s`", $val);'));
289
 
291
 
290
	return implode(', ', array_map(create_function('$v, $k', 'return sprintf("%s AS `%s`", $k, $v);'), $arr, $keys));
292
	return implode(', ', array_map(create_function('$v, $k', 'return sprintf("%s AS `%s`", $k, $v);'), $arr, $keys));
291
    }
293
    }
292
 
294
 
293
 
295
 
294
 
296
 
295
    /*
297
    /*
296
      Retourne une clause where du style:
298
      Retourne une clause where du style:
297
      CONCAT(IF(du.prenom IS NULL, "", du.prenom), [...] vdi.i_nomutilisateur) REGEXP 'xxx'
299
      CONCAT(IF(du.prenom IS NULL, "", du.prenom), [...] vdi.i_nomutilisateur) REGEXP 'xxx'
298
      Note; i_(nom|prenom_utilisateur), alias pour cel_images.(nom|prenom), n'est pas traité
300
      Note; i_(nom|prenom_utilisateur), alias pour cel_images.(nom|prenom), n'est pas traité
299
      car cette information est redondante dans cel_image et devrait être supprimée.
301
      car cette information est redondante dans cel_image et devrait être supprimée.
300
    */
302
    */
301
    static function addAuteursConstraint($val, $db, &$where) {
303
    static function addAuteursConstraint($val, $db, &$where) {
302
	@list($a, $b) = explode(' ', $val, 2);
304
	@list($a, $b) = explode(' ', $val, 2);
303
	// un seul terme
305
	// un seul terme
304
	$champs_n = array('du.prenom', // info user authentifié de l'obs depuis l'annuaire
306
	$champs_n = array('du.prenom', // info user authentifié de l'obs depuis l'annuaire
305
			  'vdi.prenom_utilisateur', // info user anonyme de l'obs
307
			  'vdi.prenom_utilisateur', // info user anonyme de l'obs
306
			  /* 'vdi.i_prenom_utilisateur' */ ); // info user anonyme de l'image
308
			  /* 'vdi.i_prenom_utilisateur' */ ); // info user anonyme de l'image
307
	$champs_p = array('du.nom', // idem pour le nom
309
	$champs_p = array('du.nom', // idem pour le nom
308
			  'vdi.nom_utilisateur',
310
			  'vdi.nom_utilisateur',
309
			  /* 'vdi.i_nom_utilisateur' */ );
311
			  /* 'vdi.i_nom_utilisateur' */ );
310
 
312
 
311
	/*
313
	/*
312
	  Note: pour l'heure, étant donnés:
314
	  Note: pour l'heure, étant donnés:
313
	  - les CONVERT() de la VIEW del_utilisateur
315
	  - les CONVERT() de la VIEW del_utilisateur
314
	  - DEFAULT CHARSET=latin1 pour tela_prod_v4.annuaire_tela
316
	  - DEFAULT CHARSET=latin1 pour tela_prod_v4.annuaire_tela
315
	  - DEFAULT CHARSET=utf8 pour tb_cel.cel_obs
317
	  - DEFAULT CHARSET=utf8 pour tb_cel.cel_obs
316
	  et l'âge du capitaine...
318
	  et l'âge du capitaine...
317
	  - REGEXP est case-sensitive, et collate les caractères accentués
319
	  - REGEXP est case-sensitive, et collate les caractères accentués
318
	  - LIKE est case-insensitive, et collate les caractères accentués
320
	  - LIKE est case-insensitive, et collate les caractères accentués
319
	*/
321
	*/
320
	if(! $b) {
322
	if(! $b) {
321
	    $where[] = sprintf('CONCAT(%s,%s) LIKE %s',
323
	    $where[] = sprintf('CONCAT(%s,%s) LIKE %s',
322
			       DelTk::sqlAddIfNullPourConcat($champs_n),
324
			       DelTk::sqlAddIfNullPourConcat($champs_n),
323
			       DelTk::sqlAddIfNullPourConcat($champs_p),
325
			       DelTk::sqlAddIfNullPourConcat($champs_p),
324
			       $db->proteger("%".$val."%"));
326
			       $db->proteger("%".$val."%"));
325
	}
327
	}
326
	else {
328
	else {
327
	    $where[] = sprintf('(CONCAT(%1$s,%2$s) LIKE %3$s AND CONCAT(%1$s,%2$s) LIKE %4$s)',
329
	    $where[] = sprintf('(CONCAT(%1$s,%2$s) LIKE %3$s AND CONCAT(%1$s,%2$s) LIKE %4$s)',
328
			       DelTk::sqlAddIfNullPourConcat($champs_n),
330
			       DelTk::sqlAddIfNullPourConcat($champs_n),
329
			       DelTk::sqlAddIfNullPourConcat($champs_p),
331
			       DelTk::sqlAddIfNullPourConcat($champs_p),
330
			       $db->proteger("%" . $a . "%"), $db->proteger("%" . $b . "%"));
332
			       $db->proteger("%" . $a . "%"), $db->proteger("%" . $b . "%"));
331
	}
333
	}
332
    }
334
    }
333
 
335
 
334
 
336
 
335
 
337
 
336
 
338
 
337
 
339
 
338
    /**
340
    /**
339
     * - Rempli le tableau des contraintes "where" et "join" nécessaire
341
     * - Rempli le tableau des contraintes "where" et "join" nécessaire
340
     * à la *recherche* des observations demandées ($req) utilisées par self::getIdObs()
342
     * à la *recherche* des observations demandées ($req) utilisées par self::getIdObs()
341
     *
343
     *
342
     * Attention, cela signifie que toutes les tables ne sont pas *forcément*
344
     * Attention, cela signifie que toutes les tables ne sont pas *forcément*
343
     * join'ées, par exemple si aucune contrainte ne le nécessite.
345
     * join'ées, par exemple si aucune contrainte ne le nécessite.
344
     * $req tel qu'il est rempli ici est utile pour récupéré la seule liste des
346
     * $req tel qu'il est rempli ici est utile pour récupéré la seule liste des
345
     * id d'observation qui match.
347
     * id d'observation qui match.
346
     * Pour la récupération effective de "toutes" les données correspondante, il faut
348
     * Pour la récupération effective de "toutes" les données correspondante, il faut
347
     * réinitialiser $req["join"] afin d'y ajouter toutes les autres tables.
349
     * réinitialiser $req["join"] afin d'y ajouter toutes les autres tables.
348
     *
350
     *
349
     * Note: toujours rajouter les préfixes de table (vdi,du,doi ou di), en fonction de ce que défini
351
     * Note: toujours rajouter les préfixes de table (vdi,du,doi ou di), en fonction de ce que défini
350
     * les JOIN qui sont utilisés.
352
     * les JOIN qui sont utilisés.
351
     * le préfix de v_del_image est "vdi" (cf: "FROM" de self::getIdObs())
353
     * le préfix de v_del_image est "vdi" (cf: "FROM" de self::getIdObs())
352
     * le préfix de del_utilisateur sur id_utilisateur = vdi.ce_utilisateur est "du"
354
     * le préfix de del_utilisateur sur id_utilisateur = vdi.ce_utilisateur est "du"
353
     *
355
     *
354
     * @param $p les paramètres (notamment de masque) passés par l'URL et déjà traités/filtrés (sauf quotes)
356
     * @param $p les paramètres (notamment de masque) passés par l'URL et déjà traités/filtrés (sauf quotes)
355
     * @param $req le tableau, passé par référence représentant les composants de la requête à bâtir
357
     * @param $req le tableau, passé par référence représentant les composants de la requête à bâtir
356
     */
358
     */
357
    static function sqlAddConstraint($p, $db, &$req) {
359
    static function sqlAddConstraint($p, $db, &$req) {
358
	if(!empty($p['masque.auteur'])) {
360
	if(!empty($p['masque.auteur'])) {
359
	    // id du poster de l'obs
361
	    // id du poster de l'obs
360
	    $req['join'][] = 'LEFT JOIN del_utilisateur AS du ON du.id_utilisateur = vdi.ce_utilisateur';
362
	    $req['join'][] = 'LEFT JOIN del_utilisateur AS du ON du.id_utilisateur = vdi.ce_utilisateur';
361
	    // id du poster de l'image... NON, c'est le même que le posteur de l'obs
363
	    // id du poster de l'image... NON, c'est le même que le posteur de l'obs
362
	    // Cette jointure de table est ignoré ci-dessous pour les recherches d'auteurs
364
	    // Cette jointure de table est ignoré ci-dessous pour les recherches d'auteurs
363
	    // $req['join'][] = 'LEFT JOIN del_utilisateur AS dui ON dui.id_utilisateur = vdi.i_ce_utilisateur';
365
	    // $req['join'][] = 'LEFT JOIN del_utilisateur AS dui ON dui.id_utilisateur = vdi.i_ce_utilisateur';
364
 
366
 
365
	    if(is_numeric($p['masque.auteur'])) {
367
	    if(is_numeric($p['masque.auteur'])) {
366
		$req['where'][] = sprintf('(du.id_utilisateur = %1$d OR vdi.id_utilisateur = %1$d)', $p['masque.auteur']);
368
		$req['where'][] = sprintf('(du.id_utilisateur = %1$d OR vdi.id_utilisateur = %1$d)', $p['masque.auteur']);
367
	    }
369
	    }
368
	    elseif(preg_match(';^.{5,}@[a-z0-9-.]{5,}$;i', $p['masque.auteur'])) {
370
	    elseif(preg_match(';^.{5,}@[a-z0-9-.]{5,}$;i', $p['masque.auteur'])) {
369
		$req['where'][] = sprintf('(du.courriel LIKE %1$s OR vdi.courriel LIKE %1$s )',
371
		$req['where'][] = sprintf('(du.courriel LIKE %1$s OR vdi.courriel LIKE %1$s )',
370
					  $db->proteger($p['masque.auteur'] . '%'));
372
					  $db->proteger($p['masque.auteur'] . '%'));
371
	    }
373
	    }
372
	    else {
374
	    else {
373
		DelTk::addAuteursConstraint($p['masque.auteur'], $db, $req['where']);
375
		DelTk::addAuteursConstraint($p['masque.auteur'], $db, $req['where']);
374
	    }
376
	    }
375
	}
377
	}
376
 
378
 
377
	if(!empty($p['masque.date'])) {
379
	if(!empty($p['masque.date'])) {
378
	    if(is_integer($p['masque.date']) && $p['masque.date'] < 2030 && $p['masque.date'] > 1600) {
380
	    if(is_integer($p['masque.date']) && $p['masque.date'] < 2030 && $p['masque.date'] > 1600) {
379
		$req['where'][] = sprintf("YEAR(vdi.date_observation) = %d", $p['masque.date']);
381
		$req['where'][] = sprintf("YEAR(vdi.date_observation) = %d", $p['masque.date']);
380
	    }
382
	    }
381
	    else {
383
	    else {
382
		$req['where'][] = sprintf("DATE_FORMAT(vdi.date_observation, '%%Y-%%m-%%d') = %s",
384
		$req['where'][] = sprintf("DATE_FORMAT(vdi.date_observation, '%%Y-%%m-%%d') = %s",
383
					  $db->proteger(strftime('%Y-%m-%d', $p['masque.date'])));
385
					  $db->proteger(strftime('%Y-%m-%d', $p['masque.date'])));
384
	    }
386
	    }
385
	}
387
	}
386
 
388
 
387
	// TODO: avoir des champs d'entrée distinct
389
	// TODO: avoir des champs d'entrée distinct
388
	if(!empty($p['masque.departement'])) {
390
	if(!empty($p['masque.departement'])) {
389
	    $req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger('INSEE-C:'.$p['masque.departement']));
391
	    $req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger('INSEE-C:'.$p['masque.departement']));
390
	}
392
	}
391
	if(!empty($p['masque.id_zone_geo'])) {
393
	if(!empty($p['masque.id_zone_geo'])) {
392
	    $req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger($p['masque.id_zone_geo']));
394
	    $req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger($p['masque.id_zone_geo']));
393
	}
395
	}
394
	if(!empty($p['masque.genre'])) {
396
	if(!empty($p['masque.genre'])) {
395
	    $req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger('%' . $p['masque.genre'].'% %');
397
	    $req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger('%' . $p['masque.genre'].'% %');
396
	}
398
	}
397
	if(!empty($p['masque.famille'])) {
399
	if(!empty($p['masque.famille'])) {
398
	    $req['where'][] = 'vdi.famille = '.$db->proteger($p['masque.famille']);
400
	    $req['where'][] = 'vdi.famille = '.$db->proteger($p['masque.famille']);
399
	}
401
	}
400
	if(!empty($p['masque.ns'])) {
402
	if(!empty($p['masque.ns'])) {
401
	    $req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger($p['masque.ns'].'%');
403
	    $req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger($p['masque.ns'].'%');
402
	}
404
	}
403
	if(!empty($p['masque.nn'])) {
405
	if(!empty($p['masque.nn'])) {
404
	    $req['where'][] = sprintf('(vdi.nom_sel_nn = %1$d OR vdi.nom_ret_nn = %1$d)', $p['masque.nn']);
406
	    $req['where'][] = sprintf('(vdi.nom_sel_nn = %1$d OR vdi.nom_ret_nn = %1$d)', $p['masque.nn']);
405
	}
407
	}
406
	if(!empty($p['masque.referentiel'])) {
408
	if(!empty($p['masque.referentiel'])) {
407
	    $req['where'][] = sprintf('vdi.nom_referentiel LIKE %s', $db->proteger($p['masque.referentiel'].'%'));
409
	    $req['where'][] = sprintf('vdi.nom_referentiel LIKE %s', $db->proteger($p['masque.referentiel'].'%'));
408
	}
410
	}
409
	if(!empty($p['masque.commune'])) {
411
	if(!empty($p['masque.commune'])) {
410
	    $req['where'][] = 'vdi.zone_geo LIKE '.$db->proteger($p['masque.commune'].'%');
412
	    $req['where'][] = 'vdi.zone_geo LIKE '.$db->proteger($p['masque.commune'].'%');
411
	}
413
	}
412
    }
414
    }
413
 
415
 
414
 
416
 
415
 
417
 
416
    // formatage de réponse HTTP
418
    // formatage de réponse HTTP
417
    static function makeJSONHeader($total, $params, $url_service) {
419
    static function makeJSONHeader($total, $params, $url_service) {
418
	$prev_url = $next_url = NULL;
420
	$prev_url = $next_url = NULL;
419
	$url_service_sans_slash = substr($url_service, 0, -1);
421
	$url_service_sans_slash = substr($url_service, 0, -1);
420
 
422
 
421
	// aplatissons les params! - une seule couche cela dit, après débrouillez-vous
423
	// aplatissons les params! - une seule couche cela dit, après débrouillez-vous
422
	$params_a_plat = $params;
424
	$params_a_plat = $params;
423
	foreach ($params_a_plat as $cle_plate => $pap) {
425
	foreach ($params_a_plat as $cle_plate => $pap) {
424
	    if (is_array($pap)) {
426
	    if (is_array($pap)) {
425
		$params_a_plat[$cle_plate] = implode(array_keys($pap), ',');
427
		$params_a_plat[$cle_plate] = implode(array_keys($pap), ',');
426
	    }
428
	    }
427
	}
429
	}
428
 
430
 
429
	$next_offset = $params['navigation.depart'] + $params['navigation.limite'];
431
	$next_offset = $params['navigation.depart'] + $params['navigation.limite'];
430
	if($next_offset < $total) {
432
	if($next_offset < $total) {
431
	    $next_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $next_offset)));
433
	    $next_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $next_offset)));
432
	}
434
	}
433
 
435
 
434
	$prev_offset = $params['navigation.depart'] - $params['navigation.limite'];
436
	$prev_offset = $params['navigation.depart'] - $params['navigation.limite'];
435
	if($prev_offset >= 0) {
437
	if($prev_offset >= 0) {
436
	    $prev_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $prev_offset)));
438
	    $prev_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $prev_offset)));
437
	}
439
	}
438
 
440
 
439
	return array(
441
	return array(
440
	    'masque' => http_build_query(array_diff_key($params, array_flip(array('navigation.depart', 'navigation.limite')))),
442
	    'masque' => http_build_query(array_diff_key($params, array_flip(array('navigation.depart', 'navigation.limite')))),
441
	    'total' => $total,
443
	    'total' => $total,
442
	    'depart' => $params['navigation.depart'],
444
	    'depart' => $params['navigation.depart'],
443
	    'limite' => $params['navigation.limite'],
445
	    'limite' => $params['navigation.limite'],
444
	    'href.precedent' => $prev_url,
446
	    'href.precedent' => $prev_url,
445
	    'href.suivant' => $next_url
447
	    'href.suivant' => $next_url
446
	);
448
	);
447
    }
449
    }
448
}
450
}