Subversion Repositories eFlore/Applications.del

Rev

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

Rev 1494 Rev 1511
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'])) {
167
		$p['masque.departement'] = $params['masque.departement'];
167
		$p['masque.departement'] = $params['masque.departement'];
168
	    }
168
	    }
169
	    // cf configurations/config_departements_bruts.ini
169
	    // cf configurations/config_departements_bruts.ini
170
	    elseif( !is_null($c) && ( $x = $c->getParametre(
170
	    elseif( !is_null($c) && ( $x = $c->getParametre(
171
		strtolower(str_replace(' ','-',iconv("UTF-8", "ASCII//TRANSLIT", $params['masque.departement'])))
171
		strtolower(str_replace(' ','-',iconv("UTF-8", "ASCII//TRANSLIT", $params['masque.departement'])))
172
	    ))) {
172
	    ))) {
173
		$p['masque.departement'] = sprintf("INSEE-C:%02d___", $x);
173
		$p['masque.departement'] = sprintf("INSEE-C:%02d___", $x);
174
	    }
174
	    }
175
	}
175
	}
176
 
176
 
177
	if(isset($params['masque.date'])) {
177
	if(isset($params['masque.date'])) {
178
	    // une année, TODO: masque.annee
178
	    // une année, TODO: masque.annee
179
	    if(is_numeric($params['masque.date'])) {
179
	    if(is_numeric($params['masque.date'])) {
180
		$p['masque.date'] = $params['masque.date'];
180
		$p['masque.date'] = $params['masque.date'];
181
	    }
181
	    }
182
	    elseif(strpos($params['masque.date'], '/' !== false) &&
182
	    elseif(strpos($params['masque.date'], '/' !== false) &&
183
		   ($x = strtotime(str_replace('/','-',$params['masque.date'])))) {
183
		   ($x = strtotime(str_replace('/','-',$params['masque.date'])))) {
184
		$p['masque.date'] = $x;
184
		$p['masque.date'] = $x;
185
	    }
185
	    }
186
	    elseif(strpos($params['masque.date'], '-' !== false) &&
186
	    elseif(strpos($params['masque.date'], '-' !== false) &&
187
		   ($x = strtotime($params['masque.date'])) ) {
187
		   ($x = strtotime($params['masque.date'])) ) {
188
		$p['masque.date'] = $x;
188
		$p['masque.date'] = $x;
189
	    }
189
	    }
190
	}
190
	}
191
 
191
 
192
	$p['masque.nn'] = filter_var(@$params['masque.nn'],
192
	$p['masque.nn'] = filter_var(@$params['masque.nn'],
193
				     FILTER_VALIDATE_INT,
193
				     FILTER_VALIDATE_INT,
194
				     array('options' => array('default' => NULL,
194
				     array('options' => array('default' => NULL,
195
							      'min_range' => 0,
195
							      'min_range' => 0,
196
							      'max_range' => _LISTE_OBS_MAX_BDTFX_NN)));
196
							      'max_range' => _LISTE_OBS_MAX_BDTFX_NN)));
197
 
197
 
198
	$p['masque.nt'] = filter_var(@$params['masque.nt'],
198
	$p['masque.nt'] = filter_var(@$params['masque.nt'],
199
				     FILTER_VALIDATE_INT,
199
				     FILTER_VALIDATE_INT,
200
				     array('options' => array('default' => NULL,
200
				     array('options' => array('default' => NULL,
201
							      'min_range' => 0,
201
							      'min_range' => 0,
202
							      'max_range' => _LISTE_OBS_MAX_BDTFX_NT)));
202
							      'max_range' => _LISTE_OBS_MAX_BDTFX_NT)));
203
 
203
 
204
 
204
 
205
	// TODO: should we really trim() ?
205
	// TODO: should we really trim() ?
206
 
206
 
207
	if(isset($params['masque.ns'])) $p['masque.ns'] = trim($params['masque.ns']);
207
	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']);
208
	// if(isset($params['masque.texte'])) $p['masque.texte'] = trim($params['masque.texte']);
209
 
209
 
210
	if(isset($params['masque.famille'])) {
210
	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"
211
	    // 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",
212
	    $p['masque.famille'] = preg_replace('/[^a-zA-Z %_]/', '', iconv("UTF-8",
213
									    "ASCII//TRANSLIT",
213
									    "ASCII//TRANSLIT",
214
									    $params['masque.famille']));
214
									    $params['masque.famille']));
215
	}
215
	}
216
 
216
 
217
	// masque.genre est un alias pour masque.ns (nom_sel), mais permet de rajouter une clause supplémentaire
217
	// 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>% %'.
218
	// 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.
219
	// 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 '_'
220
	// 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']);
221
	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']);
222
	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
223
	// 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']);
224
	if(isset($params['masque.espece'])) $p['masque.espece'] = trim($params['masque.espece']);
225
 
225
 
226
	// idem pour id_zone_geo qui mappait à ce_zone_geo:
226
	// 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'])) {
227
	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'];
228
	    $p['masque.id_zone_geo'] = $params['masque.id_zone_geo'];
229
	}
229
	}
230
 
230
 
231
	// masque.commune (zone_geo)
231
	// masque.commune (zone_geo)
232
	// TODO: que faire avec des '%' en INPUT ?
232
	// 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
233
	// 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']);
234
	if(isset($params['masque.commune'])) $p['masque.commune'] = str_replace(array('-',' '), '_', $params['masque.commune']);
235
 
235
 
236
	// masque.auteur: peut-être un id, un courriel, ou un nom ou prénom, ...
236
	// 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']);
237
	if(isset($params['masque.auteur'])) $p['masque.auteur'] = trim($params['masque.auteur']);
238
	// sera trimmé plus tard, cf sqlAddConstraint
238
	// sera trimmé plus tard, cf sqlAddConstraint
239
	if(isset($params['masque'])) $p['masque'] = trim($params['masque']);
239
	if(isset($params['masque'])) $p['masque'] = trim($params['masque']);
240
 
240
 
241
	// masque.tag, idem que pour masque.genre et masque.commune
241
	// masque.tag, idem que pour masque.genre et masque.commune
242
	if(isset($params['masque.tag'])) {
242
	if(isset($params['masque.tag'])) {
243
	    $x = explode(',',$params['masque.tag']);
243
	    $x = explode(',',$params['masque.tag']);
244
	    $x = array_map('trim', $x);
244
	    $x = array_map('trim', $x);
245
	    $p['masque.tag'] = implode('|', array_filter($x));
245
	    $p['masque.tag'] = implode('|', array_filter($x));
246
	}
246
	}
247
 
247
 
248
	// masque.type: ['adeterminer', 'aconfirmer', 'endiscussion', 'validees']
248
	// masque.type: ['adeterminer', 'aconfirmer', 'endiscussion', 'validees']
249
	if(isset($params['masque.type'])) {
249
	if(isset($params['masque.type'])) {
250
	    $p['masque.type'] = array_flip(array_intersect(array_filter(explode(';', $params['masque.type'])),
250
	    $p['masque.type'] = array_flip(array_intersect(array_filter(explode(';', $params['masque.type'])),
251
							   array('adeterminer', 'aconfirmer', 'endiscussion', 'validees')));
251
							   array('adeterminer', 'aconfirmer', 'endiscussion', 'validees')));
252
	}
252
	}
253
 
253
 
254
 
254
 
255
	// TODO: masque (général)
255
	// TODO: masque (général)
256
 
256
 
257
 
257
 
258
	// on filtre les NULL, FALSE et '', mais pas les 0, d'où le callback()
258
	// on filtre les NULL, FALSE et '', mais pas les 0, d'où le callback()
259
	// TODO: PHP-5.3
259
	// TODO: PHP-5.3
260
	return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
260
	return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
261
    }
261
    }
262
 
262
 
263
 
263
 
264
 
264
 
265
    // SQL helpers
265
    // SQL helpers
266
 
266
 
267
    /* Lorsque l'on concatène des champs, un seul NULL prend le dessus,
267
    /* Lorsque l'on concatène des champs, un seul NULL prend le dessus,
268
       Il faut donc utiliser la syntaxe IFNULL(%s, "").
268
       Il faut donc utiliser la syntaxe IFNULL(%s, "").
269
       (Cette fonction effectue aussi l'implode() "final" */
269
       (Cette fonction effectue aussi l'implode() "final" */
270
    static function sqlAddIfNullPourConcat($tab) {
270
    static function sqlAddIfNullPourConcat($tab) {
271
	// XXX: PHP-5.3
271
	// XXX: PHP-5.3
272
	return implode(',',array_map(create_function('$a', 'return "IFNULL($a, \"\")";'), $tab));
272
	return implode(',',array_map(create_function('$a', 'return "IFNULL($a, \"\")";'), $tab));
273
    }
273
    }
274
 
274
 
275
 
275
 
276
 
276
 
277
    /* Converti un tableau associatif et un préfix optionnel en une chaîne de champs adéquate
277
    /* Converti un tableau associatif et un préfix optionnel en une chaîne de champs adéquate
278
       à un SELECT MySQL.
278
       à un SELECT MySQL.
279
       $select (optionnel) restreint les champs mappés aux valeurs de $select.
279
       $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
280
       Si $select n'est pas fourni, toutes les clefs présentes dans $map seront présentes dans
281
       le SELECT en sortie */
281
       le SELECT en sortie */
282
    static function sqlFieldsToAlias($map, $select = NULL, $prefix = NULL) {
282
    static function sqlFieldsToAlias($map, $select = NULL, $prefix = NULL) {
283
	if($select) $arr = array_intersect_key($map, array_flip($select));
283
	if($select) $arr = array_intersect_key($map, array_flip($select));
284
	else $arr = $map;
284
	else $arr = $map;
285
	$keys = array_keys($arr);
285
	$keys = array_keys($arr);
286
 
286
 
287
	if($prefix) array_walk($keys, create_function('&$val, $k, $prefix', '$val = sprintf("%s.`%s`", $prefix, $val);'), $prefix);
287
	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);'));
288
	else array_walk($keys, create_function('&$val, $k', '$val = sprintf("`%s`", $val);'));
289
 
289
 
290
	return implode(', ', array_map(create_function('$v, $k', 'return sprintf("%s AS `%s`", $k, $v);'), $arr, $keys));
290
	return implode(', ', array_map(create_function('$v, $k', 'return sprintf("%s AS `%s`", $k, $v);'), $arr, $keys));
291
    }
291
    }
292
 
292
 
293
 
293
 
294
 
294
 
295
    /*
295
    /*
296
      Retourne une clause where du style:
296
      Retourne une clause where du style:
297
      CONCAT(IF(du.prenom IS NULL, "", du.prenom), [...] vdi.i_nomutilisateur) REGEXP 'xxx'
297
      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é
298
      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.
299
      car cette information est redondante dans cel_image et devrait être supprimée.
300
    */
300
    */
301
    static function addAuteursConstraint($val, $db, &$where) {
301
    static function addAuteursConstraint($val, $db, &$where) {
302
	@list($a, $b) = explode(' ', $val, 2);
302
	@list($a, $b) = explode(' ', $val, 2);
303
	// un seul terme
303
	// un seul terme
304
	$champs_n = array('du.prenom', // info user authentifié de l'obs depuis l'annuaire
304
	$champs_n = array('du.prenom', // info user authentifié de l'obs depuis l'annuaire
305
			  'vdi.prenom_utilisateur', // info user anonyme de l'obs
305
			  'vdi.prenom_utilisateur', // info user anonyme de l'obs
306
			  /* 'vdi.i_prenom_utilisateur' */ ); // info user anonyme de l'image
306
			  /* 'vdi.i_prenom_utilisateur' */ ); // info user anonyme de l'image
307
	$champs_p = array('du.nom', // idem pour le nom
307
	$champs_p = array('du.nom', // idem pour le nom
308
			  'vdi.nom_utilisateur',
308
			  'vdi.nom_utilisateur',
309
			  /* 'vdi.i_nom_utilisateur' */ );
309
			  /* 'vdi.i_nom_utilisateur' */ );
310
 
310
 
311
	/*
311
	/*
312
	  Note: pour l'heure, étant donnés:
312
	  Note: pour l'heure, étant donnés:
313
	  - les CONVERT() de la VIEW del_utilisateur
313
	  - les CONVERT() de la VIEW del_utilisateur
314
	  - DEFAULT CHARSET=latin1 pour tela_prod_v4.annuaire_tela
314
	  - DEFAULT CHARSET=latin1 pour tela_prod_v4.annuaire_tela
315
	  - DEFAULT CHARSET=utf8 pour tb_cel.cel_obs
315
	  - DEFAULT CHARSET=utf8 pour tb_cel.cel_obs
316
	  et l'âge du capitaine...
316
	  et l'âge du capitaine...
317
	  - REGEXP est case-sensitive, et collate les caractères accentués
317
	  - REGEXP est case-sensitive, et collate les caractères accentués
318
	  - LIKE est case-insensitive, et collate les caractères accentués
318
	  - LIKE est case-insensitive, et collate les caractères accentués
319
	*/
319
	*/
320
	if(! $b) {
320
	if(! $b) {
321
	    $where[] = sprintf('CONCAT(%s,%s) LIKE %s',
321
	    $where[] = sprintf('CONCAT(%s,%s) LIKE %s',
322
			       DelTk::sqlAddIfNullPourConcat($champs_n),
322
			       DelTk::sqlAddIfNullPourConcat($champs_n),
323
			       DelTk::sqlAddIfNullPourConcat($champs_p),
323
			       DelTk::sqlAddIfNullPourConcat($champs_p),
324
			       $db->proteger("%".$val."%"));
324
			       $db->proteger("%".$val."%"));
325
	}
325
	}
326
	else {
326
	else {
327
	    $where[] = sprintf('(CONCAT(%1$s,%2$s) LIKE %3$s AND CONCAT(%1$s,%2$s) LIKE %4$s)',
327
	    $where[] = sprintf('(CONCAT(%1$s,%2$s) LIKE %3$s AND CONCAT(%1$s,%2$s) LIKE %4$s)',
328
			       DelTk::sqlAddIfNullPourConcat($champs_n),
328
			       DelTk::sqlAddIfNullPourConcat($champs_n),
329
			       DelTk::sqlAddIfNullPourConcat($champs_p),
329
			       DelTk::sqlAddIfNullPourConcat($champs_p),
330
			       $db->proteger("%" . $a . "%"), $db->proteger("%" . $b . "%"));
330
			       $db->proteger("%" . $a . "%"), $db->proteger("%" . $b . "%"));
331
	}
331
	}
332
    }
332
    }
333
 
333
 
334
 
334
 
335
 
335
 
336
 
336
 
337
 
337
 
338
    /**
338
    /**
339
     * - Rempli le tableau des contraintes "where" et "join" nécessaire
339
     * - Rempli le tableau des contraintes "where" et "join" nécessaire
340
     * à la *recherche* des observations demandées ($req) utilisées par self::getIdObs()
340
     * à la *recherche* des observations demandées ($req) utilisées par self::getIdObs()
341
     *
341
     *
342
     * Attention, cela signifie que toutes les tables ne sont pas *forcément*
342
     * Attention, cela signifie que toutes les tables ne sont pas *forcément*
343
     * join'ées, par exemple si aucune contrainte ne le nécessite.
343
     * 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
344
     * $req tel qu'il est rempli ici est utile pour récupéré la seule liste des
345
     * id d'observation qui match.
345
     * id d'observation qui match.
346
     * Pour la récupération effective de "toutes" les données correspondante, il faut
346
     * 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.
347
     * réinitialiser $req["join"] afin d'y ajouter toutes les autres tables.
348
     *
348
     *
349
     * Note: toujours rajouter les préfixes de table (vdi,du,doi ou di), en fonction de ce que défini
349
     * 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.
350
     * les JOIN qui sont utilisés.
351
     * le préfix de v_del_image est "vdi" (cf: "FROM" de self::getIdObs())
351
     * 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"
352
     * le préfix de del_utilisateur sur id_utilisateur = vdi.ce_utilisateur est "du"
353
     *
353
     *
354
     * @param $p les paramètres (notamment de masque) passés par l'URL et déjà traités/filtrés (sauf quotes)
354
     * @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
355
     * @param $req le tableau, passé par référence représentant les composants de la requête à bâtir
356
     */
356
     */
357
    static function sqlAddConstraint($p, $db, &$req) {
357
    static function sqlAddConstraint($p, $db, &$req) {
358
	if(!empty($p['masque.auteur'])) {
358
	if(!empty($p['masque.auteur'])) {
359
	    // id du poster de l'obs
359
	    // id du poster de l'obs
360
	    $req['join'][] = 'LEFT JOIN del_utilisateur AS du ON du.id_utilisateur = vdi.ce_utilisateur';
360
	    $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
361
	    // 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
362
	    // 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';
363
	    // $req['join'][] = 'LEFT JOIN del_utilisateur AS dui ON dui.id_utilisateur = vdi.i_ce_utilisateur';
364
 
364
 
365
	    if(is_numeric($p['masque.auteur'])) {
365
	    if(is_numeric($p['masque.auteur'])) {
366
		$req['where'][] = sprintf('(du.id_utilisateur = %1$d OR vdi.id_utilisateur = %1$d)', $p['masque.auteur']);
366
		$req['where'][] = sprintf('(du.id_utilisateur = %1$d OR vdi.id_utilisateur = %1$d)', $p['masque.auteur']);
367
	    }
367
	    }
368
	    elseif(preg_match(';^.{5,}@[a-z0-9-.]{5,}$;i', $p['masque.auteur'])) {
368
	    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 )',
369
		$req['where'][] = sprintf('(du.courriel LIKE %1$s OR vdi.courriel LIKE %1$s )',
370
					  $db->proteger($p['masque.auteur'] . '%'));
370
					  $db->proteger($p['masque.auteur'] . '%'));
371
	    }
371
	    }
372
	    else {
372
	    else {
373
		DelTk::addAuteursConstraint($p['masque.auteur'], $db, $req['where']);
373
		DelTk::addAuteursConstraint($p['masque.auteur'], $db, $req['where']);
374
	    }
374
	    }
375
	}
375
	}
376
 
376
 
377
	if(!empty($p['masque.date'])) {
377
	if(!empty($p['masque.date'])) {
378
	    if(is_integer($p['masque.date']) && $p['masque.date'] < 2030 && $p['masque.date'] > 1600) {
378
	    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']);
379
		$req['where'][] = sprintf("YEAR(vdi.date_observation) = %d", $p['masque.date']);
380
	    }
380
	    }
381
	    else {
381
	    else {
382
		$req['where'][] = sprintf("DATE_FORMAT(vdi.date_observation, '%%Y-%%m-%%d') = %s",
382
		$req['where'][] = sprintf("DATE_FORMAT(vdi.date_observation, '%%Y-%%m-%%d') = %s",
383
					  $db->proteger(strftime('%Y-%m-%d', $p['masque.date'])));
383
					  $db->proteger(strftime('%Y-%m-%d', $p['masque.date'])));
384
	    }
384
	    }
385
	}
385
	}
386
 
386
 
387
	// TODO: avoir des champs d'entrée distinct
387
	// TODO: avoir des champs d'entrée distinct
388
	if(!empty($p['masque.departement'])) {
388
	if(!empty($p['masque.departement'])) {
389
	    $req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger('INSEE-C:'.$p['masque.departement']));
389
	    $req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger('INSEE-C:'.$p['masque.departement']));
390
	}
390
	}
391
	if(!empty($p['masque.id_zone_geo'])) {
391
	if(!empty($p['masque.id_zone_geo'])) {
392
	    $req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger($p['masque.id_zone_geo']));
392
	    $req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger($p['masque.id_zone_geo']));
393
	}
393
	}
394
	if(!empty($p['masque.genre'])) {
394
	if(!empty($p['masque.genre'])) {
395
	    $req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger('%' . $p['masque.genre'].'% %');
395
	    $req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger('%' . $p['masque.genre'].'% %');
396
	}
396
	}
397
	if(!empty($p['masque.famille'])) {
397
	if(!empty($p['masque.famille'])) {
398
	    $req['where'][] = 'vdi.famille = '.$db->proteger($p['masque.famille']);
398
	    $req['where'][] = 'vdi.famille = '.$db->proteger($p['masque.famille']);
399
	}
399
	}
400
	if(!empty($p['masque.ns'])) {
400
	if(!empty($p['masque.ns'])) {
401
	    $req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger($p['masque.ns'].'%');
401
	    $req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger($p['masque.ns'].'%');
402
	}
402
	}
403
	if(!empty($p['masque.nn'])) {
403
	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']);
404
	    $req['where'][] = sprintf('(vdi.nom_sel_nn = %1$d OR vdi.nom_ret_nn = %1$d)', $p['masque.nn']);
405
	}
405
	}
406
	if(!empty($p['masque.referentiel'])) {
406
	if(!empty($p['masque.referentiel'])) {
407
	    $req['where'][] = sprintf('vdi.nom_referentiel LIKE %s', $db->proteger($p['masque.referentiel'].'%'));
407
	    $req['where'][] = sprintf('vdi.nom_referentiel LIKE %s', $db->proteger($p['masque.referentiel'].'%'));
408
	}
408
	}
409
	if(!empty($p['masque.commune'])) {
409
	if(!empty($p['masque.commune'])) {
410
	    $req['where'][] = 'vdi.zone_geo LIKE '.$db->proteger($p['masque.commune'].'%');
410
	    $req['where'][] = 'vdi.zone_geo LIKE '.$db->proteger($p['masque.commune'].'%');
411
	}
411
	}
412
    }
412
    }
413
 
413
 
414
 
414
 
415
 
415
 
416
    // formatage de réponse HTTP
416
    // formatage de réponse HTTP
417
    static function makeJSONHeader($total, $params, $url_service) {
417
    static function makeJSONHeader($total, $params, $url_service) {
418
	$prev_url = $next_url = NULL;
418
	$prev_url = $next_url = NULL;
419
	$url_service_sans_slash = substr($url_service, 0, -1);
419
	$url_service_sans_slash = substr($url_service, 0, -1);
420
 
420
 
421
	// aplatissons les params! - une seule couche cela dit, après débrouillez-vous
421
	// aplatissons les params! - une seule couche cela dit, après débrouillez-vous
422
	$params_a_plat = $params;
422
	$params_a_plat = $params;
423
	foreach ($params_a_plat as $cle_plate => $pap) {
423
	foreach ($params_a_plat as $cle_plate => $pap) {
424
	    if (is_array($pap)) {
424
	    if (is_array($pap)) {
425
		$params_a_plat[$cle_plate] = implode(array_keys($pap), ',');
425
		$params_a_plat[$cle_plate] = implode(array_keys($pap), ',');
426
	    }
426
	    }
427
	}
427
	}
428
 
428
 
429
	$next_offset = $params['navigation.depart'] + $params['navigation.limite'];
429
	$next_offset = $params['navigation.depart'] + $params['navigation.limite'];
430
	if($next_offset < $total) {
430
	if($next_offset < $total) {
431
	    $next_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $next_offset)));
431
	    $next_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $next_offset)));
432
	}
432
	}
433
 
433
 
434
	$prev_offset = $params['navigation.depart'] - $params['navigation.limite'];
434
	$prev_offset = $params['navigation.depart'] - $params['navigation.limite'];
435
	if($prev_offset >= 0) {
435
	if($prev_offset >= 0) {
436
	    $prev_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $prev_offset)));
436
	    $prev_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $prev_offset)));
437
	}
437
	}
438
 
438
 
439
	return array(
439
	return array(
440
	    'masque' => http_build_query(array_diff_key($params, array_flip(array('navigation.depart', 'navigation.limite')))),
440
	    'masque' => http_build_query(array_diff_key($params, array_flip(array('navigation.depart', 'navigation.limite')))),
441
	    'total' => $total,
441
	    'total' => $total,
442
	    'depart' => $params['navigation.depart'],
442
	    'depart' => $params['navigation.depart'],
443
	    'limite' => $params['navigation.limite'],
443
	    'limite' => $params['navigation.limite'],
444
	    'href.precedent' => $prev_url,
444
	    'href.precedent' => $prev_url,
445
	    'href.suivant' => $next_url
445
	    'href.suivant' => $next_url
446
	);
446
	);
447
    }
447
    }
448
}
448
}