[SPIP] +version 3.0.7
[ptitvelo/web/www.git] / www / ecrire / public / criteres.php
diff --git a/www/ecrire/public/criteres.php b/www/ecrire/public/criteres.php
new file mode 100644 (file)
index 0000000..562414a
--- /dev/null
@@ -0,0 +1,1919 @@
+<?php
+
+/***************************************************************************\
+ *  SPIP, Systeme de publication pour l'internet                           *
+ *                                                                         *
+ *  Copyright (c) 2001-2012                                                *
+ *  Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James  *
+ *                                                                         *
+ *  Ce programme est un logiciel libre distribue sous licence GNU/GPL.     *
+ *  Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne.   *
+\***************************************************************************/
+
+/**
+ * Définition des {criteres} d'une boucle
+ *
+ * @package SPIP\Compilateur\Criteres
+**/
+
+if (!defined('_ECRIRE_INC_VERSION')) return;
+
+/**
+ * Une Regexp repérant une chaine produite par le compilateur,
+ * souvent utilisée pour faire de la concaténation lors de la compilation
+ * plutôt qu'à l'exécution, i.e. pour remplacer 'x'.'y' par 'xy'
+**/
+define('_CODE_QUOTE', ",^(\n//[^\n]*\n)? *'(.*)' *$,");
+
+
+
+/**
+ * Compile le critère {racine}
+ *
+ * Ce critère sélectionne les éléments à la racine d'une hiérarchie,
+ * c'est à dire ayant id_parent=0
+ *
+ * @link http://www.spip.net/@racine
+ * 
+ * @param string $idb
+ *     Identifiant de la boucle
+ * @param array $boucles
+ *     AST du squelette
+ * @param array $crit
+ *     Paramètres du critère dans cette boucle
+ * @return
+ *     AST complété de la gestion du critère
+**/
+function critere_racine_dist($idb, &$boucles, $crit){
+       global $exceptions_des_tables;
+       $not = $crit->not;
+       $boucle = &$boucles[$idb];
+       $id_parent = isset($exceptions_des_tables[$boucle->id_table]['id_parent']) ?
+               $exceptions_des_tables[$boucle->id_table]['id_parent'] :
+               'id_parent';
+
+       $c = array("'='", "'$boucle->id_table."."$id_parent'", 0);
+       $boucle->where[] = ($crit->not ? array("'NOT'", $c) : $c);
+}
+
+
+/**
+ * Compile le critère {exclus}
+ *
+ * Exclut du résultat l’élément dans lequel on se trouve déjà
+ * 
+ * @link http://www.spip.net/@exclus
+ *
+ * @param string $idb
+ *     Identifiant de la boucle
+ * @param array $boucles
+ *     AST du squelette
+ * @param array $crit
+ *     Paramètres du critère dans cette boucle
+ * @return
+ *     AST complété de la gestion du critère
+**/
+function critere_exclus_dist($idb, &$boucles, $crit){
+       $not = $crit->not;
+       $boucle = &$boucles[$idb];
+       $id = $boucle->primary;
+
+       if ($not OR !$id)
+               return (array('zbug_critere_inconnu', array('critere' => $not.$crit->op)));
+       $arg = kwote(calculer_argument_precedent($idb, $id, $boucles));
+       $boucle->where[] = array("'!='", "'$boucle->id_table."."$id'", $arg);
+}
+
+
+/**
+ * Compile le critère {doublons} ou {unique}
+ *
+ * Ce critères enlève de la boucle les éléments déjà sauvegardés
+ * dans un précédent critère {doublon} sur une boucle de même table.
+ *
+ * Il est possible de spécifier un nom au doublon tel que {doublons sommaire}
+ * 
+ * @link http://www.spip.net/@doublons
+ * 
+ * @param string $idb
+ *             Identifiant de la boucle
+ * @param array $boucles
+ *             AST du squelette
+ * @param array $crit
+ *             Paramètres du critère dans cette boucle
+ * @return
+ *             AST complété de la gestion du critère
+**/
+function critere_doublons_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       $primary = $boucle->primary;
+
+       // la table nécessite une clé primaire, non composée
+       if (!$primary OR strpos($primary, ',')){
+               return (array('zbug_doublon_sur_table_sans_cle_primaire'));
+       }
+
+       $not = ($crit->not ? '' : 'NOT');
+
+       // le doublon s'applique sur un type de boucle (article)
+       $nom = "'" . $boucle->type_requete. "'";
+
+       // compléter le nom avec un nom précisé {doublons nom}
+       // on obtient $nom = "'article' . 'nom'"
+       if (isset($crit->param[0])) {
+               $nom .= "." . calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent);
+       }
+
+       // code qui déclarera l'index du stockage de nos doublons (pour éviter une notice PHP)
+       $init_comment = "\n\n\t// Initialise le(s) critère(s) doublons\n";
+       $init_code = "\tif (!isset(\$doublons[\$d = $nom])) { \$doublons[\$d] = ''; }\n";
+
+       // on crée un sql_in avec la clé primaire de la table
+       // et la collection des doublons déjà emmagasinés dans le tableau
+       // $doublons et son index, ici $nom
+
+       // debut du code "sql_in('articles.id_article', "
+       $debut_in = "sql_in('".$boucle->id_table.'.'.$primary."', ";
+       // lecture des données du doublon "$doublons[$doublon_index[] = "
+       // Attention : boucle->doublons désigne une variable qu'on affecte
+       $debut_doub = '$doublons[' . (!$not ? '' : ($boucle->doublons."[]= "));
+
+       // le debut complet du code des doublons
+       $debut_doub = $debut_in . $debut_doub;
+
+       // nom du doublon "('article' . 'nom')]"
+       $fin_doub = "($nom)]";
+
+       // si on trouve un autre critère doublon,
+       // on fusionne pour avoir un seul IN, et on s'en va !
+       foreach ($boucle->where as $k => $w) {
+               if (strpos($w[0], $debut_doub)===0) {
+                       // fusionner le sql_in (du where)
+                       $boucle->where[$k][0] = $debut_doub . $fin_doub.' . '.substr($w[0], strlen($debut_in));
+                       // fusionner l'initialisation (du hash) pour faire plus joli
+                       $x = strpos($boucle->hash, $init_comment);
+                       $len = strlen($init_comment);
+                       $boucle->hash =
+                               substr($boucle->hash, 0, $x + $len) . $init_code . substr($boucle->hash, $x + $len);
+                       return;
+               }
+       }
+
+       // mettre l'ensemble dans un tableau pour que ce ne soit pas vu comme une constante
+       $boucle->where[] = array($debut_doub . $fin_doub.", '".$not."')");
+
+       // déclarer le doublon s'il n'existe pas encore
+       $boucle->hash .= $init_comment . $init_code;
+
+
+       # la ligne suivante avait l'intention d'eviter une collecte deja faite
+       # mais elle fait planter une boucle a 2 critere doublons:
+       # {!doublons A}{doublons B}
+       # (de http://article.gmane.org/gmane.comp.web.spip.devel/31034)
+       #       if ($crit->not) $boucle->doublons = "";
+}
+
+
+/**
+ * Compile le critère {lang_select}
+ *
+ * Permet de restreindre ou non une boucle en affichant uniquement
+ * les éléments dans la langue en cours. Certaines boucles
+ * tel que articles et rubriques restreignent par défaut sur la langue
+ * en cours.
+ * 
+ * Sans définir de valeur au critère, celui-ci utilise 'oui' comme
+ * valeur par défaut.
+ * 
+ * @param string $idb
+ *     Identifiant de la boucle
+ * @param array $boucles
+ *     AST du squelette
+ * @param array $crit
+ *     Paramètres du critère dans cette boucle
+ * @return
+ *     AST complété de la gestion du critère
+**/
+function critere_lang_select_dist($idb, &$boucles, $crit){
+       if (!isset($crit->param[1][0]) OR !($param = $crit->param[1][0]->texte)) $param = 'oui';
+       if ($crit->not) $param = ($param=='oui') ? 'non' : 'oui';
+       $boucle = &$boucles[$idb];
+       $boucle->lang_select = $param;
+}
+
+// {debut_xxx}
+// http://www.spip.net/@debut_
+// http://doc.spip.org/@critere_debut_dist
+function critere_debut_dist($idb, &$boucles, $crit){
+       list($un, $deux) = $crit->param;
+       $un = $un[0]->texte;
+       $deux = $deux[0]->texte;
+       if ($deux){
+               $boucles[$idb]->limit = 'intval($Pile[0]["debut'.
+                                       $un.
+                                       '"]) . ",'.
+                                       $deux.
+                                       '"';
+       } else calculer_critere_DEFAUT_dist($idb, $boucles, $crit);
+}
+
+// {pagination}
+// {pagination 20}
+// {pagination #ENV{pages,5}} etc
+// {pagination 20 #ENV{truc,chose}} pour utiliser la variable debut_#ENV{truc,chose}
+// http://www.spip.net/@pagination
+// http://doc.spip.org/@critere_pagination_dist
+function critere_pagination_dist($idb, &$boucles, $crit){
+
+       $boucle = &$boucles[$idb];
+       // definition de la taille de la page
+       $pas = !isset($crit->param[0][0]) ? "''"
+               : calculer_liste(array($crit->param[0][0]), array(), $boucles, $boucle->id_parent);
+
+       if (!preg_match(_CODE_QUOTE, $pas, $r)){
+               $pas = "((\$a = intval($pas)) ? \$a : 10)";
+       } else {
+               $r = intval($r[2]);
+               $pas = strval($r ? $r : 10);
+       }
+       $type = !isset($crit->param[0][1]) ? "'$idb'"
+               : calculer_liste(array($crit->param[0][1]), array(), $boucles, $boucle->id_parent);
+       $debut = ($type[0]!=="'") ? "'debut'.$type"
+               : ("'debut".substr($type, 1));
+
+       $boucle->modificateur['debut_nom'] = $type;
+       $partie =
+               // tester si le numero de page demande est de la forme '@yyy'
+               'isset($Pile[0]['.$debut.']) ? $Pile[0]['.$debut.'] : _request('.$debut.");\n"
+               ."\tif(substr(\$debut_boucle,0,1)=='@'){\n"
+               ."\t\t".'$debut_boucle = $Pile[0]['.$debut.'] = quete_debut_pagination(\''.$boucle->primary.'\',$Pile[0][\'@'.$boucle->primary.'\'] = substr($debut_boucle,1),'.$pas.',$iter);'."\n"
+               ."\t\t".'$iter->seek(0);'."\n"
+               ."\t}\n"
+               ."\t".'$debut_boucle = intval($debut_boucle)';
+
+       $boucle->hash .= '
+       $command[\'pagination\'] = array((isset($Pile[0]['.$debut.']) ? $Pile[0]['.$debut.'] : null), ' . $pas . ');';
+
+       $boucle->total_parties = $pas;
+       calculer_parties($boucles, $idb, $partie, 'p+');
+       // ajouter la cle primaire dans le select pour pouvoir gerer la pagination referencee par @id
+       // sauf si pas de primaire, ou si primaire composee
+       // dans ce cas, on ne sait pas gerer une pagination indirecte
+       $t = $boucle->id_table.'.'.$boucle->primary;
+       if ($boucle->primary
+           AND !preg_match('/[,\s]/', $boucle->primary)
+               AND !in_array($t, $boucle->select)
+       )
+               $boucle->select[] = $t;
+}
+
+
+// {recherche} ou {recherche susan}
+// http://www.spip.net/@recherche
+// http://doc.spip.org/@critere_recherche_dist
+function critere_recherche_dist($idb, &$boucles, $crit){
+
+       $boucle = &$boucles[$idb];
+
+       if (isset($crit->param[0]))
+               $quoi = calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent);
+       else
+               $quoi = '(isset($Pile[0]["recherche"])?$Pile[0]["recherche"]:(isset($GLOBALS["recherche"])?$GLOBALS["recherche"]:""))';
+
+       $_modificateur = var_export($boucle->modificateur, true);
+       $boucle->hash .= '
+       // RECHERCHE'
+                        .($crit->cond ? '
+       if (!strlen('.$quoi.')){
+               list($rech_select, $rech_where) = array("0 as points","");
+       } else' : '').'
+       {
+               $prepare_recherche = charger_fonction(\'prepare_recherche\', \'inc\');
+               list($rech_select, $rech_where) = $prepare_recherche('.$quoi.', "'.$boucle->id_table.'", "'.$crit->cond.'","'.$boucle->sql_serveur.'",'.$_modificateur.',"'.$boucle->primary.'");
+       }
+       ';
+
+       $t = $boucle->id_table.'.'.$boucle->primary;
+       if (!in_array($t, $boucles[$idb]->select))
+               $boucle->select[] = $t; # pour postgres, neuneu ici
+       // jointure uniquement sur le serveur principal
+       // (on ne peut joindre une table d'un serveur distant avec la table des resultats du serveur principal)
+       if (!$boucle->sql_serveur){
+               $boucle->join['resultats'] = array("'".$boucle->id_table."'", "'id'", "'".$boucle->primary."'");
+               $boucle->from['resultats'] = 'spip_resultats';
+       }
+       $boucle->select[] = '$rech_select';
+       //$boucle->where[]= "\$rech_where?'resultats.id=".$boucle->id_table.".".$boucle->primary."':''";
+
+       // et la recherche trouve
+       $boucle->where[] = '$rech_where?$rech_where:\'\'';
+}
+
+// {traduction}
+// http://www.spip.net/@traduction
+//   (id_trad>0 AND id_trad=id_trad(precedent))
+//    OR id_article=id_article(precedent)
+// http://doc.spip.org/@critere_traduction_dist
+function critere_traduction_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       $prim = $boucle->primary;
+       $table = $boucle->id_table;
+       $arg = kwote(calculer_argument_precedent($idb, 'id_trad', $boucles));
+       $dprim = kwote(calculer_argument_precedent($idb, $prim, $boucles));
+       $boucle->where[] =
+               array("'OR'",
+                     array("'AND'",
+                           array("'='", "'$table.id_trad'", 0),
+                           array("'='", "'$table.$prim'", $dprim)
+                     ),
+                     array("'AND'",
+                           array("'>'", "'$table.id_trad'", 0),
+                           array("'='", "'$table.id_trad'", $arg)
+                     )
+               );
+}
+
+// {origine_traduction}
+//   (id_trad>0 AND id_article=id_trad) OR (id_trad=0)
+// http://www.spip.net/@origine_traduction
+// http://doc.spip.org/@critere_origine_traduction_dist
+function critere_origine_traduction_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       $prim = $boucle->primary;
+       $table = $boucle->id_table;
+
+       $c =
+               array("'OR'",
+                     array("'='", "'$table."."id_trad'", "'$table.$prim'"),
+                     array("'='", "'$table.id_trad'", "'0'")
+               );
+       $boucle->where[] = ($crit->not ? array("'NOT'", $c) : $c);
+}
+
+// {meme_parent}
+// http://www.spip.net/@meme_parent
+// http://doc.spip.org/@critere_meme_parent_dist
+function critere_meme_parent_dist($idb, &$boucles, $crit){
+       global $exceptions_des_tables;
+       $boucle = &$boucles[$idb];
+       $arg = kwote(calculer_argument_precedent($idb, 'id_parent', $boucles));
+       $id_parent = isset($exceptions_des_tables[$boucle->id_table]['id_parent']) ?
+               $exceptions_des_tables[$boucle->id_table]['id_parent'] :
+               'id_parent';
+       $mparent = $boucle->id_table.'.'.$id_parent;
+
+       if ($boucle->type_requete=='rubriques' OR isset($exceptions_des_tables[$boucle->id_table]['id_parent'])){
+               $boucle->where[] = array("'='", "'$mparent'", $arg);
+
+       }
+               // le cas FORUMS est gere dans le plugin forum, dans la fonction critere_FORUMS_meme_parent_dist()
+       else {
+               return (array('zbug_critere_inconnu', array('critere' => $crit->op.' '.$boucle->type_requete)));
+       }
+}
+
+
+/**
+ * Sélectionne dans une boucle les éléments appartenant à une branche d'une rubrique
+ * 
+ * Calcule une branche d'une rubrique et conditionne la boucle avec.
+ * Cherche l'identifiant de la rubrique dans les boucles parentes ou par jointure
+ * et calcule la liste des identifiants de rubrique de toute la branche
+ *
+ * @link http://www.spip.net/@branche
+ * 
+ * @param string $idb
+ *             Identifiant de la boucle
+ * @param array $boucles
+ *             AST du squelette
+ * @param array $crit
+ *             Paramètres du critère dans cette boucle
+ * @return
+ *             AST complété de la condition where au niveau de la boucle,
+ *             restreignant celle ci aux rubriques de la branche
+**/
+function critere_branche_dist($idb, &$boucles, $crit){
+
+       $not = $crit->not;
+       $boucle = &$boucles[$idb];
+       $arg = calculer_argument_precedent($idb, 'id_rubrique', $boucles);
+
+       //Trouver une jointure
+       $champ = "id_rubrique";
+       $desc = $boucle->show;
+       //Seulement si necessaire
+       if (!array_key_exists($champ, $desc['field'])){
+               $cle = trouver_jointure_champ($champ, $boucle);
+               $trouver_table = charger_fonction("trouver_table", "base");
+               $desc = $trouver_table($boucle->from[$cle]);
+               if (count(trouver_champs_decomposes($champ, $desc))>1){
+                       $decompose = decompose_champ_id_objet($champ);
+                       $champ = array_shift($decompose);
+                       $boucle->where[] = array("'='", _q($cle.".".reset($decompose)), '"'.sql_quote(end($decompose)).'"');
+               }
+       }
+       else $cle = $boucle->id_table;
+
+       $c = "sql_in('$cle".".$champ', calcul_branche_in($arg)"
+            .($not ? ", 'NOT'" : '').")";
+       $boucle->where[] = !$crit->cond ? $c :
+               ("($arg ? $c : ".($not ? "'0=1'" : "'1=1'").')');
+}
+
+// {logo} liste les objets qui ont un logo
+// http://doc.spip.org/@critere_logo_dist
+function critere_logo_dist($idb, &$boucles, $crit){
+
+       $not = $crit->not;
+       $boucle = &$boucles[$idb];
+
+       $c = "sql_in('".
+            $boucle->id_table.'.'.$boucle->primary
+            ."', lister_objets_avec_logos('".$boucle->primary."'), '')";
+
+       if ($crit->cond) $c = "($arg ? $c : 1)";
+
+       if ($not)
+               $boucle->where[] = array("'NOT'", $c);
+       else
+               $boucle->where[] = $c;
+}
+
+// c'est la commande SQL "GROUP BY"
+// par exemple <boucle(articles){fusion lang}>
+// http://doc.spip.org/@critere_fusion_dist
+function critere_fusion_dist($idb, &$boucles, $crit){
+       if ($t = isset($crit->param[0])){
+               $t = $crit->param[0];
+               if ($t[0]->type=='texte'){
+                       $t = $t[0]->texte;
+                       if (preg_match("/^(.*)\.(.*)$/", $t, $r)){
+                               $t = table_objet_sql($r[1]);
+                               $t = array_search($t, $boucles[$idb]->from);
+                               if ($t) $t .= '.'.$r[2];
+                       }
+               } else {
+                       $t = '".'
+                            .calculer_critere_arg_dynamique($idb, $boucles, $t)
+                            .'."';
+               }
+       }
+       if ($t){
+               $boucles[$idb]->group[] = $t;
+               if (!in_array($t, $boucles[$idb]->select))
+                       $boucles[$idb]->select[] = $t;
+       } else
+               return (array('zbug_critere_inconnu', array('critere' => $crit->op.' ?')));
+}
+
+// c'est la commande SQL "COLLATE"
+// qui peut etre appliquee sur les order by, group by, where like ...
+// http://doc.spip.org/@critere_collecte_dist
+function critere_collecte_dist($idb, &$boucles, $crit){
+       if (isset($crit->param[0])){
+               $_coll = calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent);
+               $boucle = $boucles[$idb];
+               $boucle->modificateur['collate'] = "($_coll ?' COLLATE '.$_coll:'')";
+               $n = count($boucle->order);
+               if ($n && (strpos($boucle->order[$n-1], 'COLLATE')===false))
+                       $boucle->order[$n-1] .= " . ".$boucle->modificateur['collate'];
+       } else
+               return (array('zbug_critere_inconnu', array('critere' => $crit->op." ".count($boucles[$idb]->order))));
+}
+
+// http://doc.spip.org/@calculer_critere_arg_dynamique
+function calculer_critere_arg_dynamique($idb, &$boucles, $crit, $suffix = ''){
+       $boucle = $boucles[$idb];
+       $alt = "('".$boucle->id_table.'.\' . $x'.$suffix.')';
+       $var = '$champs_'.$idb;
+       $desc = (strpos($boucle->in, "static $var =")!==false);
+       if (!$desc){
+               $desc = $boucle->show['field'];
+               $desc = implode(',', array_map('_q', array_keys($desc)));
+               $boucles[$idb]->in .= "\n\tstatic $var = array(".$desc.");";
+       }
+       if ($desc) $alt = "(in_array(\$x, $var)  ? $alt :(\$x$suffix))";
+       $arg = calculer_liste($crit, array(), $boucles, $boucle->id_parent);
+       return "((\$x = preg_replace(\"/\\W/\",'', $arg)) ? $alt : '')";
+}
+
+// Tri : {par xxxx}
+// http://www.spip.net/@par
+// http://doc.spip.org/@critere_par_dist
+function critere_par_dist($idb, &$boucles, $crit){
+       return critere_parinverse($idb, $boucles, $crit);
+}
+
+// http://doc.spip.org/@critere_parinverse
+function critere_parinverse($idb, &$boucles, $crit, $sens = ''){
+       global $exceptions_des_jointures;
+       $boucle = &$boucles[$idb];
+       if ($crit->not) $sens = $sens ? "" : " . ' DESC'";
+       $collecte = (isset($boucle->modificateur['collecte'])) ? " . ".$boucle->modificateur['collecte'] : "";
+
+       foreach ($crit->param as $tri){
+
+               $order = $fct = ""; // en cas de fonction SQL
+               // tris specifies dynamiquement
+               if ($tri[0]->type!='texte'){
+                       // calculer le order dynamique qui verifie les champs
+                       $order = calculer_critere_arg_dynamique($idb, $boucles, $tri, $sens);
+                       // et si ce n'est fait, ajouter un champ 'hasard' 
+                       // pour supporter 'hasard' comme tri dynamique
+                       $par = "rand()";
+                       $parha = $par." AS hasard";
+                       if (!in_array($parha, $boucle->select))
+                               $boucle->select[] = $parha;
+               } else {
+                       $par = array_shift($tri);
+                       $par = $par->texte;
+                       // par multi champ
+                       if (preg_match(",^multi[\s]*(.*)$,", $par, $m)){
+                               $champ = trim($m[1]);
+                               // par multi L1.champ
+                               if (strpos($champ, '.')) {
+                                       $cle = '';
+                               // par multi champ (champ sur une autre table)
+                               } elseif (!array_key_exists($champ, $boucle->show['field'])){
+                                       $cle = trouver_jointure_champ($champ, $boucle);
+                               // par multi champ (champ dans la table en cours)
+                               } else {
+                                       $cle = $boucle->id_table;
+                               }
+                               if ($cle) { $cle .= '.'; }
+                               $texte = $cle.$champ;
+                               $boucle->select[] = "\".sql_multi('".$texte."', \$GLOBALS['spip_lang']).\"";
+                               $order = "'multi'";
+                               // par num champ(, suite)
+                       } else if (preg_match(",^num (.*)$,m", $par, $m)) {
+                               $champ = trim($m[1]);
+                               // par num L1.champ
+                               if (strpos($champ, '.')) {
+                                       $cle = '';
+                               // par num champ (champ sur une autre table)
+                               } elseif (!array_key_exists($champ, $boucle->show['field'])){
+                                       $cle = trouver_jointure_champ($champ, $boucle);
+                               // par num champ (champ dans la table en cours)
+                               } else {
+                                       $cle = $boucle->id_table;
+                               }
+                               if ($cle) { $cle .= '.'; }
+                               $texte = '0+'. $cle . $champ;
+                               $suite = calculer_liste($tri, array(), $boucles, $boucle->id_parent);
+                               if ($suite!=="''")
+                                       $texte = "\" . ((\$x = $suite) ? ('$texte' . \$x) : '0')"." . \"";
+                               $as = 'num'.($boucle->order ? count($boucle->order) : "");
+                               $boucle->select[] = $texte." AS $as";
+                               $order = "'$as'";
+                       } else {
+                               if (!preg_match(",^".CHAMP_SQL_PLUS_FONC.'$,is', $par, $match)){
+                                       return (array('zbug_critere_inconnu', array('critere' => $crit->op." $par")));
+                               } else {
+                                       if (count($match)>2){
+                                               $par = substr($match[2], 1, -1);
+                                               $fct = $match[1];
+                                       }
+                                       // par hasard
+                                       if ($par=='hasard'){
+                                               $par = "rand()";
+                                               $boucle->select[] = $par." AS alea";
+                                               $order = "'alea'";
+                                       }
+                                               // par titre_mot ou type_mot voire d'autres
+                                       else if (isset($exceptions_des_jointures[$par])){
+                                               list($table, $champ) = $exceptions_des_jointures[$par];
+                                               $order = critere_par_joint($table, $champ, $boucle, $idb);
+                                               if (!$order)
+                                                       return (array('zbug_critere_inconnu', array('critere' => $crit->op." $par")));
+                                       }
+                                       else if ($par=='date'
+                                                AND $desc = $boucle->show
+                                                    AND $desc['date']
+                                       ){
+                                               $m = $desc['date'];
+                                               $order = "'".$boucle->id_table.".".$m."'";
+                                       }
+                                               // par champ. Verifier qu'ils sont presents.
+                                       elseif (preg_match("/^([^,]*)\.(.*)$/", $par, $r)) {
+                                               // cas du tri sur champ de jointure explicite
+                                               $t = array_search($r[1], $boucle->from);
+                                               if (!$t){
+                                                       $t = trouver_jointure_champ($r[2], $boucle, array($r[1]));
+                                               }
+                                               if (!$t){
+                                                       return (array('zbug_critere_inconnu', array('critere' => $crit->op." $par")));
+                                               } else   $order = "'".$t.'.'.$r[2]."'";
+                                       } else {
+                                               $desc = $boucle->show;
+                                               if ($desc['field'][$par])
+                                                       $par = $boucle->id_table.".".$par;
+                                               // sinon tant pis, ca doit etre un champ synthetise (cf points)
+                                               $order = "'$par'";
+                                       }
+                               }
+                       }
+               }
+               if (preg_match('/^\'([^"]*)\'$/', $order, $m)){
+                       $t = $m[1];
+                       if (strpos($t, '.') AND !in_array($t, $boucle->select)){
+                               $boucle->select[] = $t;
+                       }
+               } else $sens = '';
+
+               if ($fct){
+                       if (preg_match("/^\s*'(.*)'\s*$/", $order, $r))
+                               $order = "'$fct(".$r[1].")'";
+                       else $order = "'$fct(' . $order . ')'";
+               }
+               $t = $order.$collecte.$sens;
+               if (preg_match("/^(.*)'\s*\.\s*'([^']*')$/", $t, $r))
+                       $t = $r[1].$r[2];
+               $boucle->order[] = $t;
+       }
+}
+
+// http://doc.spip.org/@critere_par_joint
+function critere_par_joint($table, $champ, &$boucle, $idb){
+       $t = array_search($table, $boucle->from);
+       if (!$t) $t = trouver_jointure_champ($champ, $boucle);
+       return !$t ? '' : ("'".$t.'.'.$champ."'");
+}
+
+// {inverse}
+// http://www.spip.net/@inverse
+
+// http://doc.spip.org/@critere_inverse_dist
+function critere_inverse_dist($idb, &$boucles, $crit){
+
+       $boucle = &$boucles[$idb];
+       // Classement par ordre inverse
+       if ($crit->not)
+               critere_parinverse($idb, $boucles, $crit);
+       else
+       {
+               $order = "' DESC'";
+               // Classement par ordre inverse fonction eventuelle de #ENV{...}
+               if (isset($crit->param[0])){
+                       $critere = calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent);
+                       $order = "(($critere)?' DESC':'')";
+               }
+
+               $n = count($boucle->order);
+               if (!$n){
+                       if (isset($boucle->default_order[0]))
+                               $boucle->default_order[0] .= ' . " DESC"';
+                       else
+                               $boucle->default_order[] = ' DESC';
+               } else {
+                       $t = $boucle->order[$n-1]." . $order";
+                       if (preg_match("/^(.*)'\s*\.\s*'([^']*')$/", $t, $r))
+                               $t = $r[1].$r[2];
+                       $boucle->order[$n-1] = $t;
+               }
+       }
+}
+
+// http://doc.spip.org/@critere_agenda_dist
+function critere_agenda_dist($idb, &$boucles, $crit){
+       $params = $crit->param;
+
+       if (count($params)<1)
+               return (array('zbug_critere_inconnu', array('critere' => $crit->op." ?")));
+
+       $parent = $boucles[$idb]->id_parent;
+
+       // les valeurs $date et $type doivent etre connus a la compilation
+       // autrement dit ne pas etre des champs
+
+       $date = array_shift($params);
+       $date = $date[0]->texte;
+
+       $type = array_shift($params);
+       $type = $type[0]->texte;
+
+       $annee = $params ? array_shift($params) : "";
+       $annee = "\n".'sprintf("%04d", ($x = '.
+                calculer_liste($annee, array(), $boucles, $parent).
+                ') ? $x : date("Y"))';
+
+       $mois = $params ? array_shift($params) : "";
+       $mois = "\n".'sprintf("%02d", ($x = '.
+               calculer_liste($mois, array(), $boucles, $parent).
+               ') ? $x : date("m"))';
+
+       $jour = $params ? array_shift($params) : "";
+       $jour = "\n".'sprintf("%02d", ($x = '.
+               calculer_liste($jour, array(), $boucles, $parent).
+               ') ? $x : date("d"))';
+
+       $annee2 = $params ? array_shift($params) : "";
+       $annee2 = "\n".'sprintf("%04d", ($x = '.
+                 calculer_liste($annee2, array(), $boucles, $parent).
+                 ') ? $x : date("Y"))';
+
+       $mois2 = $params ? array_shift($params) : "";
+       $mois2 = "\n".'sprintf("%02d", ($x = '.
+                calculer_liste($mois2, array(), $boucles, $parent).
+                ') ? $x : date("m"))';
+
+       $jour2 = $params ? array_shift($params) : "";
+       $jour2 = "\n".'sprintf("%02d", ($x = '.
+                calculer_liste($jour2, array(), $boucles, $parent).
+                ') ? $x : date("d"))';
+
+       $boucle = &$boucles[$idb];
+       $date = $boucle->id_table.".$date";
+
+       $quote_end = ",'".$boucle->sql_serveur."','text'";
+       if ($type=='jour')
+               $boucle->where[] = array("'='", "'DATE_FORMAT($date, \'%Y%m%d\')'",
+                                        ("sql_quote($annee . $mois . $jour$quote_end)"));
+       elseif ($type=='mois')
+               $boucle->where[] = array("'='", "'DATE_FORMAT($date, \'%Y%m\')'",
+                                        ("sql_quote($annee . $mois$quote_end)"));
+       elseif ($type=='semaine')
+               $boucle->where[] = array("'AND'",
+                                        array("'>='",
+                                              "'DATE_FORMAT($date, \'%Y%m%d\')'",
+                                              ("date_debut_semaine($annee, $mois, $jour)")),
+                                        array("'<='",
+                                              "'DATE_FORMAT($date, \'%Y%m%d\')'",
+                                              ("date_fin_semaine($annee, $mois, $jour)")));
+       elseif (count($crit->param)>2)
+               $boucle->where[] = array("'AND'",
+                                        array("'>='",
+                                              "'DATE_FORMAT($date, \'%Y%m%d\')'",
+                                              ("sql_quote($annee . $mois . $jour$quote_end)")),
+                                        array("'<='", "'DATE_FORMAT($date, \'%Y%m%d\')'", ("sql_quote($annee2 . $mois2 . $jour2$quote_end)")));
+       // sinon on prend tout
+}
+
+// http://doc.spip.org/@calculer_critere_parties
+function calculer_critere_parties($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       $a1 = $crit->param[0];
+       $a2 = $crit->param[1];
+       $op = $crit->op;
+
+       list($a11, $a12) = calculer_critere_parties_aux($idb, $boucles, $a1);
+       list($a21, $a22) = calculer_critere_parties_aux($idb, $boucles, $a2);
+
+       if (($op==',') && (is_numeric($a11) && (is_numeric($a21)))){
+               $boucle->limit = $a11.','.$a21;
+       }
+       else {
+               $boucle->total_parties = ($a21!='n') ? $a21 : $a22;
+               $partie = ($a11!='n') ? $a11 : $a12;
+               $mode = (($op=='/') ? '/' :
+                       (($a11=='n') ? '-' : '+').(($a21=='n') ? '-' : '+'));
+               // cas simple {0,#ENV{truc}} compilons le en LIMIT :
+               if ($a11!=='n' AND $a21!=='n' AND $mode=="++" AND $op==','){
+                       $boucle->limit =
+                               (is_numeric($a11)?"'$a11'":$a11)
+                               .".','."
+                               .(is_numeric($a21)?"'$a21'":$a21);
+               }
+               else
+                       calculer_parties($boucles, $idb, $partie, $mode);
+       }
+}
+
+//
+// Code specifique aux criteres {pagination}, {1,n} {n/m} etc
+//
+
+function calculer_parties(&$boucles, $id_boucle, $debut, $mode){
+       $total_parties = $boucles[$id_boucle]->total_parties;
+
+       preg_match(",([+-/p])([+-/])?,", $mode, $regs);
+       list(, $op1, $op2) = array_pad($regs, 3, null);
+       $nombre_boucle = "\$Numrows['$id_boucle']['total']";
+       // {1/3}
+       if ($op1=='/'){
+               $pmoins1 = is_numeric($debut) ? ($debut-1) : "($debut-1)";
+               $totpos = is_numeric($total_parties) ? ($total_parties) :
+                       "($total_parties ? $total_parties : 1)";
+               $fin = "ceil(($nombre_boucle * $debut )/$totpos) - 1";
+               $debut = !$pmoins1 ? 0 : "ceil(($nombre_boucle * $pmoins1)/$totpos);";
+       } else {
+               // cas {n-1,x}
+               if ($op1=='-') $debut = "$nombre_boucle - $debut;";
+
+               // cas {x,n-1}
+               if ($op2=='-'){
+                       $fin = '$debut_boucle + '.$nombre_boucle.' - '
+                              .(is_numeric($total_parties) ? ($total_parties+1) :
+                                       ($total_parties.' - 1'));
+               } else {
+                       // {x,1} ou {pagination}
+                       $fin = '$debut_boucle'
+                              .(is_numeric($total_parties) ?
+                                       (($total_parties==1) ? "" : (' + '.($total_parties-1))) :
+                                       ('+'.$total_parties.' - 1'));
+               }
+
+               // {pagination}, gerer le debut_xx=-1 pour tout voir
+               if ($op1=='p'){
+                       $debut .= ";\n  \$debut_boucle = ((\$tout=(\$debut_boucle == -1))?0:(\$debut_boucle))";
+                       $debut .= ";\n  \$debut_boucle = max(0,min(\$debut_boucle,floor(($nombre_boucle-1)/($total_parties))*($total_parties)))";
+                       $fin = "(\$tout ? $nombre_boucle : $fin)";
+               }
+       }
+
+       // Notes :
+       // $debut_boucle et $fin_boucle sont les indices SQL du premier
+       // et du dernier demandes dans la boucle : 0 pour le premier,
+       // n-1 pour le dernier ; donc total_boucle = 1 + debut - fin
+       // Utiliser min pour rabattre $fin_boucle sur total_boucle.
+
+       $boucles[$id_boucle]->mode_partie = "\n\t"
+                                           .'$debut_boucle = '.$debut.";\n     "
+                                           .'$fin_boucle = min('.$fin.", \$Numrows['$id_boucle']['total'] - 1);\n      "
+                                           .'$Numrows[\''.$id_boucle."']['grand_total'] = \$Numrows['$id_boucle']['total'];\n  "
+                                           .'$Numrows[\''.$id_boucle.'\']["total"] = max(0,$fin_boucle - $debut_boucle + 1);'
+                                           ."\n\tif (\$debut_boucle>0 AND \$debut_boucle < \$Numrows['$id_boucle']['grand_total'] AND \$iter->seek(\$debut_boucle,'continue'))\n\t\t\$Numrows['$id_boucle']['compteur_boucle'] = \$debut_boucle;\n\t";
+
+       $boucles[$id_boucle]->partie = "
+               if (\$Numrows['$id_boucle']['compteur_boucle'] <= \$debut_boucle) continue;
+               if (\$Numrows['$id_boucle']['compteur_boucle']-1 > \$fin_boucle) break;";
+}
+
+// http://doc.spip.org/@calculer_critere_parties_aux
+function calculer_critere_parties_aux($idb, &$boucles, $param){
+       if ($param[0]->type!='texte'){
+               $a1 = calculer_liste(array($param[0]), array('id_mere' => $idb), $boucles, $boucles[$idb]->id_parent);
+               preg_match(',^ *(-([0-9]+))? *$,', $param[1]->texte, $m);
+               return array("intval($a1)", ($m[2] ? $m[2] : 0));
+       } else {
+               preg_match(',^ *(([0-9]+)|n) *(- *([0-9]+)? *)?$,', $param[0]->texte, $m);
+               $a1 = $m[1];
+               if (!@$m[3])
+                       return array($a1, 0);
+               elseif ($m[4])
+                       return array($a1, $m[4]);
+               else return array($a1,
+                                 calculer_liste(array($param[1]), array(), $boucles[$idb]->id_parent, $boucles));
+       }
+}
+
+
+/**
+ * Compile les critères d'une boucle
+ * 
+ * Cette fonction d'aiguillage cherche des fonctions spécifiques déclarées
+ * pour chaque critère demandé, dans l'ordre ci-dessous :
+ * 
+ * - critere_{serveur}_{table}_{critere}, sinon avec _dist
+ * - critere_{serveur}_{critere}, sinon avec _dist
+ * - critere_{table}_{critere}, sinon avec _dist
+ * - critere_{critere}, sinon avec _dist
+ * - critere_defaut, sinon avec _dist
+ *
+ * Émet une erreur de squelette si un critère retourne une erreur.
+ * 
+ * @param string $idb
+ *     Identifiant de la boucle
+ * @param array $boucles
+ *     AST du squelette
+ * @return string|array
+ *     string : Chaine vide sans erreur
+ *     array : Erreur sur un des critères
+**/
+function calculer_criteres($idb, &$boucles){
+       $msg = '';
+       $boucle = $boucles[$idb];
+       $table = strtoupper($boucle->type_requete);
+       $serveur = strtolower($boucle->sql_serveur);
+
+       $defaut = charger_fonction('DEFAUT', 'calculer_critere');
+       // s'il y avait une erreur de syntaxe, propager cette info
+       if (!is_array($boucle->criteres)) return array();
+
+       foreach ($boucle->criteres as $crit){
+               $critere = $crit->op;
+               // critere personnalise ?
+               if (
+                       (!$serveur OR
+                        ((!function_exists($f = "critere_".$serveur."_".$table."_".$critere))
+                         AND (!function_exists($f = $f."_dist"))
+                             AND (!function_exists($f = "critere_".$serveur."_".$critere))
+                                 AND (!function_exists($f = $f."_dist"))
+                        )
+                       )
+                       AND (!function_exists($f = "critere_".$table."_".$critere))
+                           AND (!function_exists($f = $f."_dist"))
+                               AND (!function_exists($f = "critere_".$critere))
+                                   AND (!function_exists($f = $f."_dist"))
+               ){
+                       // fonction critere standard 
+                       $f = $defaut;
+               }
+               // compile le critere
+               $res = $f($idb, $boucles, $crit);
+
+               // Gestion centralisee des erreurs pour pouvoir propager
+               if (is_array($res)){
+                       $msg = $res;
+                       erreur_squelette($msg, $boucle);
+               }
+       }
+       return $msg;
+}
+
+/**
+ * Madeleine de Proust, revision MIT-1958 sqq, revision CERN-1989
+ * hum, c'est kwoi cette fonxion ? on va dire qu'elle desemberlificote les guillemets...
+ *
+ * http://doc.spip.org/@kwote
+ *
+ * @param string $lisp
+ * @param string $serveur
+ * @param string $type
+ * @return string
+ */
+function kwote($lisp, $serveur='', $type=''){
+       if (preg_match(_CODE_QUOTE, $lisp, $r))
+               return $r[1]."\"".sql_quote(str_replace(array("\\'", "\\\\"), array("'", "\\"), $r[2]),$serveur,$type)."\"";
+       else
+               return "sql_quote($lisp)";
+}
+
+// Si on a une liste de valeurs dans #ENV{x}, utiliser la double etoile
+// pour faire par exemple {id_article IN #ENV**{liste_articles}}
+// http://doc.spip.org/@critere_IN_dist
+function critere_IN_dist($idb, &$boucles, $crit){
+       $r = calculer_critere_infixe($idb, $boucles, $crit);
+       if (!$r){
+               return (array('zbug_critere_inconnu', array('critere' => $crit->op." ?")));
+       }
+       list($arg, $op, $val, $col, $where_complement) = $r;
+
+       $in = critere_IN_cas($idb, $boucles, $crit->not ? 'NOT' : ($crit->exclus ? 'exclus' : ''), $arg, $op, $val, $col);
+
+       //      inserer la condition; exemple: {id_mot ?IN (66, 62, 64)}
+       $where = $in;
+       if ($crit->cond){
+               $pred = calculer_argument_precedent($idb, $col, $boucles);
+               $where = array("'?'", $pred, $where, "''");
+               if ($where_complement) // condition annexe du type "AND (objet='article')"
+                       $where_complement = array("'?'", $pred, $where_complement, "''");
+       }
+       if ($crit->exclus)
+               if (!preg_match(",^L[0-9]+[.],", $arg))
+                       $where = array("'NOT'", $where);
+               else
+                       // un not sur un critere de jointure se traduit comme un NOT IN avec une sous requete
+                       // c'est une sous requete identique a la requete principale sous la forme (SELF,$select,$where) avec $select et $where qui surchargent
+                       $where = array("'NOT'", array("'IN'", "'".$boucles[$idb]->id_table.".".$boucles[$idb]->primary."'", array("'SELF'", "'".$boucles[$idb]->id_table.".".$boucles[$idb]->primary."'", $where)));
+
+       $boucles[$idb]->where[] = $where;
+       if ($where_complement) // condition annexe du type "AND (objet='article')"
+               $boucles[$idb]->where[] = $where_complement;
+}
+
+// http://doc.spip.org/@critere_IN_cas
+function critere_IN_cas($idb, &$boucles, $crit2, $arg, $op, $val, $col){
+       static $num = array();
+       $descr = $boucles[$idb]->descr;
+       $cpt = &$num[$descr['nom']][$descr['gram']][$idb];
+
+       $var = '$in'.$cpt++;
+       $x = "\n\t$var = array();";
+       foreach ($val as $k => $v){
+               if (preg_match(",^(\n//.*\n)?'(.*)'$,", $v, $r)){
+                       // optimiser le traitement des constantes
+                       if (is_numeric($r[2]))
+                               $x .= "\n\t$var"."[]= $r[2];";
+                       else
+                               $x .= "\n\t$var"."[]= ".sql_quote($r[2]).";";
+               } else {
+                       // Pour permettre de passer des tableaux de valeurs
+                       // on repere l'utilisation brute de #ENV**{X},
+                       // c'est-a-dire sa  traduction en ($PILE[0][X]).
+                       // et on deballe mais en rajoutant l'anti XSS
+                       $x .= "\n\tif (!(is_array(\$a = ($v))))\n\t\t$var"."[]= \$a;\n\telse $var = array_merge($var, \$a);";
+               }
+       }
+
+       $boucles[$idb]->in .= $x;
+
+       // inserer le tri par defaut selon les ordres du IN ... 
+       // avec une ecriture de type FIELD qui degrade les performances (du meme ordre qu'un rexgexp)
+       // et que l'on limite donc strictement aux cas necessaires :
+       // si ce n'est pas un !IN, et si il n'y a pas d'autre order dans la boucle
+       if (!$crit2){
+               $boucles[$idb]->default_order[] = "((!sql_quote($var) OR sql_quote($var)===\"''\") ? 0 : ('FIELD($arg,' . sql_quote($var) . ')'))";
+       }
+
+       return "sql_in('$arg',sql_quote($var)".($crit2=='NOT' ? ",'NOT'" : "").")";
+}
+
+/**
+ * {where}
+ * tout simplement, pour faire le pont entre php et squelettes
+ *
+ * @param <type> $idb
+ * @param <type> $boucles
+ * @param <type> $crit
+ */
+function critere_where_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       if (isset($crit->param[0]))
+               $_where = calculer_liste($crit->param[0], array(), $boucles, $boucle->id_parent);
+       else
+               $_where = '@$Pile[0]["where"]';
+
+       if ($crit->cond)
+               $_where = "(($_where) ? ($_where) : '')";
+
+       if ($crit->not)
+               $_where = "array('NOT',$_where)";
+
+       $boucle->where[] = $_where;
+}
+
+
+/**
+ * Un critere pour gerer un champ de tri qui peut etre modifie dynamiquement
+ * par la balise #TRI
+ *
+ * {tri [champ_par_defaut][,sens_par_defaut][,nom_variable]}
+ * champ_par_defaut : un champ de la table sql
+ * sens_par_defaut : -1 ou inverse pour decroissant, 1 ou direct pour croissant
+ *   peut etre un tableau pour preciser des sens par defaut associes a chaque champ
+ *   exemple : array('titre'=>1,'date'=>-1) pour trier par defaut
+ *   les titre croissant et les dates decroissantes
+ *   dans ce cas, quand un champ est utilise pour le tri et n'est pas present dans le tableau
+ *   c'est la premiere valeur qui est utilisee
+ * nom_variable : nom de la variable utilisee (par defaut tri_nomboucle)
+ *
+ * {tri titre}
+ * {tri titre,inverse}
+ * {tri titre,-1}
+ * {tri titre,-1,truc}
+ *
+ * le critere {tri} s'utilise conjointement avec la balise #TRI dans la meme boucle
+ * pour generer les liens qui permettent de changer le critere de tri et le sens du tri
+ *
+ * Exemple d'utilisation
+ *
+ * <B_articles>
+ * <p>#TRI{titre,'Trier par titre'} | #TRI{date,'Trier par date'}</p>
+ * <ul>
+ * <BOUCLE_articles(ARTICLES){tri titre}>
+ *     <li>#TITRE - [(#DATE|affdate_jourcourt)]</li>
+ * </BOUCLE_articles>
+ * </ul>
+ * </B_articles>
+ *
+ * NB :
+ * contraitement a {par ...} {tri} ne peut prendre qu'un seul champ,
+ * mais il peut etre complete avec {par ...} pour indiquer des criteres secondaires
+ *
+ * ex :
+ * {tri num titre}{par titre} permet de faire un tri sur le rang (modifiable dynamiquement)
+ * avec un second critere sur le titre en cas d'egalite des rang
+ *
+ * @param unknown_type $idb
+ * @param unknown_type $boucles
+ * @param unknown_type $crit
+ */
+function critere_tri_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+
+       // definition du champ par defaut
+       $_champ_defaut = !isset($crit->param[0][0]) ? "''"
+               : calculer_liste(array($crit->param[0][0]), array(), $boucles, $boucle->id_parent);
+       $_sens_defaut = !isset($crit->param[1][0]) ? "1"
+               : calculer_liste(array($crit->param[1][0]), array(), $boucles, $boucle->id_parent);
+       $_variable = !isset($crit->param[2][0]) ? "'$idb'"
+               : calculer_liste(array($crit->param[2][0]), array(), $boucles, $boucle->id_parent);
+
+       $_tri = "((\$t=(isset(\$Pile[0]['tri'.$_variable]))?\$Pile[0]['tri'.$_variable]:$_champ_defaut)?tri_protege_champ(\$t):'')";
+
+       $_sens_defaut = "(is_array(\$s=$_sens_defaut)?(isset(\$s[\$st=$_tri])?\$s[\$st]:reset(\$s)):\$s)";
+       $_sens = "((intval(\$t=(isset(\$Pile[0]['sens'.$_variable]))?\$Pile[0]['sens'.$_variable]:$_sens_defaut)==-1 OR \$t=='inverse')?-1:1)";
+
+       $boucle->modificateur['tri_champ'] = $_tri;
+       $boucle->modificateur['tri_sens'] = $_sens;
+       $boucle->modificateur['tri_nom'] = $_variable;
+       // faut il inserer un test sur l'existence de $tri parmi les champs de la table ?
+       // evite des erreurs sql, mais peut empecher des tri sur jointure ...
+       $boucle->hash .= "
+       \$senstri = '';
+       \$tri = $_tri;
+       if (\$tri){
+               \$senstri = $_sens;
+               \$senstri = (\$senstri<0)?' DESC':'';
+       };
+       ";
+       $boucle->select[] = "\".tri_champ_select(\$tri).\"";
+       $boucle->order[] = "tri_champ_order(\$tri,\$command['from']).\$senstri";
+}
+
+# Criteres de comparaison
+
+// http://doc.spip.org/@calculer_critere_DEFAUT
+function calculer_critere_DEFAUT_dist($idb, &$boucles, $crit){
+       // double cas particulier {0,1} et {1/2} repere a l'analyse lexicale
+       if (($crit->op==",") OR ($crit->op=='/'))
+               return calculer_critere_parties($idb, $boucles, $crit);
+
+       $r = calculer_critere_infixe($idb, $boucles, $crit);
+       if (!$r){
+       #       // on produit une erreur seulement si le critere n'a pas de '?'
+       #       if (!$crit->cond) {
+                       return (array('zbug_critere_inconnu', array('critere' => $crit->op)));
+       #       }
+       } else calculer_critere_DEFAUT_args($idb, $boucles, $crit, $r);
+}
+
+function calculer_critere_DEFAUT_args($idb, &$boucles, $crit, $args){
+       list($arg, $op, $val, $col, $where_complement) = $args;
+
+       $where = array("'$op'", "'$arg'", $val[0]);
+
+       // inserer la negation (cf !...)
+
+       if ($crit->not) $where = array("'NOT'", $where);
+       if ($crit->exclus)
+               if (!preg_match(",^L[0-9]+[.],", $arg))
+                       $where = array("'NOT'", $where);
+               else
+                       // un not sur un critere de jointure se traduit comme un NOT IN avec une sous requete
+                       // c'est une sous requete identique a la requete principale sous la forme (SELF,$select,$where) avec $select et $where qui surchargent
+                       $where = array("'NOT'", array("'IN'", "'".$boucles[$idb]->id_table.".".$boucles[$idb]->primary."'", array("'SELF'", "'".$boucles[$idb]->id_table.".".$boucles[$idb]->primary."'", $where)));
+
+       // inserer la condition (cf {lang?})
+       // traiter a part la date, elle est mise d'office par SPIP,
+       if ($crit->cond){
+               $pred = calculer_argument_precedent($idb, $col, $boucles);
+               if ($col=="date" OR $col=="date_redac"){
+                       if ($pred=="\$Pile[0]['".$col."']"){
+                               $pred = "(\$Pile[0]['{$col}_default']?'':$pred)";
+                       }
+               }
+
+               if ($op=='=' AND !$crit->not)
+                       $where = array("'?'", "(is_array($pred))",
+                                      critere_IN_cas($idb, $boucles, 'COND', $arg, $op, array($pred), $col),
+                                      $where);
+               $where = array("'?'", "!(is_array($pred)?count($pred):strlen($pred))", "''", $where);
+               if ($where_complement) // condition annexe du type "AND (objet='article')"
+                       $where_complement = array("'?'", "!(is_array($pred)?count($pred):strlen($pred))", "''", $where_complement);
+       }
+
+       $boucles[$idb]->where[] = $where;
+       if ($where_complement) // condition annexe du type "AND (objet='article')"
+               $boucles[$idb]->where[] = $where_complement;
+}
+
+// http://doc.spip.org/@calculer_critere_infixe
+function calculer_critere_infixe($idb, &$boucles, $crit){
+
+       global $table_criteres_infixes;
+       global $exceptions_des_jointures, $exceptions_des_tables;
+
+       $boucle = &$boucles[$idb];
+       $type = $boucle->type_requete;
+       $table = $boucle->id_table;
+       $desc = $boucle->show;
+       $col_vraie = null;
+
+       list($fct, $col, $op, $val, $args_sql) =
+               calculer_critere_infixe_ops($idb, $boucles, $crit);
+
+       $col_alias = $col;
+       $where_complement = false;
+
+       // Cas particulier : id_enfant => utiliser la colonne id_objet
+       if ($col=='id_enfant')
+               $col = $boucle->primary;
+
+       // Cas particulier : id_parent => verifier les exceptions de tables
+       if (in_array($col,array('id_parent','id_secteur'))
+         AND isset($exceptions_des_tables[$table][$col]))
+               $col = $exceptions_des_tables[$table][$col];
+
+       // et possibilite de gerer un critere secteur sur des tables de plugins (ie forums)
+       else if (($col=='id_secteur') AND ($critere_secteur = charger_fonction("critere_secteur_$type", "public", true))){
+               $table = $critere_secteur($idb, $boucles, $val, $crit);
+       }
+
+               // cas id_article=xx qui se mappe en id_objet=xx AND objet=article
+               // sauf si exception declaree : sauter cette etape
+       else if (
+               !isset($exceptions_des_jointures[table_objet_sql($table)][$col])
+               AND !isset($exceptions_des_jointures[$col])
+                   AND count(trouver_champs_decomposes($col, $desc))>1
+       ){
+               $e = decompose_champ_id_objet($col);
+               $col = array_shift($e);
+               $where_complement = primary_doublee($e, $table);
+       }
+               // Cas particulier : expressions de date
+       else if ($c = calculer_critere_infixe_date($idb, $boucles, $col)){
+               list($col,$col_vraie) = $c;
+               $table = '';
+       }
+       else if (preg_match('/^(.*)\.(.*)$/', $col, $r)){
+               list(, $table, $col) = $r;
+               $col_alias = $col;
+
+               $trouver_table = charger_fonction('trouver_table','base');
+               if ($desc = $trouver_table($table, $boucle->sql_serveur)
+                 AND isset($desc['field'][$col])
+                 AND $cle = array_search($desc['table'],$boucle->from))
+                       $table = $cle;
+               else {
+                       $table = trouver_jointure_champ($col, $boucle, array($table), ($crit->cond OR $op!='='));
+               }
+               #$table = calculer_critere_externe_init($boucle, array($table), $col, $desc, ($crit->cond OR $op!='='), true);
+               if (!$table) return '';
+       }
+       elseif (@!array_key_exists($col, $desc['field'])
+                // Champ joker * des iterateurs DATA qui accepte tout
+                AND @!array_key_exists('*', $desc['field'])
+       ) {
+               $r = calculer_critere_infixe_externe($boucle, $crit, $op, $desc, $col, $col_alias, $table);
+               if (!$r) return '';
+               list($col, $col_alias, $table, $where_complement, $desc) = $r;
+       }
+
+       $col_vraie = ($col_vraie?$col_vraie:$col);
+       // Dans tous les cas,
+       // virer les guillemets eventuels autour d'un int (qui sont refuses par certains SQL) et passer dans sql_quote avec le type si connu
+       // et int sinon si la valeur est numerique
+       // sinon introduire le vrai type du champ si connu dans le sql_quote (ou int NOT NULL sinon)
+       // Ne pas utiliser intval, PHP tronquant les Bigint de SQL
+       if ($op=='=' OR in_array($op, $table_criteres_infixes)){
+
+               // defaire le quote des int et les passer dans sql_quote avec le bon type de champ si on le connait, int sinon
+               // prendre en compte le debug ou la valeur arrive avec un commentaire PHP en debut
+               if (preg_match(",^\\A(\s*//.*?$\s*)?\"'(-?\d+)'\"\\z,ms", $val[0], $r))
+                       $val[0] = $r[1].'"'.sql_quote($r[2],$boucle->sql_serveur,(isset($desc['field'][$col_vraie])?$desc['field'][$col_vraie]:'int NOT NULL')).'"';
+
+               // sinon expliciter les
+               // sql_quote(truc) en sql_quote(truc,'',type)
+               // sql_quote(truc,serveur) en sql_quote(truc,serveur,type)
+               // sans toucher aux
+               // sql_quote(truc,'','varchar(10) DEFAULT \'oui\' COLLATE NOCASE')
+               // sql_quote(truc,'','varchar')
+               elseif (preg_match('/\Asql_quote[(](.*?)(,[^)]*?)?(,[^)]*(?:\(\d+\)[^)]*)?)?[)]\s*\z/ms', $val[0], $r)
+                       // si pas deja un type
+                 AND (!isset($r[3]) OR !$r[3])) {
+                       $r = $r[1]
+                               .((isset($r[2]) AND $r[2]) ? $r[2] : ",''")
+                               .",'".(isset($desc['field'][$col_vraie])?addslashes($desc['field'][$col_vraie]):'int NOT NULL')."'";
+                       $val[0] = "sql_quote($r)";
+               }
+       }
+       // Indicateur pour permettre aux fonctionx boucle_X de modifier 
+       // leurs requetes par defaut, notamment le champ statut
+       // Ne pas confondre champs de la table principale et des jointures
+       if ($table===$boucle->id_table){
+               $boucles[$idb]->modificateur['criteres'][$col_vraie] = true;
+               if ($col_alias!=$col_vraie)
+                       $boucles[$idb]->modificateur['criteres'][$col_alias] = true;
+       }
+
+       // ajout pour le cas special d'une condition sur le champ statut:
+       // il faut alors interdire a la fonction de boucle
+       // de mettre ses propres criteres de statut
+       // http://www.spip.net/@statut (a documenter)
+       // garde pour compatibilite avec code des plugins anterieurs, mais redondant avec la ligne precedente
+       if ($col=='statut') $boucles[$idb]->statut = true;
+
+       // inserer le nom de la table SQL devant le nom du champ
+       if ($table){
+               if ($col[0]=="`")
+                       $arg = "$table.".substr($col, 1, -1);
+               else $arg = "$table.$col";
+       } else $arg = $col;
+
+       // inserer la fonction SQL
+       if ($fct) $arg = "$fct($arg$args_sql)";
+
+       return array($arg, $op, $val, $col_alias, $where_complement);
+}
+
+function calculer_critere_infixe_externe($boucle, $crit, $op, $desc, $col, $col_alias, $table){
+       global $exceptions_des_jointures;
+       $where = '';
+
+       $calculer_critere_externe = 'calculer_critere_externe_init';
+       // gestion par les plugins des jointures tordues 
+       // pas automatiques mais necessaires
+       $table_sql = table_objet_sql($table);
+       if (isset($exceptions_des_jointures[$table_sql])
+         AND is_array($exceptions_des_jointures[$table_sql])
+         AND
+                       (
+                       isset($exceptions_des_jointures[$table_sql][$col])
+                       OR
+                       isset($exceptions_des_jointures[$table_sql][''])
+                       )
+               ){
+               $t = $exceptions_des_jointures[$table_sql];
+               $index = isset($t[$col])
+                       ? $t[$col] : (isset($t['']) ? $t[''] : array());
+
+               if (count($index)==3)
+                       list($t, $col, $calculer_critere_externe) = $index;
+               elseif (count($index)==2) {
+                       list($t, $col) = $t[$col];
+               }
+               elseif (count($index)==1) {
+                       list($calculer_critere_externe) = $index;
+                       $t = $table;
+               }
+               else
+                       $t = ''; // jointure non declaree. La trouver.
+       }
+       elseif (isset($exceptions_des_jointures[$col]))
+               list($t, $col) = $exceptions_des_jointures[$col];
+       else
+               $t = ''; // jointure non declaree. La trouver.
+
+       // ici on construit le from pour fournir $col en piochant dans les jointures
+
+       // si des jointures explicites sont fournies, on cherche d'abord dans celles ci
+       // permet de forcer une table de lien quand il y a ambiguite
+       // <BOUCLE_(DOCUMENTS documents_liens){id_mot}>
+       // alors que <BOUCLE_(DOCUMENTS){id_mot}> produit la meme chose que <BOUCLE_(DOCUMENTS mots_liens){id_mot}>
+       $table = "";
+       if ($boucle->jointures_explicites){
+               $jointures_explicites = explode(' ', $boucle->jointures_explicites);
+               $table = $calculer_critere_externe($boucle, $jointures_explicites, $col, $desc, ($crit->cond OR $op!='='), $t);
+       }
+
+       // et sinon on cherche parmi toutes les jointures declarees
+       if (!$table) {
+               $table = $calculer_critere_externe($boucle, $boucle->jointures, $col, $desc, ($crit->cond OR $op!='='), $t);
+       }
+
+       if (!$table) return '';
+
+       // il ne reste plus qu'a trouver le champ dans les from
+       list($nom, $desc) = trouver_champ_exterieur($col, $boucle->from, $boucle);
+
+       if (count(trouver_champs_decomposes($col, $desc))>1){
+               $col_alias = $col; // id_article devient juste le nom d'origine
+               $e = decompose_champ_id_objet($col);
+               $col = array_shift($e);
+               $where = primary_doublee($e, $table);
+       }
+
+       return array($col, $col_alias, $table, $where, $desc);
+}
+
+// Ne pas appliquer sql_quote lors de la compilation,
+// car on ne connait pas le serveur SQL, donc s'il faut \' ou ''
+
+// http://doc.spip.org/@primary_doublee
+function primary_doublee($decompose, $table){
+       $e1 = reset($decompose);
+       $e2 = "sql_quote('".end($decompose)."')";
+       return array("'='", "'$table.".$e1."'", $e2);
+}
+
+/**
+ * Champ hors table, ca ne peut etre qu'une jointure.
+ * On cherche la table du champ et on regarde si elle est deja jointe
+ * Si oui et qu'on y cherche un champ nouveau, pas de jointure supplementaire
+ * Exemple: criteres {titre_mot=...}{type_mot=...}
+ * Dans les 2 autres cas ==> jointure
+ * (Exemple: criteres {type_mot=...}{type_mot=...} donne 2 jointures
+ * pour selectioner ce qui a exactement ces 2 mots-cles.
+ *
+ * http://doc.spip.org/@calculer_critere_externe_init
+ *
+ * @param  $boucle
+ * @param  $joints
+ * @param  $col
+ * @param  $desc
+ * @param  $cond
+ * @param bool|string $checkarrivee
+ * @return mixed|string
+ */
+function calculer_critere_externe_init(&$boucle, $joints, $col, $desc, $cond, $checkarrivee = false){
+       // si on demande un truc du genre spip_mots
+       // avec aussi spip_mots_liens dans les jointures dispo
+       // et qu'on est la
+       // il faut privilegier la jointure directe en 2 etapes spip_mots_liens, spip_mots
+       if ($checkarrivee
+               AND is_string($checkarrivee)
+           AND $a = table_objet($checkarrivee)
+               AND in_array($a.'_liens', $joints)
+       ){
+               if ($res = calculer_lien_externe_init($boucle, $joints, $col, $desc, $cond, $checkarrivee)) {
+                       return $res;
+               }
+       }
+       foreach ($joints as $joint){
+               if ($arrivee = trouver_champ_exterieur($col, array($joint), $boucle, $checkarrivee)){
+                       $t = array_search($arrivee[0], $boucle->from);
+                       // transformer eventuellement id_xx en (id_objet,objet)
+                       $cols = trouver_champs_decomposes($col, $arrivee[1]);
+                       if ($t){
+                               $joindre = false;
+                               foreach ($cols as $col){
+                                       $c = '/\b'.$t.".$col".'\b/';
+                                       if (trouver_champ($c, $boucle->where)) $joindre = true;
+                                       else {
+                                               // mais ca peut etre dans le FIELD pour le Having
+                                               $c = "/FIELD.$t".".$col,/";
+                                               if (trouver_champ($c, $boucle->select)) $joindre = true;
+                                       }
+                               }
+                               if (!$joindre) return $t;
+                       }
+                       if ($res = calculer_jointure($boucle, array($boucle->id_table, $desc), $arrivee, $cols, $cond, 1)) {
+                               return $res;
+                       }
+               }
+       }
+       return '';
+
+}
+
+/**
+ * Generer directement une jointure via une table de lien spip_xxx_liens
+ * pour un critere {id_xxx}
+ * @param  $boucle
+ * @param  $joints
+ * @param  $col
+ * @param  $desc
+ * @param  $cond
+ * @param bool $checkarrivee
+ * @return string
+ */
+function calculer_lien_externe_init(&$boucle, $joints, $col, $desc, $cond, $checkarrivee = false){
+       $primary_arrivee = id_table_objet($checkarrivee);
+
+       $intermediaire = trouver_champ_exterieur($primary_arrivee, $joints, $boucle, $checkarrivee."_liens");
+       $arrivee = trouver_champ_exterieur($col, $joints, $boucle, $checkarrivee);
+
+       if (!$intermediaire OR !$arrivee) return '';
+
+       $res = fabrique_jointures($boucle,
+                                 array(
+                                      array($boucle->id_table, $intermediaire, array(id_table_objet($desc['table_objet']), 'id_objet', 'objet', $desc['type'])),
+                                      array(reset($intermediaire), $arrivee, $primary_arrivee)
+                                 )
+               , $cond, $desc, $boucle->id_table, array($col));
+       return $res;
+}
+
+
+// http://doc.spip.org/@trouver_champ
+function trouver_champ($champ, $where){
+       if (!is_array($where))
+               return preg_match($champ, $where);
+       else {
+               foreach ($where as $clause){
+                       if (trouver_champ($champ, $clause)) return true;
+               }
+               return false;
+       }
+}
+
+
+// determine l'operateur et les operandes
+
+// http://doc.spip.org/@calculer_critere_infixe_ops
+function calculer_critere_infixe_ops($idb, &$boucles, $crit){
+       // cas d'une valeur comparee a elle-meme ou son referent
+       if (count($crit->param)==0){
+               $op = '=';
+               $col = $val = $crit->op;
+               if (preg_match('/^(.*)\.(.*)$/', $col, $r)) $val = $r[2];
+               // Cas special {lang} : aller chercher $GLOBALS['spip_lang']
+               if ($val=='lang')
+                       $val = array(kwote('$GLOBALS[\'spip_lang\']'));
+               else {
+                       $defaut = null;
+                       if ($val=='id_parent') {
+                               // Si id_parent, comparer l'id_parent avec l'id_objet
+                               // de la boucle superieure.... faudrait verifier qu'il existe
+                               // pour eviter l'erreur SQL
+                               $val = $boucles[$idb]->primary;
+                               // mais si pas de boucle superieure, prendre id_parent dans l'env
+                               $defaut = "\$Pile[0]['id_parent']";
+                       }
+                       elseif ($val=='id_enfant'){
+                               // Si id_enfant, comparer l'id_objet avec l'id_parent
+                               // de la boucle superieure
+                               $val = 'id_parent';
+                       }
+                       elseif ($crit->cond AND ($col=="date" OR $col=="date_redac")){
+                               // un critere conditionnel sur date est traite a part
+                               // car la date est mise d'office par SPIP,
+                               $defaut = "(\$Pile[0]['{$col}_default']?'':\$Pile[0]['".$col."'])";
+                       }
+
+                       $val = calculer_argument_precedent($idb, $val, $boucles, $defaut);
+                       $val = array(kwote($val));
+               }
+       } else {
+               // comparaison explicite
+               // le phraseur impose que le premier param soit du texte
+               $params = $crit->param;
+               $op = $crit->op;
+               if ($op=='==') $op = 'REGEXP';
+               $col = array_shift($params);
+               $col = $col[0]->texte;
+
+               $val = array();
+               $desc = array('id_mere' => $idb);
+               $parent = $boucles[$idb]->id_parent;
+
+               // Dans le cas {x=='#DATE'} etc, defaire le travail du phraseur,
+               // celui ne sachant pas ce qu'est un critere infixe
+               // et a fortiori son 2e operande qu'entoure " ou '
+               if (count($params)==1
+                   AND count($params[0]==3)
+                       AND $params[0][0]->type=='texte'
+                           AND @$params[0][2]->type=='texte'
+                                AND ($p = $params[0][0]->texte)==$params[0][2]->texte
+                                    AND (($p=="'") OR ($p=='"'))
+                                        AND $params[0][1]->type=='champ'
+               ){
+                       $val[] = "$p\\$p#".$params[0][1]->nom_champ."\\$p$p";
+               } else
+                       foreach ((($op!='IN') ? $params : calculer_vieux_in($params)) as $p){
+                               $a = calculer_liste($p, $desc, $boucles, $parent);
+                               if (strcasecmp($op,'IN')==0) $val[] = $a;
+                               else $val[] = kwote($a, $boucles[$idb]->sql_serveur, 'char'); // toujours quoter en char ici
+                       }
+       }
+
+       $fct = $args_sql = '';
+       // fonction SQL ?
+       if (preg_match('/^(.*)'.SQL_ARGS.'$/', $col, $m)){
+               $fct = $m[1];
+               preg_match('/^\(([^,]*)(.*)\)$/', $m[2], $a);
+               $col = $a[1];
+               if (preg_match('/^(\S*)(\s+AS\s+.*)$/i', $col, $m)){
+                       $col = $m[1];
+                       $args_sql = $m[2];
+               }
+               $args_sql .= $a[2];
+               ;
+       }
+
+       return array($fct, $col, $op, $val, $args_sql);
+}
+
+// compatibilite ancienne version
+
+// http://doc.spip.org/@calculer_vieux_in
+function calculer_vieux_in($params){
+       $deb = $params[0][0];
+       $k = count($params)-1;
+       $last = $params[$k];
+       $j = count($last)-1;
+       $last = $last[$j];
+       $n = isset($last->texte) ? strlen($last->texte) : 0;
+
+       if (!((isset($deb->texte[0])     AND $deb->texte[0]=='(')
+          && (isset($last->texte[$n-1]) AND $last->texte[$n-1]==')')))
+               return $params;
+       $params[0][0]->texte = substr($deb->texte, 1);
+       // attention, on peut avoir k=0,j=0 ==> recalculer
+       $last = $params[$k][$j];
+       $n = strlen($last->texte);
+       $params[$k][$j]->texte = substr($last->texte, 0, $n-1);
+       $newp = array();
+       foreach ($params as $v){
+               if ($v[0]->type!='texte')
+                       $newp[] = $v;
+               else {
+                       foreach (explode(',', $v[0]->texte) as $x){
+                               $t = new Texte;
+                               $t->texte = $x;
+                               $newp[] = array($t);
+                       }
+               }
+       }
+       return $newp;
+}
+
+// http://doc.spip.org/@calculer_critere_infixe_date
+function calculer_critere_infixe_date($idb, &$boucles, $col){
+       if (!preg_match(",^((age|jour|mois|annee)_relatif|date|mois|annee|jour|heure|age)(_[a-z]+)?$,", $col, $regs)) return '';
+       $boucle = $boucles[$idb];
+       $table = $boucle->show;
+       
+       if (!$table['date'] && !isset($GLOBALS['table_date'][$table['id_table']])) return '';
+       $pred = $date_orig = isset($GLOBALS['table_date'][$table['id_table']])? $GLOBALS['table_date'][$table['id_table']] : $table['date'];
+       $col = $regs[1];
+       if (isset($regs[3]) AND $suite = $regs[3]){
+               # Recherche de l'existence du champ date_xxxx,
+               # si oui choisir ce champ, sinon choisir xxxx
+
+               if ($table['field']["date$suite"])
+                       $date_orig = 'date'.$suite;
+               else
+                       $date_orig = substr($suite, 1);
+               $pred = $date_orig;
+       }
+       else
+               if (isset($regs[2]) AND $rel = $regs[2]) $pred = 'date';
+
+       $date_compare = "\"' . normaliser_date(".
+                       calculer_argument_precedent($idb, $pred, $boucles).
+                       ") . '\"";
+
+       $col_vraie = $date_orig;
+       $date_orig = $boucle->id_table.'.'.$date_orig;
+
+       switch ($col) {
+               case 'date':
+                       $col = $date_orig;
+                       break;
+               case 'jour':
+                       $col = "DAYOFMONTH($date_orig)";
+                       break;
+               case 'mois':
+                       $col = "MONTH($date_orig)";
+                       break;
+               case 'annee':
+                       $col = "YEAR($date_orig)";
+                       break;
+               case 'heure':
+                       $col = "DATE_FORMAT($date_orig, \\'%H:%i\\')";
+                       break;
+               case 'age':
+                       $col = calculer_param_date("NOW()", $date_orig);
+                       $col_vraie = "";// comparer a un int (par defaut)
+                       break;
+               case 'age_relatif':
+                       $col = calculer_param_date($date_compare, $date_orig);
+                       $col_vraie = "";// comparer a un int (par defaut)
+                       break;
+               case 'jour_relatif':
+                       $col = "LEAST(TO_DAYS(".$date_compare.")-TO_DAYS(".
+                              $date_orig."), DAYOFMONTH(".$date_compare.
+                              ")-DAYOFMONTH(".$date_orig.")+30.4368*(MONTH(".
+                              $date_compare.")-MONTH(".$date_orig.
+                              "))+365.2422*(YEAR(".$date_compare.")-YEAR(".
+                              $date_orig.")))";
+                       $col_vraie = "";// comparer a un int (par defaut)
+                       break;
+               case 'mois_relatif':
+                       $col = "MONTH(".$date_compare.")-MONTH(".
+                              $date_orig.")+12*(YEAR(".$date_compare.
+                              ")-YEAR(".$date_orig."))";
+                       $col_vraie = "";// comparer a un int (par defaut)
+                       break;
+               case 'annee_relatif':
+                       $col = "YEAR(".$date_compare.")-YEAR(".
+                              $date_orig.")";
+                       $col_vraie = "";// comparer a un int (par defaut)
+                       break;
+       }
+       return array($col,$col_vraie);
+}
+
+// http://doc.spip.org/@calculer_param_date
+function calculer_param_date($date_compare, $date_orig){
+       if (preg_match(",'\" *\.(.*)\. *\"',", $date_compare, $r)){
+               $init = "'\" . (\$x = $r[1]) . \"'";
+               $date_compare = '\'$x\'';
+       }
+       else
+               $init = $date_compare;
+
+       return
+               "LEAST((UNIX_TIMESTAMP(".
+               $init.
+               ")-UNIX_TIMESTAMP(".
+               $date_orig.
+               "))/86400,\n\tTO_DAYS(".
+               $date_compare.
+               ")-TO_DAYS(".
+               $date_orig.
+               "),\n\tDAYOFMONTH(".
+               $date_compare.
+               ")-DAYOFMONTH(".
+               $date_orig.
+               ")+30.4368*(MONTH(".
+               $date_compare.
+               ")-MONTH(".
+               $date_orig.
+               "))+365.2422*(YEAR(".
+               $date_compare.
+               ")-YEAR(".
+               $date_orig.
+               ")))";
+}
+
+/**
+ * (DATA){source mode, "xxxxxx", arg, arg, arg}
+ * @param string $idb
+ * @param object $boucles
+ * @param object $crit
+ */
+function critere_DATA_source_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+
+       $args = array();
+       foreach ($crit->param as &$param)
+               array_push($args,
+                          calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent));
+
+       $boucle->hash .= '
+       $command[\'sourcemode\'] = '.array_shift($args).";\n";
+
+       $boucle->hash .= '
+       $command[\'source\'] = array('.join(', ', $args).");\n";
+}
+
+
+/**
+ * (DATA){datasource "xxxxxx", mode}  <= deprecated
+ * @param string $idb
+ * @param object $boucles
+ * @param object $crit
+ */
+function critere_DATA_datasource_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       $boucle->hash .= '
+       $command[\'source\'] = array('.calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent).');
+       $command[\'sourcemode\'] = '.calculer_liste($crit->param[1], array(), $boucles, $boucles[$idb]->id_parent).';';
+}
+
+
+/**
+ * (DATA){datacache}
+ * @param string $idb
+ * @param object $boucles
+ * @param object $crit
+ */
+function critere_DATA_datacache_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       $boucle->hash .= '
+       $command[\'datacache\'] = '.calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent).';';
+}
+
+
+/**
+ * Pour passer des arguments a un iterateur non-spip
+ * (php:xxxIterator){args argument1, argument2, argument3}
+ *
+ * @param string $idb
+ * @param object $boucles
+ * @param object $crit
+ */
+function critere_php_args_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       $boucle->hash .= '$command[\'args\']=array();';
+       foreach ($crit->param as $param){
+               $boucle->hash .= '
+                       $command[\'args\'][] = '.calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent).';';
+       }
+}
+
+/**
+ * Passer une liste de donnees a l'iterateur DATA
+ * (DATA){liste X1, X2, X3}
+ *
+ * @param string $idb
+ * @param object $boucles
+ * @param object $crit
+ */
+function critere_DATA_liste_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       $boucle->hash .= "\n\t".'$command[\'liste\'] = array();'."\n";
+       foreach ($crit->param as $param){
+               $boucle->hash .= "\t".'$command[\'liste\'][] = '.calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent).";\n";
+       }
+}
+
+/**
+ * Passer un enum min max a l'iterateur DATA
+ * (DATA){enum Xmin, Xmax}
+ *
+ * @param string $idb
+ * @param object $boucles
+ * @param object $crit
+ */
+function critere_DATA_enum_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       $boucle->hash .= "\n\t".'$command[\'enum\'] = array();'."\n";
+       foreach ($crit->param as $param){
+               $boucle->hash .= "\t".'$command[\'enum\'][] = '.calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent).";\n";
+       }
+}
+
+/**
+ * Extraire un chemin d'un tableau de donnees
+ * (DATA){datapath query.results}
+ *
+ * @param string $idb
+ * @param object $boucles
+ * @param object $crit
+ */
+function critere_DATA_datapath_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       foreach ($crit->param as $param){
+               $boucle->hash .= '
+                       $command[\'datapath\'][] = '.calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent).';';
+       }
+}
+
+
+/**
+ * le critere {si ...} applicable a toutes les boucles
+ *
+ * @param string $idb
+ * @param object $boucles
+ * @param object $crit
+ */
+function critere_si_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       // il faut initialiser 1 fois le tableau a chaque appel de la boucle
+       // (par exemple lorsque notre boucle est appelee dans une autre boucle)
+       // mais ne pas l'initialiser n fois si il y a n criteres {si } dans la boucle !
+       $boucle->hash .= "\n\tif (!isset(\$si_init)) { \$command['si'] = array(); \$si_init = true; }\n";
+       if ($crit->param){
+               foreach ($crit->param as $param){
+                       $boucle->hash .= "\t\$command['si'][] = "
+                                       . calculer_liste($param, array(), $boucles, $boucles[$idb]->id_parent) . ";\n";
+               }
+               // interdire {si 0} aussi !
+       } else {
+               $boucle->hash .= '$command[\'si\'][] = 0;';
+       }
+}
+
+/**
+ * {tableau #XX} pour compatibilite ascendante boucle POUR
+ * ... preferer la notation {datasource #XX,table}
+ *
+ * @param string $idb
+ * @param object $boucles
+ * @param object $crit
+ */
+function critere_POUR_tableau_dist($idb, &$boucles, $crit){
+       $boucle = &$boucles[$idb];
+       $boucle->hash .= '
+       $command[\'source\'] = array('.calculer_liste($crit->param[0], array(), $boucles, $boucles[$idb]->id_parent).');
+       $command[\'sourcemode\'] = \'table\';';
+}
+
+
+/**
+ * Trouver toutes les objets qui ont des enfants (les noeuds de l'arbre)
+ * {noeud}
+ * {!noeud} retourne les feuilles
+ *
+ * @global array $exceptions_des_tables
+ * @param string $idb
+ * @param array $boucles
+ * @param Object $crit
+ */
+function critere_noeud_dist($idb, &$boucles, $crit){
+       global $exceptions_des_tables;
+       $not = $crit->not;
+       $boucle = &$boucles[$idb];
+       $primary = $boucle->primary;
+
+       if (!$primary OR strpos($primary, ',')){
+               erreur_squelette(_T('zbug_doublon_sur_table_sans_cle_primaire'), "BOUCLE$idb");
+               return;
+       }
+       $table = $boucle->type_requete;
+       $table_sql = table_objet_sql(objet_type($table));
+
+       $id_parent = isset($exceptions_des_tables[$boucle->id_table]['id_parent']) ?
+               $exceptions_des_tables[$boucle->id_table]['id_parent'] :
+               'id_parent';
+
+       $in = "IN";
+       $where = array("'IN'", "'$boucle->id_table."."$primary'", "'('.sql_get_select('$id_parent', '$table_sql').')'");
+       if ($not)
+               $where = array("'NOT'", $where);
+
+       $boucle->where[] = $where;
+}
+
+/**
+ * Trouver toutes les objets qui n'ont pas d'enfants (les feuilles de l'arbre)
+ * {feuille}
+ * {!feuille} retourne les noeuds
+ *
+ * @global array $exceptions_des_tables
+ * @param string $idb
+ * @param array $boucles
+ * @param Object $crit
+ */
+function critere_feuille_dist($idb, &$boucles, $crit){
+       $not = $crit->not;
+       $crit->not = $not ? false : true;
+       critere_noeud_dist($idb, $boucles, $crit);
+       $crit->not = $not;
+}
+
+?>