e44a293a50aed8dc9626f3cc93fabfd2dee8b24a
[lhc/web/www.git] / www / ecrire / public / criteres.php
1 <?php
2
3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
5 * *
6 * Copyright (c) 2001-2017 *
7 * Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James *
8 * *
9 * Ce programme est un logiciel libre distribue sous licence GNU/GPL. *
10 * Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne. *
11 \***************************************************************************/
12
13 /**
14 * Définition des {criteres} d'une boucle
15 *
16 * @package SPIP\Core\Compilateur\Criteres
17 **/
18
19 if (!defined('_ECRIRE_INC_VERSION')) {
20 return;
21 }
22
23 /**
24 * Une Regexp repérant une chaine produite par le compilateur,
25 * souvent utilisée pour faire de la concaténation lors de la compilation
26 * plutôt qu'à l'exécution, i.e. pour remplacer 'x'.'y' par 'xy'
27 **/
28 define('_CODE_QUOTE', ",^(\n//[^\n]*\n)? *'(.*)' *$,");
29
30
31 /**
32 * Compile le critère {racine}
33 *
34 * Ce critère sélectionne les éléments à la racine d'une hiérarchie,
35 * c'est à dire ayant id_parent=0
36 *
37 * @link http://www.spip.net/@racine
38 *
39 * @param string $idb Identifiant de la boucle
40 * @param array $boucles AST du squelette
41 * @param Critere $crit Paramètres du critère dans cette boucle
42 * @return void
43 **/
44 function critere_racine_dist($idb, &$boucles, $crit) {
45
46 $not = $crit->not;
47 $boucle = &$boucles[$idb];
48 $id_parent = isset($GLOBALS['exceptions_des_tables'][$boucle->id_table]['id_parent']) ?
49 $GLOBALS['exceptions_des_tables'][$boucle->id_table]['id_parent'] :
50 'id_parent';
51
52 $c = array("'='", "'$boucle->id_table." . "$id_parent'", 0);
53 $boucle->where[] = ($crit->not ? array("'NOT'", $c) : $c);
54 }
55
56
57 /**
58 * Compile le critère {exclus}
59 *
60 * Exclut du résultat l’élément dans lequel on se trouve déjà
61 *
62 * @link http://www.spip.net/@exclus
63 *
64 * @param string $idb Identifiant de la boucle
65 * @param array $boucles AST du squelette
66 * @param Critere $crit Paramètres du critère dans cette boucle
67 * @return void
68 **/
69 function critere_exclus_dist($idb, &$boucles, $crit) {
70 $not = $crit->not;
71 $boucle = &$boucles[$idb];
72 $id = $boucle->primary;
73
74 if ($not or !$id) {
75 return (array('zbug_critere_inconnu', array('critere' => $not . $crit->op)));
76 }
77 $arg = kwote(calculer_argument_precedent($idb, $id, $boucles));
78 $boucle->where[] = array("'!='", "'$boucle->id_table." . "$id'", $arg);
79 }
80
81
82 /**
83 * Compile le critère {doublons} ou {unique}
84 *
85 * Ce critères enlève de la boucle les éléments déjà sauvegardés
86 * dans un précédent critère {doublon} sur une boucle de même table.
87 *
88 * Il est possible de spécifier un nom au doublon tel que {doublons sommaire}
89 *
90 * @link http://www.spip.net/@doublons
91 *
92 * @param string $idb Identifiant de la boucle
93 * @param array $boucles AST du squelette
94 * @param Critere $crit Paramètres du critère dans cette boucle
95 * @return void
96 **/
97 function critere_doublons_dist($idb, &$boucles, $crit) {
98 $boucle = &$boucles[$idb];
99 $primary = $boucle->primary;
100
101 // la table nécessite une clé primaire, non composée
102 if (!$primary or strpos($primary, ',')) {
103 return (array('zbug_doublon_sur_table_sans_cle_primaire'));
104 }
105
106 $not = ($crit->not ? '' : 'NOT');
107
108 // le doublon s'applique sur un type de boucle (article)
109 $nom = "'" . $boucle->type_requete . "'";
110
111 // compléter le nom avec un nom précisé {doublons nom}
112 // on obtient $nom = "'article' . 'nom'"
113 if (isset($crit->param[0])) {
114 $nom .= "." . calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent);
115 }
116
117 // code qui déclarera l'index du stockage de nos doublons (pour éviter une notice PHP)
118 $init_comment = "\n\n\t// Initialise le(s) critère(s) doublons\n";
119 $init_code = "\tif (!isset(\$doublons[\$d = $nom])) { \$doublons[\$d] = ''; }\n";
120
121 // on crée un sql_in avec la clé primaire de la table
122 // et la collection des doublons déjà emmagasinés dans le tableau
123 // $doublons et son index, ici $nom
124
125 // debut du code "sql_in('articles.id_article', "
126 $debut_in = "sql_in('" . $boucle->id_table . '.' . $primary . "', ";
127 // lecture des données du doublon "$doublons[$doublon_index[] = "
128 // Attention : boucle->doublons désigne une variable qu'on affecte
129 $debut_doub = '$doublons[' . (!$not ? '' : ($boucle->doublons . "[]= "));
130
131 // le debut complet du code des doublons
132 $debut_doub = $debut_in . $debut_doub;
133
134 // nom du doublon "('article' . 'nom')]"
135 $fin_doub = "($nom)]";
136
137 // si on trouve un autre critère doublon,
138 // on fusionne pour avoir un seul IN, et on s'en va !
139 foreach ($boucle->where as $k => $w) {
140 if (strpos($w[0], $debut_doub) === 0) {
141 // fusionner le sql_in (du where)
142 $boucle->where[$k][0] = $debut_doub . $fin_doub . ' . ' . substr($w[0], strlen($debut_in));
143 // fusionner l'initialisation (du hash) pour faire plus joli
144 $x = strpos($boucle->hash, $init_comment);
145 $len = strlen($init_comment);
146 $boucle->hash =
147 substr($boucle->hash, 0, $x + $len) . $init_code . substr($boucle->hash, $x + $len);
148
149 return;
150 }
151 }
152
153 // mettre l'ensemble dans un tableau pour que ce ne soit pas vu comme une constante
154 $boucle->where[] = array($debut_doub . $fin_doub . ", '" . $not . "')");
155
156 // déclarer le doublon s'il n'existe pas encore
157 $boucle->hash .= $init_comment . $init_code;
158
159
160 # la ligne suivante avait l'intention d'eviter une collecte deja faite
161 # mais elle fait planter une boucle a 2 critere doublons:
162 # {!doublons A}{doublons B}
163 # (de http://article.gmane.org/gmane.comp.web.spip.devel/31034)
164 # if ($crit->not) $boucle->doublons = "";
165 }
166
167
168 /**
169 * Compile le critère {lang_select}
170 *
171 * Permet de restreindre ou non une boucle en affichant uniquement
172 * les éléments dans la langue en cours. Certaines boucles
173 * tel que articles et rubriques restreignent par défaut sur la langue
174 * en cours.
175 *
176 * Sans définir de valeur au critère, celui-ci utilise 'oui' comme
177 * valeur par défaut.
178 *
179 * @param string $idb Identifiant de la boucle
180 * @param array $boucles AST du squelette
181 * @param Critere $crit Paramètres du critère dans cette boucle
182 * @return void
183 **/
184 function critere_lang_select_dist($idb, &$boucles, $crit) {
185 if (!isset($crit->param[1][0]) or !($param = $crit->param[1][0]->texte)) {
186 $param = 'oui';
187 }
188 if ($crit->not) {
189 $param = ($param == 'oui') ? 'non' : 'oui';
190 }
191 $boucle = &$boucles[$idb];
192 $boucle->lang_select = $param;
193 }
194
195
196 /**
197 * Compile le critère {debut_xxx}
198 *
199 * Limite le nombre d'éléments affichés.
200 *
201 * Ce critère permet de faire commencer la limitation des résultats
202 * par une variable passée dans l’URL et commençant par 'debut_' tel que
203 * {debut_page,10}. Le second paramètre est le nombre de résultats à
204 * afficher.
205 *
206 * Note : il est plus simple d'utiliser le critère pagination.
207 *
208 * @param string $idb Identifiant de la boucle
209 * @param array $boucles AST du squelette
210 * @param Critere $crit Paramètres du critère dans cette boucle
211 * @return void
212 **/
213 function critere_debut_dist($idb, &$boucles, $crit) {
214 list($un, $deux) = $crit->param;
215 $un = $un[0]->texte;
216 $deux = $deux[0]->texte;
217 if ($deux) {
218 $boucles[$idb]->limit = 'intval($Pile[0]["debut' .
219 $un .
220 '"]) . ",' .
221 $deux .
222 '"';
223 } else {
224 calculer_critere_DEFAUT_dist($idb, $boucles, $crit);
225 }
226 }
227
228
229 /**
230 * Compile le critère `pagination` qui demande à paginer une boucle.
231 *
232 * Demande à paginer la boucle pour n'afficher qu'une partie des résultats,
233 * et gère l'affichage de la partie de page demandée par debut_xx dans
234 * dans l'environnement du squelette.
235 *
236 * Le premier paramètre indique le nombre d'éléments par page, le second,
237 * rarement utilisé permet de définir le nom de la variable désignant la
238 * page demandée (`debut_xx`), qui par défaut utilise l'identifiant de la boucle.
239 *
240 * @critere
241 * @see balise_PAGINATION_dist()
242 * @link http://www.spip.net/3367 Le système de pagination
243 * @link http://www.spip.net/4867 Le critère pagination
244 * @example
245 * ```
246 * {pagination}
247 * {pagination 20}
248 * {pagination #ENV{pages,5}} etc
249 * {pagination 20 #ENV{truc,chose}} pour utiliser la variable debut_#ENV{truc,chose}
250 * ```
251 *
252 * @param string $idb Identifiant de la boucle
253 * @param array $boucles AST du squelette
254 * @param Critere $crit Paramètres du critère dans cette boucle
255 * @return void
256 **/
257 function critere_pagination_dist($idb, &$boucles, $crit) {
258
259 $boucle = &$boucles[$idb];
260 // definition de la taille de la page
261 $pas = !isset($crit->param[0][0]) ? "''"
262 : calculer_liste(array($crit->param[0][0]), array(), $boucles, $boucle->id_parent);
263
264 if (!preg_match(_CODE_QUOTE, $pas, $r)) {
265 $pas = "((\$a = intval($pas)) ? \$a : 10)";
266 } else {
267 $r = intval($r[2]);
268 $pas = strval($r ? $r : 10);
269 }
270
271 // Calcul du nommage de la pagination si il existe.
272 // La nouvelle syntaxe {pagination 20, nom} est prise en compte et privilégiée mais on reste
273 // compatible avec l'ancienne car certains cas fonctionnent correctement
274 $type = "'$idb'";
275 // Calcul d'un nommage spécifique de la pagination si précisé.
276 // Syntaxe {pagination 20, nom}
277 if (isset($crit->param[0][1])) {
278 $type = calculer_liste(array($crit->param[0][1]), array(), $boucles, $boucle->id_parent);
279 } // Ancienne syntaxe {pagination 20 nom} pour compatibilité
280 elseif (isset($crit->param[1][0])) {
281 $type = calculer_liste(array($crit->param[1][0]), array(), $boucles, $boucle->id_parent);
282 }
283
284 $debut = ($type[0] !== "'") ? "'debut'.$type" : ("'debut" . substr($type, 1));
285 $boucle->modificateur['debut_nom'] = $type;
286 $partie =
287 // tester si le numero de page demande est de la forme '@yyy'
288 'isset($Pile[0][' . $debut . ']) ? $Pile[0][' . $debut . '] : _request(' . $debut . ");\n"
289 . "\tif(substr(\$debut_boucle,0,1)=='@'){\n"
290 . "\t\t" . '$debut_boucle = $Pile[0][' . $debut . '] = quete_debut_pagination(\'' . $boucle->primary . '\',$Pile[0][\'@' . $boucle->primary . '\'] = substr($debut_boucle,1),' . $pas . ',$iter);' . "\n"
291 . "\t\t" . '$iter->seek(0);' . "\n"
292 . "\t}\n"
293 . "\t" . '$debut_boucle = intval($debut_boucle)';
294
295 $boucle->hash .= '
296 $command[\'pagination\'] = array((isset($Pile[0][' . $debut . ']) ? $Pile[0][' . $debut . '] : null), ' . $pas . ');';
297
298 $boucle->total_parties = $pas;
299 calculer_parties($boucles, $idb, $partie, 'p+');
300 // ajouter la cle primaire dans le select pour pouvoir gerer la pagination referencee par @id
301 // sauf si pas de primaire, ou si primaire composee
302 // dans ce cas, on ne sait pas gerer une pagination indirecte
303 $t = $boucle->id_table . '.' . $boucle->primary;
304 if ($boucle->primary
305 and !preg_match('/[,\s]/', $boucle->primary)
306 and !in_array($t, $boucle->select)
307 ) {
308 $boucle->select[] = $t;
309 }
310 }
311
312
313 /**
314 * Compile le critère `recherche` qui permet de sélectionner des résultats
315 * d'une recherche.
316 *
317 * Le texte cherché est pris dans le premier paramètre `{recherche xx}`
318 * ou à défaut dans la clé `recherche` de l'environnement du squelette.
319 *
320 * @critere
321 * @link http://www.spip.net/3878
322 * @see inc_prepare_recherche_dist()
323 *
324 * @param string $idb Identifiant de la boucle
325 * @param array $boucles AST du squelette
326 * @param Critere $crit Paramètres du critère dans cette boucle
327 * @return void
328 **/
329 function critere_recherche_dist($idb, &$boucles, $crit) {
330
331 $boucle = &$boucles[$idb];
332
333 if (!$boucle->primary or strpos($boucle->primary, ',')) {
334 erreur_squelette(_T('zbug_critere_sur_table_sans_cle_primaire', array('critere' => 'recherche')), $boucle);
335
336 return;
337 }
338
339 if (isset($crit->param[0])) {
340 $quoi = calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent);
341 } else {
342 $quoi = '(isset($Pile[0]["recherche"])?$Pile[0]["recherche"]:(isset($GLOBALS["recherche"])?$GLOBALS["recherche"]:""))';
343 }
344
345 $_modificateur = var_export($boucle->modificateur, true);
346 $boucle->hash .= '
347 // RECHERCHE'
348 . ($crit->cond ? '
349 if (!strlen(' . $quoi . ')){
350 list($rech_select, $rech_where) = array("0 as points","");
351 } else' : '') . '
352 {
353 $prepare_recherche = charger_fonction(\'prepare_recherche\', \'inc\');
354 list($rech_select, $rech_where) = $prepare_recherche(' . $quoi . ', "' . $boucle->id_table . '", "' . $crit->cond . '","' . $boucle->sql_serveur . '",' . $_modificateur . ',"' . $boucle->primary . '");
355 }
356 ';
357
358
359 $t = $boucle->id_table . '.' . $boucle->primary;
360 if (!in_array($t, $boucles[$idb]->select)) {
361 $boucle->select[] = $t;
362 } # pour postgres, neuneu ici
363 // jointure uniquement sur le serveur principal
364 // (on ne peut joindre une table d'un serveur distant avec la table des resultats du serveur principal)
365 if (!$boucle->sql_serveur) {
366 $boucle->join['resultats'] = array("'" . $boucle->id_table . "'", "'id'", "'" . $boucle->primary . "'");
367 $boucle->from['resultats'] = 'spip_resultats';
368 }
369 $boucle->select[] = '$rech_select';
370 //$boucle->where[]= "\$rech_where?'resultats.id=".$boucle->id_table.".".$boucle->primary."':''";
371
372 // et la recherche trouve
373 $boucle->where[] = '$rech_where?$rech_where:\'\'';
374 }
375
376 /**
377 * Compile le critère `traduction`
378 *
379 * Sélectionne toutes les traductions de l'élément courant (la boucle englobante)
380 * en différentes langues (y compris l'élément englobant)
381 *
382 * Équivalent à `(id_trad>0 AND id_trad=id_trad(precedent)) OR id_xx=id_xx(precedent)`
383 *
384 * @param string $idb Identifiant de la boucle
385 * @param array $boucles AST du squelette
386 * @param Critere $crit Paramètres du critère dans cette boucle
387 * @return void
388 **/
389 function critere_traduction_dist($idb, &$boucles, $crit) {
390 $boucle = &$boucles[$idb];
391 $prim = $boucle->primary;
392 $table = $boucle->id_table;
393 $arg = kwote(calculer_argument_precedent($idb, 'id_trad', $boucles));
394 $dprim = kwote(calculer_argument_precedent($idb, $prim, $boucles));
395 $boucle->where[] =
396 array(
397 "'OR'",
398 array(
399 "'AND'",
400 array("'='", "'$table.id_trad'", 0),
401 array("'='", "'$table.$prim'", $dprim)
402 ),
403 array(
404 "'AND'",
405 array("'>'", "'$table.id_trad'", 0),
406 array("'='", "'$table.id_trad'", $arg)
407 )
408 );
409 }
410
411
412 /**
413 * Compile le critère {origine_traduction}
414 *
415 * Sélectionne les éléments qui servent de base à des versions traduites
416 * (par exemple les articles "originaux" sur une boucle articles)
417 *
418 * Équivalent à (id_trad>0 AND id_xx=id_trad) OR (id_trad=0)
419 *
420 * @param string $idb Identifiant de la boucle
421 * @param array $boucles AST du squelette
422 * @param Critere $crit Paramètres du critère dans cette boucle
423 * @return void
424 **/
425 function critere_origine_traduction_dist($idb, &$boucles, $crit) {
426 $boucle = &$boucles[$idb];
427 $prim = $boucle->primary;
428 $table = $boucle->id_table;
429
430 $c =
431 array(
432 "'OR'",
433 array("'='", "'$table." . "id_trad'", "'$table.$prim'"),
434 array("'='", "'$table.id_trad'", "'0'")
435 );
436 $boucle->where[] = ($crit->not ? array("'NOT'", $c) : $c);
437 }
438
439
440 /**
441 * Compile le critère {meme_parent}
442 *
443 * Sélectionne les éléments ayant le même parent que la boucle parente,
444 * c'est à dire les frères et sœurs.
445 *
446 * @param string $idb Identifiant de la boucle
447 * @param array $boucles AST du squelette
448 * @param Critere $crit Paramètres du critère dans cette boucle
449 * @return void
450 **/
451 function critere_meme_parent_dist($idb, &$boucles, $crit) {
452
453 $boucle = &$boucles[$idb];
454 $arg = kwote(calculer_argument_precedent($idb, 'id_parent', $boucles));
455 $id_parent = isset($GLOBALS['exceptions_des_tables'][$boucle->id_table]['id_parent']) ?
456 $GLOBALS['exceptions_des_tables'][$boucle->id_table]['id_parent'] :
457 'id_parent';
458 $mparent = $boucle->id_table . '.' . $id_parent;
459
460 if ($boucle->type_requete == 'rubriques' or isset($GLOBALS['exceptions_des_tables'][$boucle->id_table]['id_parent'])) {
461 $boucle->where[] = array("'='", "'$mparent'", $arg);
462
463 } // le cas FORUMS est gere dans le plugin forum, dans la fonction critere_FORUMS_meme_parent_dist()
464 else {
465 return (array('zbug_critere_inconnu', array('critere' => $crit->op . ' ' . $boucle->type_requete)));
466 }
467 }
468
469
470 /**
471 * Compile le critère `branche` qui sélectionne dans une boucle les
472 * éléments appartenant à une branche d'une rubrique.
473 *
474 * Cherche l'identifiant de la rubrique en premier paramètre du critère `{branche XX}`
475 * s'il est renseigné, sinon, sans paramètre (`{branche}` tout court) dans les
476 * boucles parentes. On calcule avec lui la liste des identifiants
477 * de rubrique de toute la branche.
478 *
479 * La boucle qui possède ce critère cherche une liaison possible avec
480 * la colonne `id_rubrique`, et tentera de trouver une jointure avec une autre
481 * table si c'est nécessaire pour l'obtenir.
482 *
483 * Ce critère peut être rendu optionnel avec `{branche ?}` en remarquant
484 * cependant que le test s'effectue sur la présence d'un champ 'id_rubrique'
485 * sinon d'une valeur 'id_rubrique' dans l'environnement (et non 'branche'
486 * donc).
487 *
488 * @link http://www.spip.net/@branche
489 *
490 * @param string $idb Identifiant de la boucle
491 * @param array $boucles AST du squelette
492 * @param Critere $crit Paramètres du critère dans cette boucle
493 * @return void
494 **/
495 function critere_branche_dist($idb, &$boucles, $crit) {
496
497 $not = $crit->not;
498 $boucle = &$boucles[$idb];
499 // prendre en priorite un identifiant en parametre {branche XX}
500 if (isset($crit->param[0])) {
501 $arg = calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent);
502 // sinon on le prend chez une boucle parente
503 } else {
504 $arg = kwote(calculer_argument_precedent($idb, 'id_rubrique', $boucles), $boucle->sql_serveur, 'int NOT NULL');
505 }
506
507 //Trouver une jointure
508 $champ = "id_rubrique";
509 $desc = $boucle->show;
510 //Seulement si necessaire
511 if (!array_key_exists($champ, $desc['field'])) {
512 $cle = trouver_jointure_champ($champ, $boucle);
513 $trouver_table = charger_fonction("trouver_table", "base");
514 $desc = $trouver_table($boucle->from[$cle]);
515 if (count(trouver_champs_decomposes($champ, $desc)) > 1) {
516 $decompose = decompose_champ_id_objet($champ);
517 $champ = array_shift($decompose);
518 $boucle->where[] = array("'='", _q($cle . "." . reset($decompose)), '"' . sql_quote(end($decompose)) . '"');
519 }
520 } else {
521 $cle = $boucle->id_table;
522 }
523
524 $c = "sql_in('$cle" . ".$champ', calcul_branche_in($arg)"
525 . ($not ? ", 'NOT'" : '') . ")";
526 $boucle->where[] = !$crit->cond ? $c :
527 ("($arg ? $c : " . ($not ? "'0=1'" : "'1=1'") . ')');
528 }
529
530 /**
531 * Compile le critère `logo` qui liste les objets qui ont un logo
532 *
533 * @uses lister_objets_avec_logos()
534 * Pour obtenir les éléments qui ont un logo
535 *
536 * @param string $idb Identifiant de la boucle
537 * @param array $boucles AST du squelette
538 * @param Critere $crit Paramètres du critère dans cette boucle
539 * @return void
540 **/
541 function critere_logo_dist($idb, &$boucles, $crit) {
542
543 $not = $crit->not;
544 $boucle = &$boucles[$idb];
545
546 $c = "sql_in('" .
547 $boucle->id_table . '.' . $boucle->primary
548 . "', lister_objets_avec_logos('" . $boucle->primary . "'), '')";
549
550 if ($crit->cond) {
551 $c = "($arg ? $c : 1)";
552 }
553
554 if ($not) {
555 $boucle->where[] = array("'NOT'", $c);
556 } else {
557 $boucle->where[] = $c;
558 }
559 }
560
561
562 /**
563 * Compile le critère `fusion` qui regroupe les éléments selon une colonne.
564 *
565 * C'est la commande SQL «GROUP BY»
566 *
567 * @critere
568 * @link http://www.spip.net/5166
569 * @example
570 * ```
571 * <BOUCLE_a(articles){fusion lang}>
572 * ```
573 *
574 * @param string $idb Identifiant de la boucle
575 * @param array $boucles AST du squelette
576 * @param Critere $crit Paramètres du critère dans cette boucle
577 * @return void
578 **/
579 function critere_fusion_dist($idb, &$boucles, $crit) {
580 if ($t = isset($crit->param[0])) {
581 $t = $crit->param[0];
582 if ($t[0]->type == 'texte') {
583 $t = $t[0]->texte;
584 if (preg_match("/^(.*)\.(.*)$/", $t, $r)) {
585 $t = table_objet_sql($r[1]);
586 $t = array_search($t, $boucles[$idb]->from);
587 if ($t) {
588 $t .= '.' . $r[2];
589 }
590 }
591 } else {
592 $t = '".'
593 . calculer_critere_arg_dynamique($idb, $boucles, $t)
594 . '."';
595 }
596 }
597 if ($t) {
598 $boucles[$idb]->group[] = $t;
599 if (!in_array($t, $boucles[$idb]->select)) {
600 $boucles[$idb]->select[] = $t;
601 }
602 } else {
603 return (array('zbug_critere_inconnu', array('critere' => $crit->op . ' ?')));
604 }
605 }
606
607 /**
608 * Compile le critère `{collecte}` qui permet de spécifier l'interclassement
609 * à utiliser pour les tris de la boucle.
610 *
611 * Cela permet avec le critère `{par}` de trier un texte selon
612 * l'interclassement indiqué. L'instruction s'appliquera sur les critères `{par}`
613 * qui succèdent ce critère, ainsi qu'au critère `{par}` précédent
614 * si aucun interclassement ne lui est déjà appliqué.
615 *
616 * Techniquement, c'est la commande SQL "COLLATE" qui utilisée.
617 * (elle peut être appliquée sur les order by, group by, where, like ...)
618 *
619 * @example
620 * - `{par titre}{collecte utf8_spanish_ci}` ou `{collecte utf8_spanish_ci}{par titre}`
621 * - `{par titre}{par surtitre}{collecte utf8_spanish_ci}` :
622 * Seul 'surtitre' (`par` précédent) utilisera l'interclassement
623 * - `{collecte utf8_spanish_ci}{par titre}{par surtitre}` :
624 * 'titre' et 'surtitre' utiliseront l'interclassement (tous les `par` suivants)
625 *
626 * @note
627 * Piège sur une éventuelle écriture peu probable :
628 * `{par a}{collecte c1}{par b}{collecte c2}` : le tri `{par b}`
629 * utiliserait l'interclassement c1 (et non c2 qui ne s'applique pas
630 * au `par` précédent s'il a déjà un interclassement demandé).
631 *
632 * @critere
633 * @link http://www.spip.net/4028
634 * @see critere_par_dist() Le critère `{par}`
635 *
636 * @param string $idb Identifiant de la boucle
637 * @param array $boucles AST du squelette
638 * @param Critere $crit Paramètres du critère dans cette boucle
639 */
640 function critere_collecte_dist($idb, &$boucles, $crit) {
641 if (isset($crit->param[0])) {
642 $_coll = calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent);
643 $boucle = $boucles[$idb];
644 $boucle->modificateur['collate'] = "($_coll ?' COLLATE '.$_coll:'')";
645 $n = count($boucle->order);
646 if ($n && (strpos($boucle->order[$n - 1], 'COLLATE') === false)) {
647 // l'instruction COLLATE doit être placée avant ASC ou DESC
648 // notamment lors de l'utilisation `{!par xxx}{collate yyy}`
649 if (
650 (false !== $i = strpos($boucle->order[$n - 1], 'ASC'))
651 OR (false !== $i = strpos($boucle->order[$n - 1], 'DESC'))
652 ) {
653 $boucle->order[$n - 1] = substr_replace($boucle->order[$n - 1], "' . " . $boucle->modificateur['collate'] . " . ' ", $i, 0);
654 } else {
655 $boucle->order[$n - 1] .= " . " . $boucle->modificateur['collate'];
656 }
657 }
658 } else {
659 return (array('zbug_critere_inconnu', array('critere' => $crit->op . " " . count($boucles[$idb]->order))));
660 }
661 }
662
663 // http://code.spip.net/@calculer_critere_arg_dynamique
664 function calculer_critere_arg_dynamique($idb, &$boucles, $crit, $suffix = '') {
665 $boucle = $boucles[$idb];
666 $alt = "('" . $boucle->id_table . '.\' . $x' . $suffix . ')';
667 $var = '$champs_' . $idb;
668 $desc = (strpos($boucle->in, "static $var =") !== false);
669 if (!$desc) {
670 $desc = $boucle->show['field'];
671 $desc = implode(',', array_map('_q', array_keys($desc)));
672 $boucles[$idb]->in .= "\n\tstatic $var = array(" . $desc . ");";
673 }
674 if ($desc) {
675 $alt = "(in_array(\$x, $var) ? $alt :(\$x$suffix))";
676 }
677 $arg = calculer_liste($crit, array(), $boucles, $boucle->id_parent);
678
679 return "((\$x = preg_replace(\"/\\W/\",'', $arg)) ? $alt : '')";
680 }
681
682 /**
683 * Compile le critère `{par}` qui permet d'ordonner les résultats d'une boucle
684 *
685 * Demande à trier la boucle selon certains champs (en SQL, la commande `ORDER BY`).
686 * Si plusieurs tris sont demandés (plusieurs fois le critère `{par x}{par y}` dans une boucle ou plusieurs champs
687 * séparés par des virgules dans le critère `{par x, y, z}`), ils seront appliqués dans l'ordre.
688 *
689 * Quelques particularités :
690 * - `{par hasard}` : trie par hasard
691 * - `{par num titre}` : trie par numéro de titre
692 * - `{par multi titre}` : trie par la langue extraite d'une balise polyglotte `<multi>` sur le champ titre
693 * - `{!par date}` : trie par date inverse en utilisant le champ date principal déclaré pour la table (si c'est un objet éditorial).
694 * - `{!par points}` : trie par pertinence de résultat de recherche (avec le critère `{recherche}`)
695 * - `{par FUNCTION_SQL(n)}` : trie en utilisant une fonction SQL (peut dépendre du moteur SQL utilisé).
696 * Exemple : `{par SUBSTRING_INDEX(titre, ".", -1)}` (tri ~ alphabétique en ignorant les numéros de titres
697 * (exemple erroné car faux dès qu'un titre possède un point.)).
698 * - `{par table.champ}` : trie en effectuant une jointure sur la table indiquée.
699 * - `{par #BALISE}` : trie sur la valeur retournée par la balise (doit être un champ de la table, ou 'hasard').
700 *
701 * @example
702 * - `{par titre}`
703 * - `{!par date}`
704 * - `{par num titre, multi titre, hasard}`
705 *
706 * @critere
707 * @link http://www.spip.net/5531
708 * @see critere_tri_dist() Le critère `{tri ...}`
709 * @see critere_inverse_dist() Le critère `{inverse}`
710 *
711 * @uses critere_parinverse()
712 *
713 * @param string $idb Identifiant de la boucle
714 * @param array $boucles AST du squelette
715 * @param Critere $crit Paramètres du critère dans cette boucle
716 */
717 function critere_par_dist($idb, &$boucles, $crit) {
718 return critere_parinverse($idb, $boucles, $crit);
719 }
720
721 /**
722 * Calculs pour le critère `{par}` ou `{inverse}` pour ordonner les résultats d'une boucle
723 *
724 * Les expressions intermédiaires `{par expr champ}` sont calculées dans des fonctions
725 * `calculer_critere_par_expression_{expr}()` notamment `{par num champ}` ou `{par multi champ}`.
726 *
727 * @see critere_par_dist() Le critère `{par}` pour des exemples
728 *
729 * @uses calculer_critere_arg_dynamique() pour le calcul de `{par #ENV{tri}}`
730 * @uses calculer_critere_par_hasard() pour le calcul de `{par hasard}`
731 * @uses calculer_critere_par_champ()
732 * @see calculer_critere_par_expression_num() pour le calcul de `{par num x}`
733 * @see calculer_critere_par_expression_multi() pour le calcul de `{par multi x}`
734 *
735 * @param string $idb Identifiant de la boucle
736 * @param array $boucles AST du squelette
737 * @param Critere $crit Paramètres du critère dans cette boucle
738 */
739 function critere_parinverse($idb, &$boucles, $crit) {
740 $boucle = &$boucles[$idb];
741
742 $sens = $collecte = '';
743 if ($crit->not) {
744 $sens = " . ' DESC'";
745 }
746 if (isset($boucle->modificateur['collate'])) {
747 $collecte = ' . ' . $boucle->modificateur['collate'];
748 }
749
750 // Pour chaque paramètre du critère
751 foreach ($crit->param as $tri) {
752 $order = $fct = '';
753 // tris specifiés dynamiquement {par #ENV{tri}}
754 if ($tri[0]->type != 'texte') {
755 // calculer le order dynamique qui verifie les champs
756 $order = calculer_critere_arg_dynamique($idb, $boucles, $tri, $sens);
757 // ajouter 'hasard' comme possibilité de tri dynamique
758 calculer_critere_par_hasard($idb, $boucles, $crit);
759 }
760 // tris textuels {par titre}
761 else {
762 $par = array_shift($tri);
763 $par = $par->texte;
764
765 // tris de la forme {par expression champ} tel que {par num titre} ou {par multi titre}
766 if (preg_match(",^(\w+)[\s]+(.*)$,", $par, $m)) {
767 $expression = trim($m[1]);
768 $champ = trim($m[2]);
769 if (function_exists($f = 'calculer_critere_par_expression_' . $expression)) {
770 $order = $f($idb, $boucles, $crit, $tri, $champ);
771 } else {
772 return array('zbug_critere_inconnu', array('critere' => $crit->op . " $par"));
773 }
774
775 // tris de la forme {par champ} ou {par FONCTION(champ)}
776 } elseif (preg_match(",^" . CHAMP_SQL_PLUS_FONC . '$,is', $par, $match)) {
777 // {par FONCTION(champ)}
778 if (count($match) > 2) {
779 $par = substr($match[2], 1, -1);
780 $fct = $match[1];
781 }
782 // quelques cas spécifiques {par hasard}, {par date}
783 if ($par == 'hasard') {
784 $order = calculer_critere_par_hasard($idb, $boucles, $crit);
785 } elseif ($par == 'date' and !empty($boucle->show['date'])) {
786 $order = "'" . $boucle->id_table . "." . $boucle->show['date'] . "'";
787 } else {
788 // cas général {par champ}, {par table.champ}, ...
789 $order = calculer_critere_par_champ($idb, $boucles, $crit, $par);
790 }
791 }
792
793 // on ne sait pas traiter…
794 else {
795 return array('zbug_critere_inconnu', array('critere' => $crit->op . " $par"));
796 }
797
798 // En cas d'erreur de squelette retournée par une fonction
799 if (is_array($order)) {
800 return $order;
801 }
802 }
803
804 if (preg_match('/^\'([^"]*)\'$/', $order, $m)) {
805 $t = $m[1];
806 if (strpos($t, '.') and !in_array($t, $boucle->select)) {
807 $boucle->select[] = $t;
808 }
809 } else {
810 $sens = '';
811 }
812
813 if ($fct) {
814 if (preg_match("/^\s*'(.*)'\s*$/", $order, $r)) {
815 $order = "'$fct(" . $r[1] . ")'";
816 } else {
817 $order = "'$fct(' . $order . ')'";
818 }
819 }
820 $t = $order . $collecte . $sens;
821 if (preg_match("/^(.*)'\s*\.\s*'([^']*')$/", $t, $r)) {
822 $t = $r[1] . $r[2];
823 }
824
825 $boucle->order[] = $t;
826 }
827 }
828
829 /**
830 * Calculs pour le critère `{par hasard}`
831 *
832 * Ajoute le générateur d'aléatoire au SELECT de la boucle.
833 *
834 * @param string $idb Identifiant de la boucle
835 * @param array $boucles AST du squelette
836 * @param Critere $crit Paramètres du critère dans cette boucle
837 * @return string Clause pour le Order by
838 */
839 function calculer_critere_par_hasard($idb, &$boucles, $crit) {
840 $boucle = &$boucles[$idb];
841 // Si ce n'est fait, ajouter un champ 'hasard' dans le select
842 $parha = "rand() AS hasard";
843 if (!in_array($parha, $boucle->select)) {
844 $boucle->select[] = $parha;
845 }
846 return "'hasard'";
847 }
848
849 /**
850 * Calculs pour le critère `{par num champ}` qui extrait le numéro préfixant un texte
851 *
852 * Tri par numéro de texte (tel que "10. titre"). Le numéro calculé est ajouté au SELECT
853 * de la boucle. L'écriture `{par num #ENV{tri}}` est aussi prise en compte.
854 *
855 * @note
856 * Les textes sans numéro valent 0 et sont donc placés avant les titres ayant des numéros.
857 * Utiliser `{par sinum champ, num champ}` pour avoir le comportement inverse.
858 *
859 * @see calculer_critere_par_expression_sinum() pour le critère `{par sinum champ}`
860 * @uses calculer_critere_par_champ()
861 *
862 * @param string $idb Identifiant de la boucle
863 * @param array $boucles AST du squelette
864 * @param Critere $crit Paramètres du critère dans cette boucle
865 * @param array $tri Paramètre en cours du critère
866 * @param string $champ Texte suivant l'expression ('titre' dans {par num titre})
867 * @return string Clause pour le Order by
868 */
869 function calculer_critere_par_expression_num($idb, &$boucles, $crit, $tri, $champ) {
870 $_champ = calculer_critere_par_champ($idb, $boucles, $crit, $champ, true);
871 if (is_array($_champ)) {
872 return array('zbug_critere_inconnu', array('critere' => $crit->op . " num $champ"));
873 }
874 $boucle = &$boucles[$idb];
875 $texte = '0+' . $_champ;
876 $suite = calculer_liste($tri, array(), $boucles, $boucle->id_parent);
877 if ($suite !== "''") {
878 $texte = "\" . ((\$x = $suite) ? ('$texte' . \$x) : '0')" . " . \"";
879 }
880 $as = 'num' . ($boucle->order ? count($boucle->order) : "");
881 $boucle->select[] = $texte . " AS $as";
882 $order = "'$as'";
883 return $order;
884 }
885
886 /**
887 * Calculs pour le critère `{par sinum champ}` qui ordonne les champs avec numéros en premier
888 *
889 * Ajoute au SELECT la valeur 'sinum' qui vaut 0 si le champ a un numéro, 1 s'il n'en a pas.
890 * Ainsi `{par sinum titre, num titre, titre}` mettra les éléments sans numéro en fin de liste,
891 * contrairement à `{par num titre, titre}` seulement.
892 *
893 * @see calculer_critere_par_expression_num() pour le critère `{par num champ}`
894 * @uses calculer_critere_par_champ()
895 *
896 * @param string $idb Identifiant de la boucle
897 * @param array $boucles AST du squelette
898 * @param Critere $crit Paramètres du critère dans cette boucle
899 * @param array $tri Paramètre en cours du critère
900 * @param string $champ Texte suivant l'expression ('titre' dans {par sinum titre})
901 * @return string Clause pour le Order by
902 */
903 function calculer_critere_par_expression_sinum($idb, &$boucles, $crit, $tri, $champ) {
904 $_champ = calculer_critere_par_champ($idb, $boucles, $crit, $champ, true);
905 if (is_array($_champ)) {
906 return array('zbug_critere_inconnu', array('critere' => $crit->op . " sinum $champ"));
907 }
908 $boucle = &$boucles[$idb];
909 $texte = '0+' . $_champ;
910 $suite = calculer_liste($tri, array(), $boucles, $boucle->id_parent);
911 if ($suite !== "''") {
912 $texte = "\" . ((\$x = $suite) ? ('$texte' . \$x) : '0')" . " . \"";
913 }
914 $as = 'sinum' . ($boucle->order ? count($boucle->order) : "");
915 $boucle->select[] = 'CASE (' . $texte . ') WHEN 0 THEN 1 ELSE 0 END AS ' . $as;
916 $order = "'$as'";
917 return $order;
918 }
919
920
921 /**
922 * Calculs pour le critère `{par multi champ}` qui extrait la langue en cours dans les textes
923 * ayant des balises `<multi>` (polyglottes)
924 *
925 * Ajoute le calcul du texte multi extrait dans le SELECT de la boucle.
926 * Il ne peut y avoir qu'un seul critère de tri `multi` par boucle.
927 *
928 * @uses calculer_critere_par_champ()
929 * @param string $idb Identifiant de la boucle
930 * @param array $boucles AST du squelette
931 * @param Critere $crit Paramètres du critère dans cette boucle
932 * @param array $tri Paramètre en cours du critère
933 * @param string $champ Texte suivant l'expression ('titre' dans {par multi titre})
934 * @return string Clause pour le Order by
935 */
936 function calculer_critere_par_expression_multi($idb, &$boucles, $crit, $tri, $champ) {
937 $_champ = calculer_critere_par_champ($idb, $boucles, $crit, $champ, true);
938 if (is_array($_champ)) {
939 return array('zbug_critere_inconnu', array('critere' => $crit->op . " multi $champ"));
940 }
941 $boucle = &$boucles[$idb];
942 $boucle->select[] = "\".sql_multi('" . $_champ . "', \$GLOBALS['spip_lang']).\"";
943 $order = "'multi'";
944 return $order;
945 }
946
947 /**
948 * Retourne le champ de tri demandé en ajoutant éventuellement les jointures nécessaires à la boucle.
949 *
950 * - si le champ existe dans la table, on l'utilise
951 * - si c'est une exception de jointure, on l'utilise (et crée la jointure au besoin)
952 * - si c'est un champ dont la jointure est déjà présente on la réutilise
953 * - si c'est un champ dont la jointure n'est pas présente, on la crée.
954 *
955 * @param string $idb Identifiant de la boucle
956 * @param array $boucles AST du squelette
957 * @param Critere $crit Paramètres du critère dans cette boucle
958 * @param string $par Nom du tri à analyser ('champ' ou 'table.champ')
959 * @param bool $raw Retourne le champ pour le compilateur ("'alias.champ'") ou brut ('alias.champ')
960 * @return array|string
961 */
962 function calculer_critere_par_champ($idb, &$boucles, $crit, $par, $raw = false) {
963 $boucle = &$boucles[$idb];
964
965 // le champ existe dans la table, pas de souci (le plus commun)
966 if (isset($desc['field'][$par])) {
967 $par = $boucle->id_table . "." . $par;
968 }
969 // le champ est peut être une jointure
970 else {
971 $table = $table_alias = false; // toutes les tables de jointure possibles
972 $champ = $par;
973
974 // le champ demandé est une exception de jointure {par titre_mot}
975 if (isset($GLOBALS['exceptions_des_jointures'][$par])) {
976 list($table, $champ) = $GLOBALS['exceptions_des_jointures'][$par];
977 } // la table de jointure est explicitement indiquée {par truc.muche}
978 elseif (preg_match("/^([^,]*)\.(.*)$/", $par, $r)) {
979 list(, $table, $champ) = $r;
980 $table_alias = $table; // c'est peut-être un alias de table {par L1.titre}
981 $table = table_objet_sql($table);
982 }
983
984 // Si on connait la table d'arrivée, on la demande donc explicitement
985 // Sinon on cherche le champ dans les tables possibles de jointures
986 // Si la table est déjà dans le from, on la réutilise.
987 if ($infos = chercher_champ_dans_tables($champ, $boucle->from, $boucle->sql_serveur, $table)) {
988 $par = $infos['alias'] . "." . $champ;
989 } elseif (
990 $boucle->jointures_explicites
991 and $alias = trouver_jointure_champ($champ, $boucle, explode(' ', $boucle->jointures_explicites), false, $table)
992 ) {
993 $par = $alias . "." . $champ;
994 } elseif ($alias = trouver_jointure_champ($champ, $boucle, $boucle->jointures, false, $table)) {
995 $par = $alias . "." . $champ;
996 // en spécifiant directement l'alias {par L2.titre} (situation hasardeuse tout de même)
997 } elseif (
998 $table_alias
999 and isset($boucle->from[$table_alias])
1000 and $infos = chercher_champ_dans_tables($champ, $boucle->from, $boucle->sql_serveur, $boucle->from[$table_alias])
1001 ) {
1002 $par = $infos['alias'] . "." . $champ;
1003 } elseif ($table) {
1004 // On avait table + champ, mais on ne les a pas trouvés
1005 return array('zbug_critere_inconnu', array('critere' => $crit->op . " $par"));
1006 } else {
1007 // Sinon tant pis, ca doit etre un champ synthetise (cf points)
1008 }
1009 }
1010
1011 return $raw ? $par : "'$par'";
1012 }
1013
1014 /**
1015 * Retourne un champ de tri en créant une jointure
1016 * si la table n'est pas présente dans le from de la boucle.
1017 *
1018 * @deprecated
1019 * @param string $table Table du champ désiré
1020 * @param string $champ Champ désiré
1021 * @param Boucle $boucle Boucle en cours de compilation
1022 * @return string Champ pour le compilateur si trouvé, tel que "'alias.champ'", sinon vide.
1023 */
1024 function critere_par_joint($table, $champ, &$boucle) {
1025 $t = array_search($table, $boucle->from);
1026 if (!$t) {
1027 $t = trouver_jointure_champ($champ, $boucle);
1028 }
1029 return !$t ? '' : ("'" . $t . '.' . $champ . "'");
1030 }
1031
1032 /**
1033 * Compile le critère `{inverse}` qui inverse l'ordre utilisé par le précédent critère `{par}`
1034 *
1035 * Accèpte un paramètre pour déterminer le sens : `{inverse #X}` utilisera un tri croissant (ASC)
1036 * si la valeur retournée par `#X` est considérée vrai (`true`),
1037 * le sens contraire (DESC) sinon.
1038 *
1039 * @example
1040 * - `{par date}{inverse}`, équivalent à `{!par date}`
1041 * - `{par date}{inverse #ENV{sens}}` utilise la valeur d'environnement sens pour déterminer le sens.
1042 *
1043 * @critere
1044 * @see critere_par_dist() Le critère `{par}`
1045 * @link http://www.spip.net/5530
1046 * @uses critere_parinverse()
1047 *
1048 * @param string $idb Identifiant de la boucle
1049 * @param array $boucles AST du squelette
1050 * @param Critere $crit Paramètres du critère dans cette boucle
1051 */
1052 function critere_inverse_dist($idb, &$boucles, $crit) {
1053
1054 $boucle = &$boucles[$idb];
1055 // Classement par ordre inverse
1056 if ($crit->not) {
1057 critere_parinverse($idb, $boucles, $crit);
1058 } else {
1059 $order = "' DESC'";
1060 // Classement par ordre inverse fonction eventuelle de #ENV{...}
1061 if (isset($crit->param[0])) {
1062 $critere = calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent);
1063 $order = "(($critere)?' DESC':'')";
1064 }
1065
1066 $n = count($boucle->order);
1067 if (!$n) {
1068 if (isset($boucle->default_order[0])) {
1069 $boucle->default_order[0] .= ' . " DESC"';
1070 } else {
1071 $boucle->default_order[] = ' DESC';
1072 }
1073 } else {
1074 $t = $boucle->order[$n - 1] . " . $order";
1075 if (preg_match("/^(.*)'\s*\.\s*'([^']*')$/", $t, $r)) {
1076 $t = $r[1] . $r[2];
1077 }
1078 $boucle->order[$n - 1] = $t;
1079 }
1080 }
1081 }
1082
1083 // http://code.spip.net/@critere_agenda_dist
1084 function critere_agenda_dist($idb, &$boucles, $crit) {
1085 $params = $crit->param;
1086
1087 if (count($params) < 1) {
1088 return array('zbug_critere_inconnu', array('critere' => $crit->op . " ?"));
1089 }
1090
1091 $boucle = &$boucles[$idb];
1092 $parent = $boucle->id_parent;
1093 $fields = $boucle->show['field'];
1094
1095 $date = array_shift($params);
1096 $type = array_shift($params);
1097
1098 // la valeur $type doit etre connue a la compilation
1099 // donc etre forcement reduite a un litteral unique dans le source
1100 $type = is_object($type[0]) ? $type[0]->texte : null;
1101
1102 // La valeur date doit designer un champ de la table SQL.
1103 // Si c'est un litteral unique dans le source, verifier a la compil,
1104 // sinon synthetiser le test de verif pour execution ulterieure
1105 // On prendra arbitrairement le premier champ si test negatif.
1106 if ((count($date) == 1) and ($date[0]->type == 'texte')) {
1107 $date = $date[0]->texte;
1108 if (!isset($fields[$date])) {
1109 return array('zbug_critere_inconnu', array('critere' => $crit->op . " " . $date));
1110 }
1111 } else {
1112 $a = calculer_liste($date, array(), $boucles, $parent);
1113 $noms = array_keys($fields);
1114 $defaut = $noms[0];
1115 $noms = join(" ", $noms);
1116 # bien laisser 2 espaces avant $nom pour que strpos<>0
1117 $cond = "(\$a=strval($a))AND\nstrpos(\" $noms \",\" \$a \")";
1118 $date = "'.(($cond)\n?\$a:\"$defaut\").'";
1119 }
1120 $annee = $params ? array_shift($params) : "";
1121 $annee = "\n" . 'sprintf("%04d", ($x = ' .
1122 calculer_liste($annee, array(), $boucles, $parent) .
1123 ') ? $x : date("Y"))';
1124
1125 $mois = $params ? array_shift($params) : "";
1126 $mois = "\n" . 'sprintf("%02d", ($x = ' .
1127 calculer_liste($mois, array(), $boucles, $parent) .
1128 ') ? $x : date("m"))';
1129
1130 $jour = $params ? array_shift($params) : "";
1131 $jour = "\n" . 'sprintf("%02d", ($x = ' .
1132 calculer_liste($jour, array(), $boucles, $parent) .
1133 ') ? $x : date("d"))';
1134
1135 $annee2 = $params ? array_shift($params) : "";
1136 $annee2 = "\n" . 'sprintf("%04d", ($x = ' .
1137 calculer_liste($annee2, array(), $boucles, $parent) .
1138 ') ? $x : date("Y"))';
1139
1140 $mois2 = $params ? array_shift($params) : "";
1141 $mois2 = "\n" . 'sprintf("%02d", ($x = ' .
1142 calculer_liste($mois2, array(), $boucles, $parent) .
1143 ') ? $x : date("m"))';
1144
1145 $jour2 = $params ? array_shift($params) : "";
1146 $jour2 = "\n" . 'sprintf("%02d", ($x = ' .
1147 calculer_liste($jour2, array(), $boucles, $parent) .
1148 ') ? $x : date("d"))';
1149
1150 $date = $boucle->id_table . ".$date";
1151
1152 $quote_end = ",'" . $boucle->sql_serveur . "','text'";
1153 if ($type == 'jour') {
1154 $boucle->where[] = array(
1155 "'='",
1156 "'DATE_FORMAT($date, \'%Y%m%d\')'",
1157 ("sql_quote($annee . $mois . $jour$quote_end)")
1158 );
1159 } elseif ($type == 'mois') {
1160 $boucle->where[] = array(
1161 "'='",
1162 "'DATE_FORMAT($date, \'%Y%m\')'",
1163 ("sql_quote($annee . $mois$quote_end)")
1164 );
1165 } elseif ($type == 'semaine') {
1166 $boucle->where[] = array(
1167 "'AND'",
1168 array(
1169 "'>='",
1170 "'DATE_FORMAT($date, \'%Y%m%d\')'",
1171 ("date_debut_semaine($annee, $mois, $jour)")
1172 ),
1173 array(
1174 "'<='",
1175 "'DATE_FORMAT($date, \'%Y%m%d\')'",
1176 ("date_fin_semaine($annee, $mois, $jour)")
1177 )
1178 );
1179 } elseif (count($crit->param) > 2) {
1180 $boucle->where[] = array(
1181 "'AND'",
1182 array(
1183 "'>='",
1184 "'DATE_FORMAT($date, \'%Y%m%d\')'",
1185 ("sql_quote($annee . $mois . $jour$quote_end)")
1186 ),
1187 array("'<='", "'DATE_FORMAT($date, \'%Y%m%d\')'", ("sql_quote($annee2 . $mois2 . $jour2$quote_end)"))
1188 );
1189 }
1190 // sinon on prend tout
1191 }
1192
1193
1194 /**
1195 * Compile les critères {i,j} et {i/j}
1196 *
1197 * Le critère {i,j} limite l'affiche de la boucle en commançant l'itération
1198 * au i-ème élément, et pour j nombre d'éléments.
1199 * Le critère {n-i,j} limite en commençant au n moins i-ème élément de boucle
1200 * Le critère {i,n-j} limite en terminant au n moins j-ème élément de boucle.
1201 *
1202 * Le critère {i/j} affiche une part d'éléments de la boucle.
1203 * Commence à i*n/j élément et boucle n/j éléments. {2/4} affiche le second
1204 * quart des éléments d'une boucle.
1205 *
1206 * Traduit si possible (absence de n dans {i,j}) la demande en une
1207 * expression LIMIT du gestionnaire SQL
1208 *
1209 * @param string $idb Identifiant de la boucle
1210 * @param array $boucles AST du squelette
1211 * @param Critere $crit Paramètres du critère dans cette boucle
1212 * @return void
1213 **/
1214 function calculer_critere_parties($idb, &$boucles, $crit) {
1215 $boucle = &$boucles[$idb];
1216 $a1 = $crit->param[0];
1217 $a2 = $crit->param[1];
1218 $op = $crit->op;
1219
1220 list($a11, $a12) = calculer_critere_parties_aux($idb, $boucles, $a1);
1221 list($a21, $a22) = calculer_critere_parties_aux($idb, $boucles, $a2);
1222
1223 if (($op == ',') && (is_numeric($a11) && (is_numeric($a21)))) {
1224 $boucle->limit = $a11 . ',' . $a21;
1225 } else {
1226 // 3 dans {1/3}, {2,3} ou {1,n-3}
1227 $boucle->total_parties = ($a21 != 'n') ? $a21 : $a22;
1228 // 2 dans {2/3}, {2,5}, {n-2,1}
1229 $partie = ($a11 != 'n') ? $a11 : $a12;
1230 $mode = (($op == '/') ? '/' :
1231 (($a11 == 'n') ? '-' : '+') . (($a21 == 'n') ? '-' : '+'));
1232 // cas simple {0,#ENV{truc}} compilons le en LIMIT :
1233 if ($a11 !== 'n' and $a21 !== 'n' and $mode == "++" and $op == ',') {
1234 $boucle->limit =
1235 (is_numeric($a11) ? "'$a11'" : $a11)
1236 . ".','."
1237 . (is_numeric($a21) ? "'$a21'" : $a21);
1238 } else {
1239 calculer_parties($boucles, $idb, $partie, $mode);
1240 }
1241 }
1242 }
1243
1244 /**
1245 * Compile certains critères {i,j} et {i/j}
1246 *
1247 * Calcule une expression déterminant $debut_boucle et $fin_boucle (le
1248 * début et la fin des éléments de la boucle qui doivent être affichés)
1249 * et les déclare dans la propriété «mode_partie» de la boucle, qui se
1250 * charge également de déplacer le pointeur de boucle sur le premier
1251 * élément à afficher.
1252 *
1253 * Place dans la propriété partie un test vérifiant que l'élément de
1254 * boucle en cours de lecture appartient bien à la plage autorisée.
1255 * Trop tôt, passe à l'élément suivant, trop tard, sort de l'itération de boucle.
1256 *
1257 * @param array $boucles AST du squelette
1258 * @param string $id_boucle Identifiant de la boucle
1259 * @param string $debut Valeur ou code pour trouver le début (i dans {i,j})
1260 * @param string $mode
1261 * Mode (++, p+, +- ...) : 2 signes début & fin
1262 * - le signe - indique
1263 * -- qu'il faut soustraire debut du total {n-3,x}. 3 étant $debut
1264 * -- qu'il faut raccourcir la fin {x,n-3} de 3 elements. 3 étant $total_parties
1265 * - le signe p indique une pagination
1266 * @return void
1267 **/
1268 function calculer_parties(&$boucles, $id_boucle, $debut, $mode) {
1269 $total_parties = $boucles[$id_boucle]->total_parties;
1270
1271 preg_match(",([+-/p])([+-/])?,", $mode, $regs);
1272 list(, $op1, $op2) = array_pad($regs, 3, null);
1273 $nombre_boucle = "\$Numrows['$id_boucle']['total']";
1274 // {1/3}
1275 if ($op1 == '/') {
1276 $pmoins1 = is_numeric($debut) ? ($debut - 1) : "($debut-1)";
1277 $totpos = is_numeric($total_parties) ? ($total_parties) :
1278 "($total_parties ? $total_parties : 1)";
1279 $fin = "ceil(($nombre_boucle * $debut )/$totpos) - 1";
1280 $debut = !$pmoins1 ? 0 : "ceil(($nombre_boucle * $pmoins1)/$totpos);";
1281 } else {
1282 // cas {n-1,x}
1283 if ($op1 == '-') {
1284 $debut = "$nombre_boucle - $debut;";
1285 }
1286
1287 // cas {x,n-1}
1288 if ($op2 == '-') {
1289 $fin = '$debut_boucle + ' . $nombre_boucle . ' - '
1290 . (is_numeric($total_parties) ? ($total_parties + 1) :
1291 ($total_parties . ' - 1'));
1292 } else {
1293 // {x,1} ou {pagination}
1294 $fin = '$debut_boucle'
1295 . (is_numeric($total_parties) ?
1296 (($total_parties == 1) ? "" : (' + ' . ($total_parties - 1))) :
1297 ('+' . $total_parties . ' - 1'));
1298 }
1299
1300 // {pagination}, gerer le debut_xx=-1 pour tout voir
1301 if ($op1 == 'p') {
1302 $debut .= ";\n \$debut_boucle = ((\$tout=(\$debut_boucle == -1))?0:(\$debut_boucle))";
1303 $debut .= ";\n \$debut_boucle = max(0,min(\$debut_boucle,floor(($nombre_boucle-1)/($total_parties))*($total_parties)))";
1304 $fin = "(\$tout ? $nombre_boucle : $fin)";
1305 }
1306 }
1307
1308 // Notes :
1309 // $debut_boucle et $fin_boucle sont les indices SQL du premier
1310 // et du dernier demandes dans la boucle : 0 pour le premier,
1311 // n-1 pour le dernier ; donc total_boucle = 1 + debut - fin
1312 // Utiliser min pour rabattre $fin_boucle sur total_boucle.
1313
1314 $boucles[$id_boucle]->mode_partie = "\n\t"
1315 . '$debut_boucle = ' . $debut . ";\n "
1316 . "\$debut_boucle = intval(\$debut_boucle);\n "
1317 . '$fin_boucle = min(' . $fin . ", \$Numrows['$id_boucle']['total'] - 1);\n "
1318 . '$Numrows[\'' . $id_boucle . "']['grand_total'] = \$Numrows['$id_boucle']['total'];\n "
1319 . '$Numrows[\'' . $id_boucle . '\']["total"] = max(0,$fin_boucle - $debut_boucle + 1);'
1320 . "\n\tif (\$debut_boucle>0"
1321 . " AND \$debut_boucle < \$Numrows['$id_boucle']['grand_total']"
1322 . " AND \$iter->seek(\$debut_boucle,'continue'))"
1323 . "\n\t\t\$Numrows['$id_boucle']['compteur_boucle'] = \$debut_boucle;\n\t";
1324
1325 $boucles[$id_boucle]->partie = "
1326 if (\$Numrows['$id_boucle']['compteur_boucle'] <= \$debut_boucle) continue;
1327 if (\$Numrows['$id_boucle']['compteur_boucle']-1 > \$fin_boucle) break;";
1328 }
1329
1330 /**
1331 * Analyse un des éléments des critères {a,b} ou {a/b}
1332 *
1333 * Pour l'élément demandé (a ou b) retrouve la valeur de l'élément,
1334 * et de combien il est soustrait si c'est le cas comme dans {a-3,b}
1335 *
1336 * @param string $idb Identifiant de la boucle
1337 * @param array $boucles AST du squelette
1338 * @param array $param Paramètre à analyser (soit a, soit b dans {a,b} ou {a/b})
1339 * @return array Valeur de l'élément (peut être une expression PHP), Nombre soustrait
1340 **/
1341 function calculer_critere_parties_aux($idb, &$boucles, $param) {
1342 if ($param[0]->type != 'texte') {
1343 $a1 = calculer_liste(array($param[0]), array('id_mere' => $idb), $boucles, $boucles[$idb]->id_parent);
1344 if (isset($param[1]->texte)) {
1345 preg_match(',^ *(-([0-9]+))? *$,', $param[1]->texte, $m);
1346
1347 return array("intval($a1)", ((isset($m[2]) and $m[2]) ? $m[2] : 0));
1348 } else {
1349 return array("intval($a1)", 0);
1350 }
1351 } else {
1352 preg_match(',^ *(([0-9]+)|n) *(- *([0-9]+)? *)?$,', $param[0]->texte, $m);
1353 $a1 = $m[1];
1354 if (empty($m[3])) {
1355 return array($a1, 0);
1356 } elseif (!empty($m[4])) {
1357 return array($a1, $m[4]);
1358 } else {
1359 return array($a1, calculer_liste(array($param[1]), array(), $boucles, $boucles[$idb]->id_parent));
1360 }
1361 }
1362 }
1363
1364
1365 /**
1366 * Compile les critères d'une boucle
1367 *
1368 * Cette fonction d'aiguillage cherche des fonctions spécifiques déclarées
1369 * pour chaque critère demandé, dans l'ordre ci-dessous :
1370 *
1371 * - critere_{serveur}_{table}_{critere}, sinon avec _dist
1372 * - critere_{serveur}_{critere}, sinon avec _dist
1373 * - critere_{table}_{critere}, sinon avec _dist
1374 * - critere_{critere}, sinon avec _dist
1375 * - calculer_critere_defaut, sinon avec _dist
1376 *
1377 * Émet une erreur de squelette si un critère retourne une erreur.
1378 *
1379 * @param string $idb
1380 * Identifiant de la boucle
1381 * @param array $boucles
1382 * AST du squelette
1383 * @return string|array
1384 * string : Chaine vide sans erreur
1385 * array : Erreur sur un des critères
1386 **/
1387 function calculer_criteres($idb, &$boucles) {
1388 $msg = '';
1389 $boucle = $boucles[$idb];
1390 $table = strtoupper($boucle->type_requete);
1391 $serveur = strtolower($boucle->sql_serveur);
1392
1393 $defaut = charger_fonction('DEFAUT', 'calculer_critere');
1394 // s'il y avait une erreur de syntaxe, propager cette info
1395 if (!is_array($boucle->criteres)) {
1396 return array();
1397 }
1398
1399 foreach ($boucle->criteres as $crit) {
1400 $critere = $crit->op;
1401 // critere personnalise ?
1402 if (
1403 (!$serveur or
1404 ((!function_exists($f = "critere_" . $serveur . "_" . $table . "_" . $critere))
1405 and (!function_exists($f = $f . "_dist"))
1406 and (!function_exists($f = "critere_" . $serveur . "_" . $critere))
1407 and (!function_exists($f = $f . "_dist"))
1408 )
1409 )
1410 and (!function_exists($f = "critere_" . $table . "_" . $critere))
1411 and (!function_exists($f = $f . "_dist"))
1412 and (!function_exists($f = "critere_" . $critere))
1413 and (!function_exists($f = $f . "_dist"))
1414 ) {
1415 // fonction critere standard
1416 $f = $defaut;
1417 }
1418 // compile le critere
1419 $res = $f($idb, $boucles, $crit);
1420
1421 // Gestion centralisee des erreurs pour pouvoir propager
1422 if (is_array($res)) {
1423 $msg = $res;
1424 erreur_squelette($msg, $boucle);
1425 }
1426 }
1427
1428 return $msg;
1429 }
1430
1431 /**
1432 * Désemberlificote les guillements et échappe (ou fera échapper) le contenu...
1433 *
1434 * Madeleine de Proust, revision MIT-1958 sqq, revision CERN-1989
1435 * hum, c'est kwoi cette fonxion ? on va dire qu'elle desemberlificote les guillemets...
1436 *
1437 * http://code.spip.net/@kwote
1438 *
1439 * @param string $lisp Code compilé
1440 * @param string $serveur Connecteur de bdd utilisé
1441 * @param string $type Type d'échappement (char, int...)
1442 * @return string Code compilé rééchappé
1443 */
1444 function kwote($lisp, $serveur = '', $type = '') {
1445 if (preg_match(_CODE_QUOTE, $lisp, $r)) {
1446 return $r[1] . "\"" . sql_quote(str_replace(array("\\'", "\\\\"), array("'", "\\"), $r[2]), $serveur, $type) . "\"";
1447 } else {
1448 return "sql_quote($lisp, '$serveur', '" . str_replace("'", "\\'", $type) . "')";
1449 }
1450 }
1451
1452
1453 /**
1454 * Compile un critère possédant l'opérateur IN : {xx IN yy}
1455 *
1456 * Permet de restreindre un champ sur une liste de valeurs tel que
1457 * {id_article IN 3,4} {id_article IN #LISTE{3,4}}
1458 *
1459 * Si on a une liste de valeurs dans #ENV{x}, utiliser la double etoile
1460 * pour faire par exemple {id_article IN #ENV**{liste_articles}}
1461 *
1462 * @param string $idb Identifiant de la boucle
1463 * @param array $boucles AST du squelette
1464 * @param Critere $crit Paramètres du critère dans cette boucle
1465 * @return void
1466 **/
1467 function critere_IN_dist($idb, &$boucles, $crit) {
1468 $r = calculer_critere_infixe($idb, $boucles, $crit);
1469 if (!$r) {
1470 return (array('zbug_critere_inconnu', array('critere' => $crit->op . " ?")));
1471 }
1472 list($arg, $op, $val, $col, $where_complement) = $r;
1473
1474 $in = critere_IN_cas($idb, $boucles, $crit->not ? 'NOT' : ($crit->exclus ? 'exclus' : ''), $arg, $op, $val, $col);
1475
1476 // inserer la condition; exemple: {id_mot ?IN (66, 62, 64)}
1477 $where = $in;
1478 if ($crit->cond) {
1479 $pred = calculer_argument_precedent($idb, $col, $boucles);
1480 $where = array("'?'", $pred, $where, "''");
1481 if ($where_complement) // condition annexe du type "AND (objet='article')"
1482 {
1483 $where_complement = array("'?'", $pred, $where_complement, "''");
1484 }
1485 }
1486 if ($crit->exclus) {
1487 if (!preg_match(",^L[0-9]+[.],", $arg)) {
1488 $where = array("'NOT'", $where);
1489 } else
1490 // un not sur un critere de jointure se traduit comme un NOT IN avec une sous requete
1491 // c'est une sous requete identique a la requete principale sous la forme (SELF,$select,$where) avec $select et $where qui surchargent
1492 {
1493 $where = array(
1494 "'NOT'",
1495 array(
1496 "'IN'",
1497 "'" . $boucles[$idb]->id_table . "." . $boucles[$idb]->primary . "'",
1498 array("'SELF'", "'" . $boucles[$idb]->id_table . "." . $boucles[$idb]->primary . "'", $where)
1499 )
1500 );
1501 }
1502 }
1503
1504 $boucles[$idb]->where[] = $where;
1505 if ($where_complement) // condition annexe du type "AND (objet='article')"
1506 {
1507 $boucles[$idb]->where[] = $where_complement;
1508 }
1509 }
1510
1511 // http://code.spip.net/@critere_IN_cas
1512 function critere_IN_cas($idb, &$boucles, $crit2, $arg, $op, $val, $col) {
1513 static $num = array();
1514 $descr = $boucles[$idb]->descr;
1515 $cpt = &$num[$descr['nom']][$descr['gram']][$idb];
1516
1517 $var = '$in' . $cpt++;
1518 $x = "\n\t$var = array();";
1519 foreach ($val as $k => $v) {
1520 if (preg_match(",^(\n//.*\n)?'(.*)'$,", $v, $r)) {
1521 // optimiser le traitement des constantes
1522 if (is_numeric($r[2])) {
1523 $x .= "\n\t$var" . "[]= $r[2];";
1524 } else {
1525 $x .= "\n\t$var" . "[]= " . sql_quote($r[2]) . ";";
1526 }
1527 } else {
1528 // Pour permettre de passer des tableaux de valeurs
1529 // on repere l'utilisation brute de #ENV**{X},
1530 // c'est-a-dire sa traduction en ($PILE[0][X]).
1531 // et on deballe mais en rajoutant l'anti XSS
1532 $x .= "\n\tif (!(is_array(\$a = ($v))))\n\t\t$var" . "[]= \$a;\n\telse $var = array_merge($var, \$a);";
1533 }
1534 }
1535
1536 $boucles[$idb]->in .= $x;
1537
1538 // inserer le tri par defaut selon les ordres du IN ...
1539 // avec une ecriture de type FIELD qui degrade les performances (du meme ordre qu'un regexp)
1540 // et que l'on limite donc strictement aux cas necessaires :
1541 // si ce n'est pas un !IN, et si il n'y a pas d'autre order dans la boucle
1542 if (!$crit2) {
1543 $boucles[$idb]->default_order[] = "((!sql_quote($var) OR sql_quote($var)===\"''\") ? 0 : ('FIELD($arg,' . sql_quote($var) . ')'))";
1544 }
1545
1546 return "sql_in('$arg',sql_quote($var)" . ($crit2 == 'NOT' ? ",'NOT'" : "") . ")";
1547 }
1548
1549 /**
1550 * Compile le critère {where}
1551 *
1552 * Ajoute une contrainte sql WHERE, tout simplement pour faire le pont
1553 * entre php et squelettes, en utilisant la syntaxe attendue par
1554 * la propriété $where d'une Boucle.
1555 *
1556 * @param string $idb Identifiant de la boucle
1557 * @param array $boucles AST du squelette
1558 * @param Critere $crit Paramètres du critère dans cette boucle
1559 * @return void
1560 */
1561 function critere_where_dist($idb, &$boucles, $crit) {
1562 $boucle = &$boucles[$idb];
1563 if (isset($crit->param[0])) {
1564 $_where = calculer_liste($crit->param[0], array(), $boucles, $boucle->id_parent);
1565 } else {
1566 $_where = '@$Pile[0]["where"]';
1567 }
1568
1569 if ($crit->cond) {
1570 $_where = "(($_where) ? ($_where) : '')";
1571 }
1572
1573 if ($crit->not) {
1574 $_where = "array('NOT',$_where)";
1575 }
1576
1577 $boucle->where[] = $_where;
1578 }
1579
1580
1581 /**
1582 * Compile le critère `{tri}` permettant le tri dynamique d'un champ
1583 *
1584 * Le critère `{tri}` gère un champ de tri qui peut être modifié dynamiquement par la balise `#TRI`.
1585 * Il s'utilise donc conjointement avec la balise `#TRI` dans la même boucle
1586 * pour génerér les liens qui permettent de changer le critère de tri et le sens du tri
1587 *
1588 * @syntaxe `{tri [champ_par_defaut][,sens_par_defaut][,nom_variable]}`
1589 *
1590 * - champ_par_defaut : un champ de la table sql
1591 * - sens_par_defaut : -1 ou inverse pour décroissant, 1 ou direct pour croissant
1592 * peut être un tableau pour préciser des sens par défaut associés à chaque champ
1593 * exemple : `array('titre' => 1, 'date' => -1)` pour trier par défaut
1594 * les titre croissants et les dates décroissantes
1595 * dans ce cas, quand un champ est utilisé pour le tri et n'est pas présent dans le tableau
1596 * c'est la première valeur qui est utilisée
1597 * - nom_variable : nom de la variable utilisée (par defaut `tri_{nomboucle}`)
1598 *
1599 * {tri titre}
1600 * {tri titre,inverse}
1601 * {tri titre,-1}
1602 * {tri titre,-1,truc}
1603 *
1604 * Exemple d'utilisation :
1605 *
1606 * <B_articles>
1607 * <p>#TRI{titre,'Trier par titre'} | #TRI{date,'Trier par date'}</p>
1608 * <ul>
1609 * <BOUCLE_articles(ARTICLES){tri titre}>
1610 * <li>#TITRE - [(#DATE|affdate_jourcourt)]</li>
1611 * </BOUCLE_articles>
1612 * </ul>
1613 * </B_articles>
1614 *
1615 * @note
1616 * Contraitement à `{par ...}`, `{tri}` ne peut prendre qu'un seul champ,
1617 * mais il peut être complété avec `{par ...}` pour indiquer des criteres secondaires
1618 *
1619 * Exemble :
1620 * `{tri num titre}{par titre}` permet de faire un tri sur le rang (modifiable dynamiquement)
1621 * avec un second critère sur le titre en cas d'égalité des rangs
1622 *
1623 * @link http://www.spip.net/5429
1624 * @see critere_par_dist() Le critère `{par ...}`
1625 * @see balise_TRI_dist() La balise `#TRI`
1626 *
1627 * @param string $idb Identifiant de la boucle
1628 * @param array $boucles AST du squelette
1629 * @param Critere $crit Paramètres du critère dans cette boucle
1630 * @return void
1631 */
1632 function critere_tri_dist($idb, &$boucles, $crit) {
1633 $boucle = &$boucles[$idb];
1634
1635 // definition du champ par defaut
1636 $_champ_defaut = !isset($crit->param[0][0]) ? "''"
1637 : calculer_liste(array($crit->param[0][0]), array(), $boucles, $boucle->id_parent);
1638 $_sens_defaut = !isset($crit->param[1][0]) ? "1"
1639 : calculer_liste(array($crit->param[1][0]), array(), $boucles, $boucle->id_parent);
1640 $_variable = !isset($crit->param[2][0]) ? "'$idb'"
1641 : calculer_liste(array($crit->param[2][0]), array(), $boucles, $boucle->id_parent);
1642
1643 $_tri = "((\$t=(isset(\$Pile[0]['tri'.$_variable]))?\$Pile[0]['tri'.$_variable]:((strncmp($_variable,'session',7)==0 AND session_get('tri'.$_variable))?session_get('tri'.$_variable):$_champ_defaut))?tri_protege_champ(\$t):'')";
1644
1645 $_sens_defaut = "(is_array(\$s=$_sens_defaut)?(isset(\$s[\$st=$_tri])?\$s[\$st]:reset(\$s)):\$s)";
1646 $_sens = "((intval(\$t=(isset(\$Pile[0]['sens'.$_variable]))?\$Pile[0]['sens'.$_variable]:((strncmp($_variable,'session',7)==0 AND session_get('sens'.$_variable))?session_get('sens'.$_variable):$_sens_defaut))==-1 OR \$t=='inverse')?-1:1)";
1647
1648 $boucle->modificateur['tri_champ'] = $_tri;
1649 $boucle->modificateur['tri_sens'] = $_sens;
1650 $boucle->modificateur['tri_nom'] = $_variable;
1651 // faut il inserer un test sur l'existence de $tri parmi les champs de la table ?
1652 // evite des erreurs sql, mais peut empecher des tri sur jointure ...
1653 $boucle->hash .= "
1654 \$senstri = '';
1655 \$tri = $_tri;
1656 if (\$tri){
1657 \$senstri = $_sens;
1658 \$senstri = (\$senstri<0)?' DESC':'';
1659 };
1660 ";
1661 $boucle->select[] = "\".tri_champ_select(\$tri).\"";
1662 $boucle->order[] = "tri_champ_order(\$tri,\$command['from']).\$senstri";
1663 }
1664
1665 # Criteres de comparaison
1666
1667 /**
1668 * Compile un critère non déclaré explicitement
1669 *
1670 * Compile les critères non déclarés, ainsi que les parties de boucles
1671 * avec les critères {0,1} ou {1/2}
1672 *
1673 * @param string $idb Identifiant de la boucle
1674 * @param array $boucles AST du squelette
1675 * @param Critere $crit Paramètres du critère dans cette boucle
1676 * @return void
1677 **/
1678 function calculer_critere_DEFAUT_dist($idb, &$boucles, $crit) {
1679 // double cas particulier {0,1} et {1/2} repere a l'analyse lexicale
1680 if (($crit->op == ",") or ($crit->op == '/')) {
1681 return calculer_critere_parties($idb, $boucles, $crit);
1682 }
1683
1684 $r = calculer_critere_infixe($idb, $boucles, $crit);
1685 if (!$r) {
1686 # // on produit une erreur seulement si le critere n'a pas de '?'
1687 # if (!$crit->cond) {
1688 return (array('zbug_critere_inconnu', array('critere' => $crit->op)));
1689 # }
1690 } else {
1691 calculer_critere_DEFAUT_args($idb, $boucles, $crit, $r);
1692 }
1693 }
1694
1695
1696 /**
1697 * Compile un critère non déclaré explicitement, dont on reçoit une analyse
1698 *
1699 * Ajoute en fonction des arguments trouvés par calculer_critere_infixe()
1700 * les conditions WHERE à appliquer sur la boucle.
1701 *
1702 * @see calculer_critere_infixe()
1703 *
1704 * @param string $idb Identifiant de la boucle
1705 * @param array $boucles AST du squelette
1706 * @param Critere $crit Paramètres du critère dans cette boucle
1707 * @param array $args Description du critère
1708 * Cf. retour de calculer_critere_infixe()
1709 * @return void
1710 **/
1711 function calculer_critere_DEFAUT_args($idb, &$boucles, $crit, $args) {
1712 list($arg, $op, $val, $col, $where_complement) = $args;
1713
1714 $where = array("'$op'", "'$arg'", $val[0]);
1715
1716 // inserer la negation (cf !...)
1717
1718 if ($crit->not) {
1719 $where = array("'NOT'", $where);
1720 }
1721 if ($crit->exclus) {
1722 if (!preg_match(",^L[0-9]+[.],", $arg)) {
1723 $where = array("'NOT'", $where);
1724 } else
1725 // un not sur un critere de jointure se traduit comme un NOT IN avec une sous requete
1726 // c'est une sous requete identique a la requete principale sous la forme (SELF,$select,$where) avec $select et $where qui surchargent
1727 {
1728 $where = array(
1729 "'NOT'",
1730 array(
1731 "'IN'",
1732 "'" . $boucles[$idb]->id_table . "." . $boucles[$idb]->primary . "'",
1733 array("'SELF'", "'" . $boucles[$idb]->id_table . "." . $boucles[$idb]->primary . "'", $where)
1734 )
1735 );
1736 }
1737 }
1738
1739 // inserer la condition (cf {lang?})
1740 // traiter a part la date, elle est mise d'office par SPIP,
1741 if ($crit->cond) {
1742 $pred = calculer_argument_precedent($idb, $col, $boucles);
1743 if ($col == "date" or $col == "date_redac") {
1744 if ($pred == "\$Pile[0]['" . $col . "']") {
1745 $pred = "(\$Pile[0]['{$col}_default']?'':$pred)";
1746 }
1747 }
1748
1749 if ($op == '=' and !$crit->not) {
1750 $where = array(
1751 "'?'",
1752 "(is_array($pred))",
1753 critere_IN_cas($idb, $boucles, 'COND', $arg, $op, array($pred), $col),
1754 $where
1755 );
1756 }
1757 $where = array("'?'", "!(is_array($pred)?count($pred):strlen($pred))", "''", $where);
1758 if ($where_complement) // condition annexe du type "AND (objet='article')"
1759 {
1760 $where_complement = array("'?'", "!(is_array($pred)?count($pred):strlen($pred))", "''", $where_complement);
1761 }
1762 }
1763
1764 $boucles[$idb]->where[] = $where;
1765 if ($where_complement) // condition annexe du type "AND (objet='article')"
1766 {
1767 $boucles[$idb]->where[] = $where_complement;
1768 }
1769 }
1770
1771
1772 /**
1773 * Décrit un critère non déclaré explicitement
1774 *
1775 * Décrit un critère non déclaré comme {id_article} {id_article>3} en
1776 * retournant un tableau de l'analyse si la colonne (ou l'alias) existe vraiment.
1777 *
1778 * Ajoute au passage pour chaque colonne utilisée (alias et colonne véritable)
1779 * un modificateur['criteres'][colonne].
1780 *
1781 * S'occupe de rechercher des exceptions, tel que
1782 * - les id_parent, id_enfant, id_secteur,
1783 * - des colonnes avec des exceptions déclarées,
1784 * - des critères de date (jour_relatif, ...),
1785 * - des critères sur tables jointes explicites (mots.titre),
1786 * - des critères sur tables de jointure non explicite (id_mot sur une boucle articles...)
1787 *
1788 *
1789 * @param string $idb Identifiant de la boucle
1790 * @param array $boucles AST du squelette
1791 * @param Critere $crit Paramètres du critère dans cette boucle
1792 * @return array|string
1793 * Liste si on trouve le champ :
1794 * - string $arg
1795 * Opérande avant l'opérateur : souvent la colonne d'application du critère, parfois un calcul
1796 * plus complexe dans le cas des dates.
1797 * - string $op
1798 * L'opérateur utilisé, tel que '='
1799 * - string[] $val
1800 * Liste de codes PHP obtenant les valeurs des comparaisons (ex: id_article sur la boucle parente)
1801 * Souvent (toujours ?) un tableau d'un seul élément.
1802 * - $col_alias
1803 * - $where_complement
1804 *
1805 * Chaîne vide si on ne trouve pas le champ...
1806 **/
1807 function calculer_critere_infixe($idb, &$boucles, $crit) {
1808
1809 $boucle = &$boucles[$idb];
1810 $type = $boucle->type_requete;
1811 $table = $boucle->id_table;
1812 $desc = $boucle->show;
1813 $col_vraie = null;
1814
1815 list($fct, $col, $op, $val, $args_sql) =
1816 calculer_critere_infixe_ops($idb, $boucles, $crit);
1817
1818 $col_alias = $col;
1819 $where_complement = false;
1820
1821 // Cas particulier : id_enfant => utiliser la colonne id_objet
1822 if ($col == 'id_enfant') {
1823 $col = $boucle->primary;
1824 }
1825
1826 // Cas particulier : id_parent => verifier les exceptions de tables
1827 if ((in_array($col, array('id_parent', 'id_secteur')) and isset($GLOBALS['exceptions_des_tables'][$table][$col]))
1828 or (isset($GLOBALS['exceptions_des_tables'][$table][$col]) and is_string($GLOBALS['exceptions_des_tables'][$table][$col]))
1829 ) {
1830 $col = $GLOBALS['exceptions_des_tables'][$table][$col];
1831 } // et possibilite de gerer un critere secteur sur des tables de plugins (ie forums)
1832 else {
1833 if (($col == 'id_secteur') and ($critere_secteur = charger_fonction("critere_secteur_$type", "public", true))) {
1834 $table = $critere_secteur($idb, $boucles, $val, $crit);
1835 }
1836
1837 // cas id_article=xx qui se mappe en id_objet=xx AND objet=article
1838 // sauf si exception declaree : sauter cette etape
1839 else {
1840 if (
1841 !isset($GLOBALS['exceptions_des_jointures'][table_objet_sql($table)][$col])
1842 and !isset($GLOBALS['exceptions_des_jointures'][$col])
1843 and count(trouver_champs_decomposes($col, $desc)) > 1
1844 ) {
1845 $e = decompose_champ_id_objet($col);
1846 $col = array_shift($e);
1847 $where_complement = primary_doublee($e, $table);
1848 } // Cas particulier : expressions de date
1849 else {
1850 if ($c = calculer_critere_infixe_date($idb, $boucles, $col)) {
1851 list($col, $col_vraie) = $c;
1852 $table = '';
1853 } // table explicitée {mots.titre}
1854 else {
1855 if (preg_match('/^(.*)\.(.*)$/', $col, $r)) {
1856 list(, $table, $col) = $r;
1857 $col_alias = $col;
1858
1859 $trouver_table = charger_fonction('trouver_table', 'base');
1860 if ($desc = $trouver_table($table, $boucle->sql_serveur)
1861 and isset($desc['field'][$col])
1862 and $cle = array_search($desc['table'], $boucle->from)
1863 ) {
1864 $table = $cle;
1865 } else {
1866 $table = trouver_jointure_champ($col, $boucle, array($table), ($crit->cond or $op != '='));
1867 }
1868 #$table = calculer_critere_externe_init($boucle, array($table), $col, $desc, ($crit->cond OR $op!='='), true);
1869 if (!$table) {
1870 return '';
1871 }
1872 }
1873 // si le champ n'est pas trouvé dans la table,
1874 // on cherche si une jointure peut l'obtenir
1875 elseif (@!array_key_exists($col, $desc['field'])) {
1876 // Champ joker * des iterateurs DATA qui accepte tout
1877 if (@array_key_exists('*', $desc['field'])) {
1878 $desc['field'][$col_vraie ? $col_vraie : $col] = ''; // on veut pas de cast INT par defaut car le type peut etre n'importe quoi dans les boucles DATA
1879 }
1880 else {
1881 $r = calculer_critere_infixe_externe($boucle, $crit, $op, $desc, $col, $col_alias, $table);
1882 if (!$r) {
1883 return '';
1884 }
1885 list($col, $col_alias, $table, $where_complement, $desc) = $r;
1886 }
1887 }
1888 }
1889 }
1890 }
1891 }
1892
1893 $col_vraie = ($col_vraie ? $col_vraie : $col);
1894 // Dans tous les cas,
1895 // virer les guillemets eventuels autour d'un int (qui sont refuses par certains SQL)
1896 // et passer dans sql_quote avec le type si connu
1897 // et int sinon si la valeur est numerique
1898 // sinon introduire le vrai type du champ si connu dans le sql_quote (ou int NOT NULL sinon)
1899 // Ne pas utiliser intval, PHP tronquant les Bigint de SQL
1900 if ($op == '=' or in_array($op, $GLOBALS['table_criteres_infixes'])) {
1901
1902 // defaire le quote des int et les passer dans sql_quote avec le bon type de champ si on le connait, int sinon
1903 // prendre en compte le debug ou la valeur arrive avec un commentaire PHP en debut
1904 if (preg_match(",^\\A(\s*//.*?$\s*)?\"'(-?\d+)'\"\\z,ms", $val[0], $r)) {
1905 $val[0] = $r[1] . '"' . sql_quote($r[2], $boucle->sql_serveur,
1906 (isset($desc['field'][$col_vraie]) ? $desc['field'][$col_vraie] : 'int NOT NULL')) . '"';
1907 }
1908
1909 // sinon expliciter les
1910 // sql_quote(truc) en sql_quote(truc,'',type)
1911 // sql_quote(truc,serveur) en sql_quote(truc,serveur,type)
1912 // sql_quote(truc,serveur,'') en sql_quote(truc,serveur,type)
1913 // sans toucher aux
1914 // sql_quote(truc,'','varchar(10) DEFAULT \'oui\' COLLATE NOCASE')
1915 // sql_quote(truc,'','varchar')
1916 elseif (preg_match('/\Asql_quote[(](.*?)(,[^)]*?)?(,[^)]*(?:\(\d+\)[^)]*)?)?[)]\s*\z/ms', $val[0], $r)
1917 // si pas deja un type
1918 and (!isset($r[3]) or !$r[3] or !trim($r[3],", '"))
1919 ) {
1920 $r = $r[1]
1921 . ((isset($r[2]) and $r[2]) ? $r[2] : ",''")
1922 . ",'" . (isset($desc['field'][$col_vraie]) ? addslashes($desc['field'][$col_vraie]) : 'int NOT NULL') . "'";
1923 $val[0] = "sql_quote($r)";
1924 }
1925 }
1926 // Indicateur pour permettre aux fonctionx boucle_X de modifier
1927 // leurs requetes par defaut, notamment le champ statut
1928 // Ne pas confondre champs de la table principale et des jointures
1929 if ($table === $boucle->id_table) {
1930 $boucles[$idb]->modificateur['criteres'][$col_vraie] = true;
1931 if ($col_alias != $col_vraie) {
1932 $boucles[$idb]->modificateur['criteres'][$col_alias] = true;
1933 }
1934 }
1935
1936 // ajout pour le cas special d'une condition sur le champ statut:
1937 // il faut alors interdire a la fonction de boucle
1938 // de mettre ses propres criteres de statut
1939 // http://www.spip.net/@statut (a documenter)
1940 // garde pour compatibilite avec code des plugins anterieurs, mais redondant avec la ligne precedente
1941 if ($col == 'statut') {
1942 $boucles[$idb]->statut = true;
1943 }
1944
1945 // inserer le nom de la table SQL devant le nom du champ
1946 if ($table) {
1947 if ($col[0] == "`") {
1948 $arg = "$table." . substr($col, 1, -1);
1949 } else {
1950 $arg = "$table.$col";
1951 }
1952 } else {
1953 $arg = $col;
1954 }
1955
1956 // inserer la fonction SQL
1957 if ($fct) {
1958 $arg = "$fct($arg$args_sql)";
1959 }
1960
1961 return array($arg, $op, $val, $col_alias, $where_complement);
1962 }
1963
1964
1965 /**
1966 * Décrit un critère non déclaré explicitement, sur un champ externe à la table
1967 *
1968 * Décrit un critère non déclaré comme {id_article} {id_article>3} qui correspond
1969 * à un champ non présent dans la table, et donc à retrouver par jointure si possible.
1970 *
1971 * @param Boucle $boucle Description de la boucle
1972 * @param Critere $crit Paramètres du critère dans cette boucle
1973 * @param string $op L'opérateur utilisé, tel que '='
1974 * @param array $desc Description de la table
1975 * @param string $col Nom de la colonne à trouver (la véritable)
1976 * @param string $col_alias Alias de la colonne éventuel utilisé dans le critère ex: id_enfant
1977 * @param string $table Nom de la table SQL de la boucle
1978 * @return array|string
1979 * Liste si jointure possible :
1980 * - string $col
1981 * - string $col_alias
1982 * - string $table
1983 * - array $where
1984 * - array $desc
1985 *
1986 * Chaîne vide si on ne trouve pas le champ par jointure...
1987 **/
1988 function calculer_critere_infixe_externe($boucle, $crit, $op, $desc, $col, $col_alias, $table) {
1989
1990 $where = '';
1991
1992 $calculer_critere_externe = 'calculer_critere_externe_init';
1993 // gestion par les plugins des jointures tordues
1994 // pas automatiques mais necessaires
1995 $table_sql = table_objet_sql($table);
1996 if (isset($GLOBALS['exceptions_des_jointures'][$table_sql])
1997 and is_array($GLOBALS['exceptions_des_jointures'][$table_sql])
1998 and
1999 (
2000 isset($GLOBALS['exceptions_des_jointures'][$table_sql][$col])
2001 or
2002 isset($GLOBALS['exceptions_des_jointures'][$table_sql][''])
2003 )
2004 ) {
2005 $t = $GLOBALS['exceptions_des_jointures'][$table_sql];
2006 $index = isset($t[$col])
2007 ? $t[$col] : (isset($t['']) ? $t[''] : array());
2008
2009 if (count($index) == 3) {
2010 list($t, $col, $calculer_critere_externe) = $index;
2011 } elseif (count($index) == 2) {
2012 list($t, $col) = $t[$col];
2013 } elseif (count($index) == 1) {
2014 list($calculer_critere_externe) = $index;
2015 $t = $table;
2016 } else {
2017 $t = '';
2018 } // jointure non declaree. La trouver.
2019 } elseif (isset($GLOBALS['exceptions_des_jointures'][$col])) {
2020 list($t, $col) = $GLOBALS['exceptions_des_jointures'][$col];
2021 } else {
2022 $t = '';
2023 } // jointure non declaree. La trouver.
2024
2025 // ici on construit le from pour fournir $col en piochant dans les jointures
2026
2027 // si des jointures explicites sont fournies, on cherche d'abord dans celles ci
2028 // permet de forcer une table de lien quand il y a ambiguite
2029 // <BOUCLE_(DOCUMENTS documents_liens){id_mot}>
2030 // alors que <BOUCLE_(DOCUMENTS){id_mot}> produit la meme chose que <BOUCLE_(DOCUMENTS mots_liens){id_mot}>
2031 $table = "";
2032 if ($boucle->jointures_explicites) {
2033 $jointures_explicites = explode(' ', $boucle->jointures_explicites);
2034 $table = $calculer_critere_externe($boucle, $jointures_explicites, $col, $desc, ($crit->cond or $op != '='), $t);
2035 }
2036
2037 // et sinon on cherche parmi toutes les jointures declarees
2038 if (!$table) {
2039 $table = $calculer_critere_externe($boucle, $boucle->jointures, $col, $desc, ($crit->cond or $op != '='), $t);
2040 }
2041
2042 if (!$table) {
2043 return '';
2044 }
2045
2046 // il ne reste plus qu'a trouver le champ dans les from
2047 list($nom, $desc, $cle) = trouver_champ_exterieur($col, $boucle->from, $boucle);
2048
2049 if (count($cle) > 1 or reset($cle) !== $col) {
2050 $col_alias = $col; // id_article devient juste le nom d'origine
2051 if (count($cle) > 1 and reset($cle) == 'id_objet') {
2052 $e = decompose_champ_id_objet($col);
2053 $col = array_shift($e);
2054 $where = primary_doublee($e, $table);
2055 } else {
2056 $col = reset($cle);
2057 }
2058 }
2059
2060 return array($col, $col_alias, $table, $where, $desc);
2061 }
2062
2063
2064 /**
2065 * Calcule une condition WHERE entre un nom du champ et une valeur
2066 *
2067 * Ne pas appliquer sql_quote lors de la compilation,
2068 * car on ne connait pas le serveur SQL
2069 *
2070 * @todo Ce nom de fonction n'est pas très clair ?
2071 *
2072 * @param array $decompose Liste nom du champ, code PHP pour obtenir la valeur
2073 * @param string $table Nom de la table
2074 * @return string[]
2075 * Liste de 3 éléments pour une description where du compilateur :
2076 * - operateur (=),
2077 * - table.champ,
2078 * - valeur
2079 **/
2080 function primary_doublee($decompose, $table) {
2081 $e1 = reset($decompose);
2082 $e2 = "sql_quote('" . end($decompose) . "')";
2083
2084 return array("'='", "'$table." . $e1 . "'", $e2);
2085 }
2086
2087 /**
2088 * Champ hors table, ça ne peut être qu'une jointure.
2089 *
2090 * On cherche la table du champ et on regarde si elle est déjà jointe
2091 * Si oui et qu'on y cherche un champ nouveau, pas de jointure supplementaire
2092 * Exemple: criteres {titre_mot=...}{type_mot=...}
2093 * Dans les 2 autres cas ==> jointure
2094 * (Exemple: criteres {type_mot=...}{type_mot=...} donne 2 jointures
2095 * pour selectioner ce qui a exactement ces 2 mots-cles.
2096 *
2097 * @param Boucle $boucle
2098 * Description de la boucle
2099 * @param array $joints
2100 * Liste de jointures possibles (ex: $boucle->jointures ou $boucle->jointures_explicites)
2101 * @param string $col
2102 * Colonne cible de la jointure
2103 * @param array $desc
2104 * Description de la table
2105 * @param bool $cond
2106 * Flag pour savoir si le critère est conditionnel ou non
2107 * @param bool|string $checkarrivee
2108 * string : nom de la table jointe où on veut trouver le champ.
2109 * n'a normalement pas d'appel sans $checkarrivee.
2110 * @return string
2111 * Alias de la table de jointure (Lx)
2112 * Vide sinon.
2113 */
2114 function calculer_critere_externe_init(&$boucle, $joints, $col, $desc, $cond, $checkarrivee = false) {
2115 // si on demande un truc du genre spip_mots
2116 // avec aussi spip_mots_liens dans les jointures dispo
2117 // et qu'on est la
2118 // il faut privilegier la jointure directe en 2 etapes spip_mots_liens, spip_mots
2119 if ($checkarrivee
2120 and is_string($checkarrivee)
2121 and $a = table_objet($checkarrivee)
2122 and in_array($a . '_liens', $joints)
2123 ) {
2124 if ($res = calculer_lien_externe_init($boucle, $joints, $col, $desc, $cond, $checkarrivee)) {
2125 return $res;
2126 }
2127 }
2128 foreach ($joints as $joint) {
2129 if ($arrivee = trouver_champ_exterieur($col, array($joint), $boucle, $checkarrivee)) {
2130 // alias de table dans le from
2131 $t = array_search($arrivee[0], $boucle->from);
2132 // recuperer la cle id_xx eventuellement decomposee en (id_objet,objet)
2133 $cols = $arrivee[2];
2134 // mais on ignore la 3eme cle si presente qui correspond alors au point de depart
2135 if (count($cols) > 2) {
2136 array_pop($cols);
2137 }
2138 if ($t) {
2139 // la table est déjà dans le FROM, on vérifie si le champ est utilisé.
2140 $joindre = false;
2141 foreach ($cols as $col) {
2142 $c = '/\b' . $t . ".$col" . '\b/';
2143 if (trouver_champ($c, $boucle->where)) {
2144 $joindre = true;
2145 } else {
2146 // mais ca peut etre dans le FIELD pour le Having
2147 $c = "/FIELD.$t" . ".$col,/";
2148 if (trouver_champ($c, $boucle->select)) {
2149 $joindre = true;
2150 }
2151 }
2152 }
2153 if (!$joindre) {
2154 return $t;
2155 }
2156 }
2157 array_pop($arrivee);
2158 if ($res = calculer_jointure($boucle, array($boucle->id_table, $desc), $arrivee, $cols, $cond, 1)) {
2159 return $res;
2160 }
2161 }
2162 }
2163
2164 return '';
2165
2166 }
2167
2168 /**
2169 * Générer directement une jointure via une table de lien spip_xxx_liens
2170 * pour un critère {id_xxx}
2171 *
2172 * @todo $checkarrivee doit être obligatoire ici ?
2173 *
2174 * @param Boucle $boucle
2175 * Description de la boucle
2176 * @param array $joints
2177 * Liste de jointures possibles (ex: $boucle->jointures ou $boucle->jointures_explicites)
2178 * @param string $col
2179 * Colonne cible de la jointure
2180 * @param array $desc
2181 * Description de la table
2182 * @param bool $cond
2183 * Flag pour savoir si le critère est conditionnel ou non
2184 * @param bool|string $checkarrivee
2185 * string : nom de la table jointe où on veut trouver le champ.
2186 * n'a normalement pas d'appel sans $checkarrivee.
2187 * @return string
2188 * Alias de la table de jointure (Lx)
2189 */
2190 function calculer_lien_externe_init(&$boucle, $joints, $col, $desc, $cond, $checkarrivee = false) {
2191 $primary_arrivee = id_table_objet($checkarrivee);
2192
2193 // [FIXME] $checkarrivee peut-il arriver avec false ????
2194 $intermediaire = trouver_champ_exterieur($primary_arrivee, $joints, $boucle, $checkarrivee . "_liens");
2195 $arrivee = trouver_champ_exterieur($col, $joints, $boucle, $checkarrivee);
2196
2197 if (!$intermediaire or !$arrivee) {
2198 return '';
2199 }
2200 array_pop($intermediaire); // enlever la cle en 3eme argument
2201 array_pop($arrivee); // enlever la cle en 3eme argument
2202
2203 $res = fabrique_jointures($boucle,
2204 array(
2205 array(
2206 $boucle->id_table,
2207 $intermediaire,
2208 array(id_table_objet($desc['table_objet']), 'id_objet', 'objet', $desc['type'])
2209 ),
2210 array(reset($intermediaire), $arrivee, $primary_arrivee)
2211 ), $cond, $desc, $boucle->id_table, array($col));
2212
2213 return $res;
2214 }
2215
2216
2217 /**
2218 * Recherche la présence d'un champ dans une valeur de tableau
2219 *
2220 * @param string $champ
2221 * Expression régulière pour trouver un champ donné.
2222 * Exemple : /\barticles.titre\b/
2223 * @param array $where
2224 * Tableau de valeurs dans lesquels chercher le champ.
2225 * @return bool
2226 * true si le champ est trouvé quelque part dans $where
2227 * false sinon.
2228 **/
2229 function trouver_champ($champ, $where) {
2230 if (!is_array($where)) {
2231 return preg_match($champ, $where);
2232 } else {
2233 foreach ($where as $clause) {
2234 if (trouver_champ($champ, $clause)) {
2235 return true;
2236 }
2237 }
2238
2239 return false;
2240 }
2241 }
2242
2243
2244 /**
2245 * Détermine l'operateur et les opérandes d'un critère non déclaré
2246 *
2247 * Lorsque l'opérateur n'est pas explicite comme sur {id_article>0} c'est
2248 * l'opérateur '=' qui est utilisé.
2249 *
2250 * Traite les cas particuliers id_parent, id_enfant, date, lang
2251 *
2252 * @param string $idb Identifiant de la boucle
2253 * @param array $boucles AST du squelette
2254 * @param Critere $crit Paramètres du critère dans cette boucle
2255 * @return array
2256 * Liste :
2257 * - string $fct Nom d'une fonction SQL sur le champ ou vide (ex: SUM)
2258 * - string $col Nom de la colonne SQL utilisée
2259 * - string $op Opérateur
2260 * - string[] $val
2261 * Liste de codes PHP obtenant les valeurs des comparaisons (ex: id_article sur la boucle parente)
2262 * Souvent un tableau d'un seul élément.
2263 * - string $args_sql Suite des arguments du critère. ?
2264 **/
2265 function calculer_critere_infixe_ops($idb, &$boucles, $crit) {
2266 // cas d'une valeur comparee a elle-meme ou son referent
2267 if (count($crit->param) == 0) {
2268 $op = '=';
2269 $col = $val = $crit->op;
2270 if (preg_match('/^(.*)\.(.*)$/', $col, $r)) {
2271 $val = $r[2];
2272 }
2273 // Cas special {lang} : aller chercher $GLOBALS['spip_lang']
2274 if ($val == 'lang') {
2275 $val = array(kwote('$GLOBALS[\'spip_lang\']'));
2276 } else {
2277 $defaut = null;
2278 if ($val == 'id_parent') {
2279 // Si id_parent, comparer l'id_parent avec l'id_objet
2280 // de la boucle superieure.... faudrait verifier qu'il existe
2281 // pour eviter l'erreur SQL
2282 $val = $boucles[$idb]->primary;
2283 // mais si pas de boucle superieure, prendre id_parent dans l'env
2284 $defaut = "@\$Pile[0]['id_parent']";
2285 } elseif ($val == 'id_enfant') {
2286 // Si id_enfant, comparer l'id_objet avec l'id_parent
2287 // de la boucle superieure
2288 $val = 'id_parent';
2289 } elseif ($crit->cond and ($col == "date" or $col == "date_redac")) {
2290 // un critere conditionnel sur date est traite a part
2291 // car la date est mise d'office par SPIP,
2292 $defaut = "(\$Pile[0]['{$col}_default']?'':\$Pile[0]['" . $col . "'])";
2293 }
2294
2295 $val = calculer_argument_precedent($idb, $val, $boucles, $defaut);
2296 $val = array(kwote($val));
2297 }
2298 } else {
2299 // comparaison explicite
2300 // le phraseur impose que le premier param soit du texte
2301 $params = $crit->param;
2302 $op = $crit->op;
2303 if ($op == '==') {
2304 $op = 'REGEXP';
2305 }
2306 $col = array_shift($params);
2307 $col = $col[0]->texte;
2308
2309 $val = array();
2310 $desc = array('id_mere' => $idb);
2311 $parent = $boucles[$idb]->id_parent;
2312
2313 // Dans le cas {x=='#DATE'} etc, defaire le travail du phraseur,
2314 // celui ne sachant pas ce qu'est un critere infixe
2315 // et a fortiori son 2e operande qu'entoure " ou '
2316 if (count($params) == 1
2317 and count($params[0]) == 3
2318 and $params[0][0]->type == 'texte'
2319 and $params[0][2]->type == 'texte'
2320 and ($p = $params[0][0]->texte) == $params[0][2]->texte
2321 and (($p == "'") or ($p == '"'))
2322 and $params[0][1]->type == 'champ'
2323 ) {
2324 $val[] = "$p\\$p#" . $params[0][1]->nom_champ . "\\$p$p";
2325 } else {
2326 foreach ((($op != 'IN') ? $params : calculer_vieux_in($params)) as $p) {
2327 $a = calculer_liste($p, $desc, $boucles, $parent);
2328 if (strcasecmp($op, 'IN') == 0) {
2329 $val[] = $a;
2330 } else {
2331 $val[] = kwote($a, $boucles[$idb]->sql_serveur, 'char');
2332 } // toujours quoter en char ici
2333 }
2334 }
2335 }
2336
2337 $fct = $args_sql = '';
2338 // fonction SQL ?
2339 // chercher FONCTION(champ) tel que CONCAT(titre,descriptif)
2340 if (preg_match('/^(.*)' . SQL_ARGS . '$/', $col, $m)) {
2341 $fct = $m[1];
2342 preg_match('/^\(([^,]*)(.*)\)$/', $m[2], $a);
2343 $col = $a[1];
2344 if (preg_match('/^(\S*)(\s+AS\s+.*)$/i', $col, $m)) {
2345 $col = $m[1];
2346 $args_sql = $m[2];
2347 }
2348 $args_sql .= $a[2];
2349 }
2350
2351 return array($fct, $col, $op, $val, $args_sql);
2352 }
2353
2354 // compatibilite ancienne version
2355
2356 // http://code.spip.net/@calculer_vieux_in
2357 function calculer_vieux_in($params) {
2358 $deb = $params[0][0];
2359 $k = count($params) - 1;
2360 $last = $params[$k];
2361 $j = count($last) - 1;
2362 $last = $last[$j];
2363 $n = isset($last->texte) ? strlen($last->texte) : 0;
2364
2365 if (!((isset($deb->texte[0]) and $deb->texte[0] == '(')
2366 && (isset($last->texte[$n - 1]) and $last->texte[$n - 1] == ')'))
2367 ) {
2368 return $params;
2369 }
2370 $params[0][0]->texte = substr($deb->texte, 1);
2371 // attention, on peut avoir k=0,j=0 ==> recalculer
2372 $last = $params[$k][$j];
2373 $n = strlen($last->texte);
2374 $params[$k][$j]->texte = substr($last->texte, 0, $n - 1);
2375 $newp = array();
2376 foreach ($params as $v) {
2377 if ($v[0]->type != 'texte') {
2378 $newp[] = $v;
2379 } else {
2380 foreach (explode(',', $v[0]->texte) as $x) {
2381 $t = new Texte;
2382 $t->texte = $x;
2383 $newp[] = array($t);
2384 }
2385 }
2386 }
2387
2388 return $newp;
2389 }
2390
2391 /**
2392 * Calcule les cas particuliers de critères de date
2393 *
2394 * Lorsque la colonne correspond à un critère de date, tel que
2395 * jour, jour_relatif, jour_x, age, age_relatif, age_x...
2396 *
2397 * @param string $idb Identifiant de la boucle
2398 * @param array $boucles AST du squelette
2399 * @param string $col Nom du champ demandé
2400 * @return string|array
2401 * chaine vide si ne correspond pas à une date,
2402 * sinon liste
2403 * - expression SQL de calcul de la date,
2404 * - nom de la colonne de date (si le calcul n'est pas relatif)
2405 **/
2406 function calculer_critere_infixe_date($idb, &$boucles, $col) {
2407 if (!preg_match(",^((age|jour|mois|annee)_relatif|date|mois|annee|jour|heure|age)(_[a-z]+)?$,", $col, $regs)) {
2408 return '';
2409 }
2410
2411 $boucle = $boucles[$idb];
2412 $table = $boucle->show;
2413
2414 // si c'est une colonne de la table, ne rien faire
2415 if (isset($table['field'][$col])) {
2416 return '';
2417 }
2418
2419 if (!$table['date'] && !isset($GLOBALS['table_date'][$table['id_table']])) {
2420 return '';
2421 }
2422 $pred = $date_orig = isset($GLOBALS['table_date'][$table['id_table']]) ? $GLOBALS['table_date'][$table['id_table']] : $table['date'];
2423
2424 $col = $regs[1];
2425 if (isset($regs[3]) and $suite = $regs[3]) {
2426 # Recherche de l'existence du champ date_xxxx,
2427 # si oui choisir ce champ, sinon choisir xxxx
2428
2429 if (isset($table['field']["date$suite"])) {
2430 $date_orig = 'date' . $suite;
2431 } else {
2432 $date_orig = substr($suite, 1);
2433 }
2434 $pred = $date_orig;
2435 } else {
2436 if (isset($regs[2]) and $rel = $regs[2]) {
2437 $pred = 'date';
2438 }
2439 }
2440
2441 $date_compare = "\"' . normaliser_date(" .
2442 calculer_argument_precedent($idb, $pred, $boucles) .
2443 ") . '\"";
2444
2445 $col_vraie = $date_orig;
2446 $date_orig = $boucle->id_table . '.' . $date_orig;
2447
2448 switch ($col) {
2449 case 'date':
2450 $col = $date_orig;
2451 break;
2452 case 'jour':
2453 $col = "DAYOFMONTH($date_orig)";
2454 break;
2455 case 'mois':
2456 $col = "MONTH($date_orig)";
2457 break;
2458 case 'annee':
2459 $col = "YEAR($date_orig)";
2460 break;
2461 case 'heure':
2462 $col = "DATE_FORMAT($date_orig, \\'%H:%i\\')";
2463 break;
2464 case 'age':
2465 $col = calculer_param_date("NOW()", $date_orig);
2466 $col_vraie = "";// comparer a un int (par defaut)
2467 break;
2468 case 'age_relatif':
2469 $col = calculer_param_date($date_compare, $date_orig);
2470 $col_vraie = "";// comparer a un int (par defaut)
2471 break;
2472 case 'jour_relatif':
2473 $col = "(TO_DAYS(" . $date_compare . ")-TO_DAYS(" . $date_orig . "))";
2474 $col_vraie = "";// comparer a un int (par defaut)
2475 break;
2476 case 'mois_relatif':
2477 $col = "MONTH(" . $date_compare . ")-MONTH(" .
2478 $date_orig . ")+12*(YEAR(" . $date_compare .
2479 ")-YEAR(" . $date_orig . "))";
2480 $col_vraie = "";// comparer a un int (par defaut)
2481 break;
2482 case 'annee_relatif':
2483 $col = "YEAR(" . $date_compare . ")-YEAR(" .
2484 $date_orig . ")";
2485 $col_vraie = "";// comparer a un int (par defaut)
2486 break;
2487 }
2488
2489 return array($col, $col_vraie);
2490 }
2491
2492 /**
2493 * Calcule l'expression SQL permettant de trouver un nombre de jours écoulés.
2494 *
2495 * Le calcul SQL retournera un nombre de jours écoulés entre la date comparée
2496 * et la colonne SQL indiquée
2497 *
2498 * @param string $date_compare
2499 * Code PHP permettant d'obtenir le timestamp référent.
2500 * C'est à partir de lui que l'on compte les jours
2501 * @param string $date_orig
2502 * Nom de la colonne SQL qui possède la date
2503 * @return string
2504 * Expression SQL calculant le nombre de jours écoulé entre une valeur
2505 * de colonne SQL et une date.
2506 **/
2507 function calculer_param_date($date_compare, $date_orig) {
2508 if (preg_match(",'\" *\.(.*)\. *\"',", $date_compare, $r)) {
2509 $init = "'\" . (\$x = $r[1]) . \"'";
2510 $date_compare = '\'$x\'';
2511 } else {
2512 $init = $date_compare;
2513 }
2514
2515 return
2516 // optimisation : mais prevoir le support SQLite avant
2517 "TIMESTAMPDIFF(HOUR,$date_orig,$init)/24";
2518 }
2519
2520 /**
2521 * Compile le critère {source} d'une boucle DATA
2522 *
2523 * Permet de déclarer le mode d'obtention des données dans une boucle
2524 * DATA (premier argument) et les données (la suite).
2525 *
2526 * @example
2527 * (DATA){source mode, "xxxxxx", arg, arg, arg}
2528 * (DATA){source tableau, #LISTE{un,deux,trois}}
2529 *
2530 * @param string $idb Identifiant de la boucle
2531 * @param array $boucles AST du squelette
2532 * @param Critere $crit Paramètres du critère dans cette boucle
2533 */
2534 function critere_DATA_source_dist($idb, &$boucles, $crit) {
2535 $boucle = &$boucles[$idb];
2536
2537 $args = array();
2538 foreach ($crit->param as &$param) {
2539 array_push($args,
2540 calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent));
2541 }
2542
2543 $boucle->hash .= '
2544 $command[\'sourcemode\'] = ' . array_shift($args) . ";\n";
2545
2546 $boucle->hash .= '
2547 $command[\'source\'] = array(' . join(', ', $args) . ");\n";
2548 }
2549
2550
2551 /**
2552 * Compile le critère {datasource} d'une boucle DATA
2553 *
2554 * Permet de déclarer le mode d'obtention des données dans une boucle DATA
2555 *
2556 * @deprecated Utiliser directement le critère {source}
2557 *
2558 * @param string $idb Identifiant de la boucle
2559 * @param array $boucles AST du squelette
2560 * @param Critere $crit Paramètres du critère dans cette boucle
2561 */
2562 function critere_DATA_datasource_dist($idb, &$boucles, $crit) {
2563 $boucle = &$boucles[$idb];
2564 $boucle->hash .= '
2565 $command[\'source\'] = array(' . calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent) . ');
2566 $command[\'sourcemode\'] = ' . calculer_liste($crit->param[1], array(), $boucles, $boucles[$idb]->id_parent) . ';';
2567 }
2568
2569
2570 /**
2571 * Compile le critère {datacache} d'une boucle DATA
2572 *
2573 * Permet de transmettre une durée de cache (time to live) utilisée
2574 * pour certaines sources d'obtention des données (par exemple RSS),
2575 * indiquant alors au bout de combien de temps la donnée est à réobtenir.
2576 *
2577 * La durée par défaut est 1 journée.
2578 *
2579 * @param string $idb Identifiant de la boucle
2580 * @param array $boucles AST du squelette
2581 * @param Critere $crit Paramètres du critère dans cette boucle
2582 */
2583 function critere_DATA_datacache_dist($idb, &$boucles, $crit) {
2584 $boucle = &$boucles[$idb];
2585 $boucle->hash .= '
2586 $command[\'datacache\'] = ' . calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent) . ';';
2587 }
2588
2589
2590 /**
2591 * Compile le critère {args} d'une boucle PHP
2592 *
2593 * Permet de passer des arguments à un iterateur non-spip
2594 * (PHP:xxxIterator){args argument1, argument2, argument3}
2595 *
2596 * @param string $idb Identifiant de la boucle
2597 * @param array $boucles AST du squelette
2598 * @param Critere $crit Paramètres du critère dans cette boucle
2599 */
2600 function critere_php_args_dist($idb, &$boucles, $crit) {
2601 $boucle = &$boucles[$idb];
2602 $boucle->hash .= '$command[\'args\']=array();';
2603 foreach ($crit->param as $param) {
2604 $boucle->hash .= '
2605 $command[\'args\'][] = ' . calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent) . ';';
2606 }
2607 }
2608
2609 /**
2610 * Compile le critère {liste} d'une boucle DATA
2611 *
2612 * Passe une liste de données à l'itérateur DATA
2613 *
2614 * @example
2615 * (DATA){liste X1, X2, X3}
2616 * équivalent à (DATA){source tableau,#LISTE{X1, X2, X3}}
2617 *
2618 * @param string $idb Identifiant de la boucle
2619 * @param array $boucles AST du squelette
2620 * @param Critere $crit Paramètres du critère dans cette boucle
2621 */
2622 function critere_DATA_liste_dist($idb, &$boucles, $crit) {
2623 $boucle = &$boucles[$idb];
2624 $boucle->hash .= "\n\t" . '$command[\'liste\'] = array();' . "\n";
2625 foreach ($crit->param as $param) {
2626 $boucle->hash .= "\t" . '$command[\'liste\'][] = ' . calculer_liste($param, array(), $boucles,
2627 $boucles[$idb]->id_parent) . ";\n";
2628 }
2629 }
2630
2631 /**
2632 * Compile le critère {enum} d'une boucle DATA
2633 *
2634 * Passe les valeurs de début et de fin d'une énumération, qui seront
2635 * vues comme une liste d'autant d'éléments à parcourir pour aller du
2636 * début à la fin.
2637 *
2638 * Cela utilisera la fonction range() de PHP.
2639 *
2640 * @example
2641 * (DATA){enum Xdebut, Xfin}
2642 * (DATA){enum a,z}
2643 * (DATA){enum z,a}
2644 * (DATA){enum 1.0,9.2}
2645 *
2646 * @link http://php.net/manual/fr/function.range.php
2647 *
2648 * @param string $idb Identifiant de la boucle
2649 * @param array $boucles AST du squelette
2650 * @param Critere $crit Paramètres du critère dans cette boucle
2651 */
2652 function critere_DATA_enum_dist($idb, &$boucles, $crit) {
2653 $boucle = &$boucles[$idb];
2654 $boucle->hash .= "\n\t" . '$command[\'enum\'] = array();' . "\n";
2655 foreach ($crit->param as $param) {
2656 $boucle->hash .= "\t" . '$command[\'enum\'][] = ' . calculer_liste($param, array(), $boucles,
2657 $boucles[$idb]->id_parent) . ";\n";
2658 }
2659 }
2660
2661 /**
2662 * Compile le critère {datapath} d'une boucle DATA
2663 *
2664 * Extrait un chemin d'un tableau de données
2665 *
2666 * (DATA){datapath query.results}
2667 *
2668 * @param string $idb Identifiant de la boucle
2669 * @param array $boucles AST du squelette
2670 * @param Critere $crit Paramètres du critère dans cette boucle
2671 */
2672 function critere_DATA_datapath_dist($idb, &$boucles, $crit) {
2673 $boucle = &$boucles[$idb];
2674 foreach ($crit->param as $param) {
2675 $boucle->hash .= '
2676 $command[\'datapath\'][] = ' . calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent) . ';';
2677 }
2678 }
2679
2680
2681 /**
2682 * Compile le critère {si}
2683 *
2684 * Le critère {si condition} est applicable à toutes les boucles et conditionne
2685 * l'exécution de la boucle au résultat de la condition. La partie alternative
2686 * de la boucle est alors affichée si une condition n'est pas remplie (comme
2687 * lorsque la boucle ne ramène pas de résultat).
2688 * La différence étant que si la boucle devait réaliser une requête SQL
2689 * (par exemple une boucle ARTICLES), celle ci n'est pas réalisée si la
2690 * condition n'est pas remplie.
2691 *
2692 * Les valeurs de la condition sont forcément extérieures à cette boucle
2693 * (sinon il faudrait l'exécuter pour connaître le résultat, qui doit tester
2694 * si on exécute la boucle !)
2695 *
2696 * Si plusieurs critères {si} sont présents, ils sont cumulés :
2697 * si une seule des conditions n'est pas vérifiée, la boucle n'est pas exécutée.
2698 *
2699 * @example
2700 * {si #ENV{exec}|=={article}}
2701 * {si (#_contenu:GRAND_TOTAL|>{10})}
2702 * {si #AUTORISER{voir,articles}}
2703 *
2704 * @param string $idb Identifiant de la boucle
2705 * @param array $boucles AST du squelette
2706 * @param Critere $crit Paramètres du critère dans cette boucle
2707 */
2708 function critere_si_dist($idb, &$boucles, $crit) {
2709 $boucle = &$boucles[$idb];
2710 // il faut initialiser 1 fois le tableau a chaque appel de la boucle
2711 // (par exemple lorsque notre boucle est appelee dans une autre boucle)
2712 // mais ne pas l'initialiser n fois si il y a n criteres {si } dans la boucle !
2713 $boucle->hash .= "\n\tif (!isset(\$si_init)) { \$command['si'] = array(); \$si_init = true; }\n";
2714 if ($crit->param) {
2715 foreach ($crit->param as $param) {
2716 $boucle->hash .= "\t\$command['si'][] = "
2717 . calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent) . ";\n";
2718 }
2719 // interdire {si 0} aussi !
2720 } else {
2721 $boucle->hash .= '$command[\'si\'][] = 0;';
2722 }
2723 }
2724
2725 /**
2726 * Compile le critère {tableau} d'une boucle POUR
2727 *
2728 * {tableau #XX} pour compatibilite ascendante boucle POUR
2729 * ... préférer la notation (DATA){source tableau,#XX}
2730 *
2731 * @deprecated Utiliser une boucle (DATA){source tableau,#XX}
2732 *
2733 * @param string $idb Identifiant de la boucle
2734 * @param array $boucles AST du squelette
2735 * @param Critere $crit Paramètres du critère dans cette boucle
2736 */
2737 function critere_POUR_tableau_dist($idb, &$boucles, $crit) {
2738 $boucle = &$boucles[$idb];
2739 $boucle->hash .= '
2740 $command[\'source\'] = array(' . calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent) . ');
2741 $command[\'sourcemode\'] = \'table\';';
2742 }
2743
2744
2745 /**
2746 * Compile le critère {noeud}
2747 *
2748 * Trouver tous les objets qui ont des enfants (les noeuds de l'arbre)
2749 * {noeud}
2750 * {!noeud} retourne les feuilles
2751 *
2752 * @global array $exceptions_des_tables
2753 *
2754 * @param string $idb Identifiant de la boucle
2755 * @param array $boucles AST du squelette
2756 * @param Critere $crit Paramètres du critère dans cette boucle
2757 */
2758 function critere_noeud_dist($idb, &$boucles, $crit) {
2759
2760 $not = $crit->not;
2761 $boucle = &$boucles[$idb];
2762 $primary = $boucle->primary;
2763
2764 if (!$primary or strpos($primary, ',')) {
2765 erreur_squelette(_T('zbug_doublon_sur_table_sans_cle_primaire'), $boucle);
2766
2767 return;
2768 }
2769 $table = $boucle->type_requete;
2770 $table_sql = table_objet_sql(objet_type($table));
2771
2772 $id_parent = isset($GLOBALS['exceptions_des_tables'][$boucle->id_table]['id_parent']) ?
2773 $GLOBALS['exceptions_des_tables'][$boucle->id_table]['id_parent'] :
2774 'id_parent';
2775
2776 $in = "IN";
2777 $where = array("'IN'", "'$boucle->id_table." . "$primary'", "'('.sql_get_select('$id_parent', '$table_sql').')'");
2778 if ($not) {
2779 $where = array("'NOT'", $where);
2780 }
2781
2782 $boucle->where[] = $where;
2783 }
2784
2785 /**
2786 * Compile le critère {feuille}
2787 *
2788 * Trouver tous les objets qui n'ont pas d'enfants (les feuilles de l'arbre)
2789 * {feuille}
2790 * {!feuille} retourne les noeuds
2791 *
2792 * @global array $exceptions_des_tables
2793 * @param string $idb Identifiant de la boucle
2794 * @param array $boucles AST du squelette
2795 * @param Critere $crit Paramètres du critère dans cette boucle
2796 */
2797 function critere_feuille_dist($idb, &$boucles, $crit) {
2798 $not = $crit->not;
2799 $crit->not = $not ? false : true;
2800 critere_noeud_dist($idb, $boucles, $crit);
2801 $crit->not = $not;
2802 }