Subversion Repositories eFlore/Applications.del

Rev

Go to most recent revision | Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1490 raphael 1
<?php
2
/**
3
 * DEL (Détermination en ligne [Pictoflora/Identiplante]) Toolkit
4
 * Quelques fonctions utiles, utilisées et/ou utilisables aussi bien par images/*, observations/*
5
 * et probablement d'autres, comme determination/*.
6
 *
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
9
 * - génération de SQL
10
 * - processing de tableau de pattern d'utilisation SQL assez commun
11
 * - formattage basique de sortie (JSON)
12
 * + quelques helpers basiques
13
 *
14
 * @category	php 5.2
15
 * @package		del
16
 * @author		Raphaël Droz <raphael@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
19
 * @license	http://www.gnu.org/licenses/gpl.html Licence GNU-GPL
20
 */
21
 
22
 
23
define('_LISTE_OBS_MAX_RESULT_LIMIT', 1000);
24
define('_LISTE_OBS_MAX_ID_OBS', 10e7);
25
// SELECT MAX(num_taxonomique) FROM bdtfx_v2_00;
26
define('_LISTE_OBS_MAX_BDTFX_NT', 1000000); // 44378 + 1000
27
// SELECT MAX(num_nom) FROM bdtfx_v2_00;
28
define('_LISTE_OBS_MAX_BDTFX_NN', 1000000); // 120816 + 10000
29
 
30
class DelTk {
31
	static $parametres_autorises = array(
32
        'masque', 'masque.famille', 'masque.nn', 'masque.referentiel', // taxon
33
        'masque.genre', 'masque.espece', 'masque.ns', // nom_sel
34
        'masque.commune', 'masque.departement', 'masque.id_zone_geo', // loc
35
        'masque.auteur', 'masque.date', 'masque.tag', 'masque.type', // autres
36
        // tri, offset
37
        'navigation.depart', 'navigation.limite',
38
        'tri', 'ordre', // TODO: 'total=[yes]', 'fields=[x,y,...]'
39
        // TODO: masque.annee, masque.insee (!= departement)
40
	);
41
 
42
	static $default_params = array(
43
        'navigation.depart' => 0, 'navigation.limite' => 10,
44
        'tri' => 'date_transmission', 'ordre' => 'desc');
45
 
46
 
47
    // input filtering
48
 
49
 
50
	/* Construit un (vulgaire) abstract syntax tree:
51
	   "AND" => [ "tag1", "tag2" ]
52
	   Idéalement (avec un parser simple comme proposé par http://hoa-project.net/Literature/Hack/Compiler.html#Langage_PP)
53
	   nous aurions:
54
	   "AND" => [ "tag1", "tag2", "OR" => [ "tag3", "tag4" ] ]
55
 
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.
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é.
60
 
61
	   Théorie:
62
	   1) tags passés par "champ tag":
63
	   - support du ET/OU, et explode par virgule.
64
	   - si pas d'opérande détectée: "OU"
65
 
66
	   2) tags passés par "recherche générale":
67
	   - support du ET/OU, et explode par whitespace.
68
	   - si pas d'opérande détectée: "ET"
69
 
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
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"
74
	   * a,b AND c => "a" AND "b" AND "c"
75
	   * a OR b AND c,d => "a" AND "b" AND "c" AND "d"
76
	   C'est à dire par ordre décroissant de priorité:
77
	   1) opérande contenu dans la chaîne
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)
80
 
81
	   // TODO: support des parenthèses, imbrications & co: "(", ")"
82
	   // http://codehackit.blogspot.fr/2011/08/expression-parser-in-php.html
83
	   // http://blog.angeloff.name/post/2012/08/05/php-recursive-patterns/
84
 
85
	   @param $str: la chaîne à "parser"
86
	   @param $default_op: "AND" ou "OR"
87
	   @param $additional_sep: séparateur de mots:
88
	*/
89
	static function buildTagsAST($str = NULL, $default_op, $additional_sep = ',') {
90
		if(!$str) return;
91
		$words = preg_split('/ (OR|AND|ET|OU) /', $str, -1, PREG_SPLIT_NO_EMPTY);
92
 
93
		if(preg_match('/\b(ET|AND)\b/', $str)) $op = 'AND';
94
		elseif(preg_match('/\b(OU|OR)\b/', $str)) $op = 'OR';
95
		else $op = $default_op;
96
 
97
		if($additional_sep) {
98
			array_walk($words,
99
            create_function('&$v, $k, $sep', '$v = preg_split("/".$sep."/", $v, -1, PREG_SPLIT_NO_EMPTY);'),
100
            $additional_sep);
101
		}
102
		$words = DelTk::array_flatten($words);
103
		$words = array_map('trim', $words);
104
		return array($op => array_filter($words));
105
	}
106
 
107
 
108
	static function array_flatten($arr) {
109
		$arr = array_values($arr);
110
		while (list($k,$v)=each($arr)) {
111
			if (is_array($v)) {
112
				array_splice($arr,$k,1,$v);
113
				next($arr);
114
			}
115
		}
116
		return $arr;
117
	}
118
 
119
	// supprime l'index du tableau des paramètres si sa valeur ne correspond pas
120
	// au spectre passé par $values.
121
	static function unsetIfInvalid(&$var, $index, $values) {
122
		if(array_key_exists($index, $var)) {
123
			if(!in_array($var[$index], $values)) unset($var[$index]);
124
			else return $var[$index];
125
		}
126
		return NULL;
127
	}
128
 
129
 
130
 
131
 
132
	/* Filtre et valide les paramètres reconnus. Effectue *toute* la sanitization *sauf* l'escape-string
133
	   Cette fonction est appelée:
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,
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
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
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 */ ) {
142
		if($parametres_autorises) { // filtrage de toute clef inconnue
143
			$params = array_intersect_key($params, array_flip($parametres_autorises));
144
		}
145
 
146
		$p['tri'] = DelTK::unsetIfInvalid($params, 'tri', array('date_observation'));
147
		$p['ordre'] = DelTK::unsetIfInvalid($params, 'ordre', array('asc','desc'));
148
		$p['masque.referentiel'] = DelTK::unsetIfInvalid($params, 'masque.referentiel', array('bdtfx','bdtxa','isfan'));
149
 
150
		// TODO: use filter_input(INPUT_GET);
151
		// renvoie FALSE ou NULL si absent ou invalide
152
		$p['navigation.limite'] = filter_var(@$params['navigation.limite'],
153
		FILTER_VALIDATE_INT,
154
		array('options' => array('default' => NULL,
155
		'min_range' => 1,
156
		'max_range' => _LISTE_OBS_MAX_RESULT_LIMIT)));
157
		$p['navigation.depart'] = filter_var(@$params['navigation.depart'],
158
		FILTER_VALIDATE_INT,
159
		array('options' => array('default' => NULL,
160
		'min_range' => 0,
161
		'max_range' => _LISTE_OBS_MAX_ID_OBS)));
162
		if(isset($params['masque.departement'])) {
163
			// STRING: 0 -> 95, 971 -> 976, 2A + 2B (./services/configurations/config_departements_bruts.ini)
164
			// accept leading 0 ?
165
			// TODO; filter patterns like 555.
166
			if(preg_match(';^(\d{2}|\d{3}|2a|2b)$;i', $params['masque.departement'])) {
167
				$p['masque.departement'] = $params['masque.departement'];
168
			}
169
			// cf configurations/config_departements_bruts.ini
170
			elseif( !is_null($c) && ( $x = $c->getParametre(
171
				strtolower(str_replace(' ','-',iconv("UTF-8", "ASCII//TRANSLIT", $params['masque.departement'])))
172
			))) {
173
				$p['masque.departement'] = sprintf("INSEE-C:%02d___", $x);
174
			}
175
		}
176
 
177
		if(isset($params['masque.date'])) {
178
			// une année, TODO: masque.annee
179
			if(is_numeric($params['masque.date'])) {
180
				$p['masque.date'] = $params['masque.date'];
181
			}
182
			elseif(strpos($params['masque.date'], '/' !== false) &&
183
			($x = strtotime(str_replace('/','-',$params['masque.date'])))) {
184
				$p['masque.date'] = $x;
185
			}
186
			elseif(strpos($params['masque.date'], '-' !== false) &&
187
			($x = strtotime($params['masque.date'])) ) {
188
				$p['masque.date'] = $x;
189
			}
190
		}
191
 
192
		$p['masque.nn'] = filter_var(@$params['masque.nn'],
193
		FILTER_VALIDATE_INT,
194
		array('options' => array('default' => NULL,
195
		'min_range' => 0,
196
		'max_range' => _LISTE_OBS_MAX_BDTFX_NN)));
197
 
198
		$p['masque.nt'] = filter_var(@$params['masque.nt'],
199
		FILTER_VALIDATE_INT,
200
		array('options' => array('default' => NULL,
201
		'min_range' => 0,
202
		'max_range' => _LISTE_OBS_MAX_BDTFX_NT)));
203
 
204
 
205
		// TODO: should we really trim() ?
206
 
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']);
209
 
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"
212
			$p['masque.famille'] = preg_replace('/[^a-zA-Z %_]/', '', iconv("UTF-8",
213
			"ASCII//TRANSLIT",
214
			$params['masque.famille']));
215
		}
216
 
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>% %'.
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 '_'
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']);
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']);
225
 
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'])) {
228
			$p['masque.id_zone_geo'] = $params['masque.id_zone_geo'];
229
		}
230
 
231
		// masque.commune (zone_geo)
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
234
		if(isset($params['masque.commune'])) $p['masque.commune'] = str_replace(array('-',' '), '_', $params['masque.commune']);
235
 
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']);
238
		// sera trimmé plus tard, cf sqlAddConstraint
239
		if(isset($params['masque'])) $p['masque'] = trim($params['masque']);
240
 
241
		// masque.tag, idem que pour masque.genre et masque.commune
242
		if(isset($params['masque.tag'])) {
243
			$x = explode(',',$params['masque.tag']);
244
			$x = array_map('trim', $x);
245
			$p['masque.tag'] = implode('|', array_filter($x));
246
		}
247
 
248
		// masque.type: ['adeterminer', 'aconfirmer', 'endiscussion', 'validees']
249
		if(isset($params['masque.type'])) {
250
			$p['masque.type'] = array_flip(array_intersect(array_filter(explode(';', $params['masque.type'])),
251
			array('adeterminer', 'aconfirmer', 'endiscussion', 'validees')));
252
		}
253
 
254
 
255
		// TODO: masque (général)
256
 
257
 
258
		// on filtre les NULL, FALSE et '', mais pas les 0, d'où le callback()
259
		// TODO: PHP-5.3
260
		return array_filter($p, create_function('$a','return !in_array($a, array("",false,null),true);'));
261
	}
262
 
263
 
264
 
265
    // SQL helpers
266
 
267
	/* Lorsque l'on concatène des champs, un seul NULL prend le dessus,
268
	   Il faut donc utiliser la syntaxe IFNULL(%s, "").
269
	   (Cette fonction effectue aussi l'implode() "final" */
270
	static function sqlAddIfNullPourConcat($tab) {
271
		// XXX: PHP-5.3
272
		return implode(',',array_map(create_function('$a', 'return "IFNULL($a, \"\")";'), $tab));
273
	}
274
 
275
 
276
 
277
	/* Converti un tableau associatif et un préfix optionnel en une chaîne de champs adéquate
278
	   à un SELECT MySQL.
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
281
	   le SELECT en sortie */
282
	static function sqlFieldsToAlias($map, $select = NULL, $prefix = NULL) {
283
		if($select) $arr = array_intersect_key($map, array_flip($select));
284
		else $arr = $map;
285
		$keys = array_keys($arr);
286
 
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);'));
289
 
290
		return implode(', ', array_map(create_function('$v, $k', 'return sprintf("%s AS `%s`", $k, $v);'), $arr, $keys));
291
	}
292
 
293
 
294
 
295
	/*
296
	  Retourne une clause where du style:
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é
299
	  car cette information est redondante dans cel_image et devrait être supprimée.
300
	*/
301
	static function addAuteursConstraint($val, $db, &$where) {
302
		@list($a, $b) = explode(' ', $val, 2);
303
		// un seul terme
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
306
		/* 'vdi.i_prenom_utilisateur' */ ); // info user anonyme de l'image
307
		$champs_p = array('du.nom', // idem pour le nom
308
		'vdi.nom_utilisateur',
309
		/* 'vdi.i_nom_utilisateur' */ );
310
 
311
		/*
312
		  Note: pour l'heure, étant donnés:
313
		  - les CONVERT() de la VIEW del_utilisateur
314
		  - DEFAULT CHARSET=latin1 pour tela_prod_v4.annuaire_tela
315
		  - DEFAULT CHARSET=utf8 pour tb_cel.cel_obs
316
		  et l'âge du capitaine...
317
		  - REGEXP est case-sensitive, et collate les caractères accentués
318
		  - LIKE est case-insensitive, et collate les caractères accentués
319
		*/
320
		if(! $b) {
321
			$where[] = sprintf('CONCAT(%s,%s) LIKE %s',
322
			DelTk::sqlAddIfNullPourConcat($champs_n),
323
			DelTk::sqlAddIfNullPourConcat($champs_p),
324
			$db->proteger("%".$val."%"));
325
		}
326
		else {
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),
329
			DelTk::sqlAddIfNullPourConcat($champs_p),
330
			$db->proteger("%" . $a . "%"), $db->proteger("%" . $b . "%"));
331
		}
332
	}
333
 
334
 
335
 
336
 
337
 
338
	/**
339
	 * - Rempli le tableau des contraintes "where" et "join" nécessaire
340
	 * à la *recherche* des observations demandées ($req) utilisées par self::getIdObs()
341
	 *
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.
344
	 * $req tel qu'il est rempli ici est utile pour récupéré la seule liste des
345
	 * id d'observation qui match.
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.
348
	 *
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.
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"
353
	 *
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
356
	 */
357
	static function sqlAddConstraint($p, $db, &$req) {
358
		if(!empty($p['masque.auteur'])) {
359
			// id du poster de l'obs
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
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';
364
 
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']);
367
			}
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 )',
370
				$db->proteger($p['masque.auteur'] . '%'));
371
			}
372
			else {
373
				DelTk::addAuteursConstraint($p['masque.auteur'], $db, $req['where']);
374
			}
375
		}
376
 
377
		if(!empty($p['masque.date'])) {
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']);
380
			}
381
			else {
382
				$req['where'][] = sprintf("DATE_FORMAT(vdi.date_observation, '%%Y-%%m-%%d') = %s",
383
				$db->proteger(strftime('%Y-%m-%d', $p['masque.date'])));
384
			}
385
		}
386
 
387
		// TODO: avoir des champs d'entrée distinct
388
		if(!empty($p['masque.departement'])) {
389
			$req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger('INSEE-C:'.$p['masque.departement']));
390
		}
391
		if(!empty($p['masque.id_zone_geo'])) {
392
			$req['where'][] = sprintf("vdi.ce_zone_geo = %s", $db->proteger($p['masque.id_zone_geo']));
393
		}
394
		if(!empty($p['masque.genre'])) {
395
			$req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger('%' . $p['masque.genre'].'% %');
396
		}
397
		if(!empty($p['masque.famille'])) {
398
			$req['where'][] = 'vdi.famille = '.$db->proteger($p['masque.famille']);
399
		}
400
		if(!empty($p['masque.ns'])) {
401
			$req['where'][] = 'vdi.nom_sel LIKE '.$db->proteger($p['masque.ns'].'%');
402
		}
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']);
405
		}
406
		if(!empty($p['masque.referentiel'])) {
407
			$req['where'][] = sprintf('vdi.nom_referentiel LIKE %s', $db->proteger($p['masque.referentiel'].'%'));
408
		}
409
		if(!empty($p['masque.commune'])) {
410
			$req['where'][] = 'vdi.zone_geo LIKE '.$db->proteger($p['masque.commune'].'%');
411
		}
412
		if(!empty($p['masque.tag'])) {
413
			// TODO: remove LOWER() lorsqu'on est sur que les tags sont uniformés en minuscule
414
			// i_mots_cles_texte provient de la VIEW v_del_image
415
			if(isset($p['masque.tag']['AND'])) {
416
                /* Lorsque nous interprêtons la chaîne provenant du masque général (cf: buildTagsAST($p['masque'], 'OR', ' ') dans sqlAddMasqueConstraint()),
417
                   nous sommes splittés par espace. Cependant, assurons que si une virgule à été saisie, nous n'aurons pas le motif
418
                   " AND CONCAT(mots_cles_texte, i_mots_cles_texte) REGEXP ',' " dans notre requête.
419
                   XXX: Au 12/11/2013, une recherche sur tag depuis le masque général implique un OU, donc le problème ne se pose pas ici */
420
				$subwhere = array();
421
				foreach($p['masque.tag']['AND'] as $tag) {
422
                    if(trim($tag) == ',') continue;
423
 
424
					$subwhere[] = sprintf(
425
						'LOWER(CONCAT(%s)) REGEXP %s',
426
						DelTk::sqlAddIfNullPourConcat(array('vdi.mots_cles_texte', 'vdi.i_mots_cles_texte')),
427
						$db->proteger(strtolower($tag)));
428
				}
429
				$req['where'][] = '(' . implode(' AND ', $subwhere) . ')';
430
			}
431
			else {
432
				$req['where'][] = sprintf(
433
					'LOWER(CONCAT(%s)) REGEXP %s',
434
					DelTk::sqlAddIfNullPourConcat(array('vdi.mots_cles_texte', 'vdi.i_mots_cles_texte')),
435
					$db->proteger(strtolower(implode('|', $p['masque.tag']['OR']))));
436
			}
437
		}
438
    }
439
 
440
 
441
 
442
 
443
 
444
 
445
 
446
    // formatage de réponse HTTP
447
	static function makeJSONHeader($total, $params, $url_service) {
448
		$prev_url = $next_url = NULL;
449
		$url_service_sans_slash = substr($url_service, 0, -1);
450
 
451
		// aplatissons les params! - une seule couche cela dit, après débrouillez-vous
452
		$params_a_plat = $params;
453
		foreach ($params_a_plat as $cle_plate => $pap) {
454
			if (is_array($pap)) {
455
				$params_a_plat[$cle_plate] = implode(array_keys($pap), ',');
456
			}
457
		}
458
 
459
		$next_offset = $params['navigation.depart'] + $params['navigation.limite'];
460
		if($next_offset < $total) {
461
			$next_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $next_offset)));
462
		}
463
 
464
		$prev_offset = $params['navigation.depart'] - $params['navigation.limite'];
465
		if($prev_offset >= 0) {
466
			$prev_url = $url_service_sans_slash . '?' . http_build_query(array_merge($params_a_plat, array('navigation.depart' => $prev_offset)));
467
		}
468
 
469
		return array(
470
			'masque' => http_build_query(array_diff_key($params, array_flip(array('navigation.depart', 'navigation.limite')))),
471
			'total' => $total,
472
			'depart' => $params['navigation.depart'],
473
			'limite' => $params['navigation.limite'],
474
			'href.precedent' => $prev_url,
475
			'href.suivant' => $next_url
476
		);
477
	}
478
 
479
}