Subversion Repositories eFlore/Applications.del

Rev

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