Subversion Repositories eFlore/Applications.del

Rev

Rev 1597 | 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)));
1596 aurelien 162
    // STRING: 0 -> 95, 971 -> 976, 2A + 2B (./services/configurations/config_departements_bruts.ini)
163
	// accept leading 0 ?
164
	// TODO; filter patterns like 555.
165
	if (preg_match(';^(\d{2}|\d{3}|2a|2b)$;i', $params['masque.departement']) != 0) {
166
		if(is_numeric($params['masque.departement'])) {
167
			$p['masque.departement'] = str_pad($params['masque.departement'], 5, '_');
168
		} else {
169
			$p['masque.departement'] = $params['masque.departement'];
170
		}
171
	} else {
172
		$dpt_chaine = strtolower(str_replace(' ','-',iconv("UTF-8", "ASCII//TRANSLIT", $params['masque.departement'])));
173
		$c->chargerConfiguration('config_departements_bruts.ini');
174
		$p['masque.departement'] = str_pad($c->getParametre($dpt_chaine), 5, '_');
1494 raphael 175
	}
1490 raphael 176
 
1494 raphael 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
	}
1490 raphael 191
 
1494 raphael 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)));
1490 raphael 197
 
1494 raphael 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)));
1490 raphael 203
 
204
 
1494 raphael 205
	// TODO: should we really trim() ?
1490 raphael 206
 
1494 raphael 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']);
1490 raphael 209
 
1494 raphael 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
	}
1490 raphael 216
 
1494 raphael 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']);
1490 raphael 225
 
1494 raphael 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
	}
1490 raphael 230
 
1494 raphael 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']);
1490 raphael 235
 
1494 raphael 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']);
1490 raphael 240
 
1494 raphael 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
	}
1490 raphael 247
 
1494 raphael 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
	}
1490 raphael 253
 
254
 
1494 raphael 255
	// TODO: masque (général)
1490 raphael 256
 
257
 
1494 raphael 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
    }
1490 raphael 262
 
263
 
264
 
265
    // SQL helpers
266
 
1494 raphael 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
    }
1490 raphael 274
 
275
 
276
 
1494 raphael 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);
1490 raphael 286
 
1494 raphael 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);'));
1490 raphael 289
 
1494 raphael 290
	return implode(', ', array_map(create_function('$v, $k', 'return sprintf("%s AS `%s`", $k, $v);'), $arr, $keys));
291
    }
1490 raphael 292
 
293
 
294
 
1494 raphael 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
 
1490 raphael 311
	/*
1494 raphael 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
1490 raphael 319
	*/
1494 raphael 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."%"));
1490 raphael 325
	}
1494 raphael 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
    }
1490 raphael 333
 
334
 
335
 
336
 
337
 
1494 raphael 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';
1490 raphael 364
 
1494 raphael 365
	    if(is_numeric($p['masque.auteur'])) {
1607 aurelien 366
		$req['where'][] = sprintf('(du.id_utilisateur = %1$d OR vdi.ce_utilisateur = %1$d)', $p['masque.auteur']);
1494 raphael 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
	}
1490 raphael 376
 
1494 raphael 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
	}
1490 raphael 386
 
1494 raphael 387
	// TODO: avoir des champs d'entrée distinct
388
	if(!empty($p['masque.departement'])) {
1597 aurelien 389
	    $req['where'][] = sprintf("vdi.ce_zone_geo LIKE %s", $db->proteger('INSEE-C:'.$p['masque.departement']));
1494 raphael 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'])) {
1511 aurelien 404
	    $req['where'][] = sprintf('(vdi.nom_sel_nn = %1$d OR vdi.nom_ret_nn = %1$d)', $p['masque.nn']);
1494 raphael 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
	}
1490 raphael 412
    }
413
 
414
 
415
 
416
    // formatage de réponse HTTP
1494 raphael 417
    static function makeJSONHeader($total, $params, $url_service) {
418
	$prev_url = $next_url = NULL;
419
	$url_service_sans_slash = substr($url_service, 0, -1);
1490 raphael 420
 
1494 raphael 421
	// aplatissons les params! - une seule couche cela dit, après débrouillez-vous
422
	$params_a_plat = $params;
423
	foreach ($params_a_plat as $cle_plate => $pap) {
424
	    if (is_array($pap)) {
425
		$params_a_plat[$cle_plate] = implode(array_keys($pap), ',');
426
	    }
427
	}
1490 raphael 428
 
1494 raphael 429
	$next_offset = $params['navigation.depart'] + $params['navigation.limite'];
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)));
432
	}
1490 raphael 433
 
1494 raphael 434
	$prev_offset = $params['navigation.depart'] - $params['navigation.limite'];
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)));
1490 raphael 437
	}
438
 
1494 raphael 439
	return array(
440
	    'masque' => http_build_query(array_diff_key($params, array_flip(array('navigation.depart', 'navigation.limite')))),
441
	    'total' => $total,
442
	    'depart' => $params['navigation.depart'],
443
	    'limite' => $params['navigation.limite'],
444
	    'href.precedent' => $prev_url,
445
	    'href.suivant' => $next_url
446
	);
447
    }
448
}