[SPIP] v3.2.1-->v3.2.2
[lhc/web/www.git] / www / ecrire / public / composer.php
1 <?php
2
3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
5 * *
6 * Copyright (c) 2001-2019 *
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 * Compose un squelette : compile le squelette au besoin et vérifie
15 * la validité du code compilé
16 *
17 * @package SPIP\Core\Compilateur\Composer
18 **/
19
20 if (!defined('_ECRIRE_INC_VERSION')) {
21 return;
22 }
23
24 include_spip('inc/texte');
25 include_spip('inc/documents');
26 include_spip('inc/distant');
27 include_spip('inc/rubriques'); # pour calcul_branche (cf critere branche)
28 include_spip('inc/acces'); // Gestion des acces pour ical
29 include_spip('inc/actions');
30 include_spip('public/iterateur');
31 include_spip('public/interfaces');
32 include_spip('public/quete');
33
34 # Charge et retourne un composeur ou '' s'il est inconnu. Le compile au besoin
35 # Charge egalement un fichier homonyme de celui du squelette
36 # mais de suffixe '_fonctions.php' pouvant contenir:
37 # 1. des filtres
38 # 2. des fonctions de traduction de balise, de critere et de boucle
39 # 3. des declaration de tables SQL supplementaires
40 # Toutefois pour 2. et 3. preferer la technique de la surcharge
41
42 // http://code.spip.net/@public_composer_dist
43 function public_composer_dist($squelette, $mime_type, $gram, $source, $connect = '') {
44
45 $nom = calculer_nom_fonction_squel($squelette, $mime_type, $connect);
46
47 // si deja en memoire (INCLURE a repetition) c'est bon.
48 if (function_exists($nom)) {
49 return $nom;
50 }
51
52 if (defined('_VAR_MODE') and _VAR_MODE == 'debug') {
53 $GLOBALS['debug_objets']['courant'] = $nom;
54 }
55
56 $phpfile = sous_repertoire(_DIR_SKELS, '', false, true) . $nom . '.php';
57
58 // si squelette est deja compile et perenne, le charger
59 if (!squelette_obsolete($phpfile, $source)) {
60 include_once $phpfile;
61 #if (!squelette_obsolete($phpfile, $source)
62 # AND lire_fichier ($phpfile, $skel_code,
63 # array('critique' => 'oui', 'phpcheck' => 'oui'))){
64 ## eval('?'.'>'.$skel_code);
65 # spip_log($skel_code, 'comp')
66 #}
67 }
68
69 if (file_exists($lib = $squelette . '_fonctions' . '.php')) {
70 include_once $lib;
71 }
72
73 // tester si le eval ci-dessus a mis le squelette en memoire
74
75 if (function_exists($nom)) {
76 return $nom;
77 }
78
79 // charger le source, si possible, et compiler
80 if (lire_fichier($source, $skel)) {
81 $compiler = charger_fonction('compiler', 'public');
82 $skel_code = $compiler($skel, $nom, $gram, $source, $connect);
83 }
84
85 // Ne plus rien faire si le compilateur n'a pas pu operer.
86 if (!$skel_code) {
87 return false;
88 }
89
90 foreach ($skel_code as $id => $boucle) {
91 $f = $boucle->return;
92 if (@eval("return true; $f ;") === false) {
93 // Code syntaxiquement faux (critere etc mal programme')
94 $msg = _T('zbug_erreur_compilation');
95 erreur_squelette($msg, $boucle);
96 // continuer pour trouver d'autres fautes eventuelles
97 // mais prevenir que c'est mort
98 $nom = '';
99 }
100 // Contexte de compil inutile a present
101 // (mais la derniere valeur de $boucle est utilisee ci-dessous)
102 $skel_code[$id] = $f;
103 }
104
105 $code = '';
106 if ($nom) {
107 // Si le code est bon, concatener et mettre en cache
108 if (function_exists($nom)) {
109 $code = squelette_traduit($skel, $source, $phpfile, $skel_code);
110 } else {
111 // code semantiquement faux: bug du compilateur
112 // $boucle est en fait ici la fct principale du squelette
113 $msg = _T('zbug_erreur_compilation');
114 erreur_squelette($msg, $boucle);
115 $nom = '';
116 }
117 }
118
119 if (defined('_VAR_MODE') and _VAR_MODE == 'debug') {
120
121 // Tracer ce qui vient d'etre compile
122 $GLOBALS['debug_objets']['code'][$nom . 'tout'] = $code;
123
124 // si c'est ce que demande le debusqueur, lui passer la main
125 if ($GLOBALS['debug_objets']['sourcefile']
126 and (_request('var_mode_objet') == $nom)
127 and (_request('var_mode_affiche') == 'code')
128 ) {
129 erreur_squelette();
130 }
131 }
132
133 return $nom ? $nom : false;
134 }
135
136 function squelette_traduit($squelette, $sourcefile, $phpfile, $boucles) {
137
138 // Le dernier index est '' (fonction principale)
139 $noms = substr(join(', ', array_keys($boucles)), 0, -2);
140 if (CODE_COMMENTE) {
141 $code = "
142 /*
143 * Squelette : $sourcefile
144 * Date : " . gmdate("D, d M Y H:i:s", @filemtime($sourcefile)) . " GMT
145 * Compile : " . gmdate("D, d M Y H:i:s", time()) . " GMT
146 * " . (!$boucles ? "Pas de boucle" : ("Boucles : " . $noms)) . "
147 */ ";
148 }
149
150 $code = '<' . "?php\n" . $code . join('', $boucles) . "\n?" . '>';
151 if (!defined('_VAR_NOCACHE') or !_VAR_NOCACHE) {
152 ecrire_fichier($phpfile, $code);
153 }
154
155 return $code;
156 }
157
158 // Le squelette compile est-il trop vieux ?
159 // http://code.spip.net/@squelette_obsolete
160 function squelette_obsolete($skel, $squelette) {
161 static $date_change = null;
162 // ne verifier la date de mes_fonctions et mes_options qu'une seule fois
163 // par hit
164 if (is_null($date_change)) {
165 if (@file_exists($fonc = 'mes_fonctions.php')) {
166 $date_change = @filemtime($fonc);
167 } # compatibilite
168 if (defined('_FILE_OPTIONS')) {
169 $date_change = max($date_change, @filemtime(_FILE_OPTIONS));
170 }
171 }
172
173 return (
174 (defined('_VAR_MODE') and in_array(_VAR_MODE, array('recalcul', 'preview', 'debug')))
175 or !@file_exists($skel)
176 or ((@file_exists($squelette) ? @filemtime($squelette) : 0)
177 > ($date = @filemtime($skel)))
178 or ($date_change > $date)
179 );
180 }
181
182 // Activer l'invalideur de session
183 // http://code.spip.net/@invalideur_session
184 function invalideur_session(&$Cache, $code = null) {
185 $Cache['session'] = spip_session();
186
187 return $code;
188 }
189
190
191 // http://code.spip.net/@analyse_resultat_skel
192 function analyse_resultat_skel($nom, $cache, $corps, $source = '') {
193 static $filtres = array();
194 $headers = array();
195
196 // Recupere les < ?php header('Xx: y'); ? > pour $page['headers']
197 // note: on essaie d'attrapper aussi certains de ces entetes codes
198 // "a la main" dans les squelettes, mais evidemment sans exhaustivite
199 if (stripos($corps, 'header') !== false
200 and preg_match_all(
201 '/(<[?]php\s+)@?header\s*\(\s*.([^:\'"]*):?\s*([^)]*)[^)]\s*\)\s*[;]?\s*[?]>/ims',
202 $corps, $regs, PREG_SET_ORDER)
203 ) {
204 foreach ($regs as $r) {
205 $corps = str_replace($r[0], '', $corps);
206 # $j = Content-Type, et pas content-TYPE.
207 $j = join('-', array_map('ucwords', explode('-', strtolower($r[2]))));
208
209 if ($j == 'X-Spip-Filtre' and isset($headers[$j])) {
210 $headers[$j] .= "|" . $r[3];
211 } else {
212 $headers[$j] = $r[3];
213 }
214 }
215 }
216 // S'agit-il d'un resultat constant ou contenant du code php
217 $process_ins = (
218 strpos($corps, '<' . '?') === false
219 or
220 (strpos($corps, '<' . '?xml') !== false and
221 strpos(str_replace('<' . '?xml', '', $corps), '<' . '?') === false)
222 )
223 ? 'html'
224 : 'php';
225
226 $skel = array(
227 'squelette' => $nom,
228 'source' => $source,
229 'process_ins' => $process_ins,
230 'invalideurs' => $cache,
231 'entetes' => $headers,
232 'duree' => isset($headers['X-Spip-Cache']) ? intval($headers['X-Spip-Cache']) : 0
233 );
234
235 // traiter #FILTRE{} et filtres
236 if (!isset($filtres[$nom])) {
237 $filtres[$nom] = pipeline('declarer_filtres_squelettes', array('args' => $skel, 'data' => array()));
238 }
239 if (count($filtres[$nom]) or (isset($headers['X-Spip-Filtre']) and strlen($headers['X-Spip-Filtre']))) {
240 include_spip('public/sandbox');
241 $corps = sandbox_filtrer_squelette($skel, $corps,
242 strlen($headers['X-Spip-Filtre']) ? explode('|', $headers['X-Spip-Filtre']) : array(), $filtres[$nom]);
243 unset($headers['X-Spip-Filtre']);
244
245 if ($process_ins == 'html') {
246 $skel['process_ins'] = (
247 strpos($corps, '<' . '?') === false
248 or
249 (strpos($corps, '<' . '?xml') !== false and
250 strpos(str_replace('<' . '?xml', '', $corps), '<' . '?') === false)
251 )
252 ? 'html'
253 : 'php';
254 }
255 }
256
257 $skel['entetes'] = $headers;
258 $skel['texte'] = $corps;
259
260 return $skel;
261 }
262
263 //
264 // Des fonctions diverses utilisees lors du calcul d'une page ; ces fonctions
265 // bien pratiques n'ont guere de logique organisationnelle ; elles sont
266 // appelees par certaines balises au moment du calcul des pages. (Peut-on
267 // trouver un modele de donnees qui les associe physiquement au fichier
268 // definissant leur balise ???
269 //
270
271
272 /**
273 * Calcul d'une introduction
274 *
275 * L'introduction est prise dans le descriptif s'il est renseigné,
276 * sinon elle est calculée depuis le texte : à ce moment là,
277 * l'introduction est prise dans le contenu entre les balises
278 * `<intro>` et `</intro>` si présentes, sinon en coupant le
279 * texte à la taille indiquée.
280 *
281 * Cette fonction est utilisée par la balise #INTRODUCTION
282 *
283 * @param string $descriptif
284 * Descriptif de l'introduction
285 * @param string $texte
286 * Texte à utiliser en absence de descriptif
287 * @param string $longueur
288 * Longueur de l'introduction
289 * @param string $connect
290 * Nom du connecteur à la base de données
291 * @param string $suite
292 * points de suite si on coupe (par defaut _INTRODUCTION_SUITE et sinon &nbsp;(...)
293 * @return string
294 * Introduction calculée
295 **/
296 function filtre_introduction_dist($descriptif, $texte, $longueur, $connect, $suite = null) {
297 // Si un descriptif est envoye, on l'utilise directement
298 if (strlen($descriptif)) {
299 return appliquer_traitement_champ($descriptif, 'introduction', '', array(), $connect);
300 }
301
302 // De preference ce qui est marque <intro>...</intro>
303 $intro = '';
304 $texte = preg_replace(",(</?)intro>,i", "\\1intro>", $texte); // minuscules
305 while ($fin = strpos($texte, "</intro>")) {
306 $zone = substr($texte, 0, $fin);
307 $texte = substr($texte, $fin + strlen("</intro>"));
308 if ($deb = strpos($zone, "<intro>") or substr($zone, 0, 7) == "<intro>") {
309 $zone = substr($zone, $deb + 7);
310 }
311 $intro .= $zone;
312 }
313
314 // [12025] On ne *PEUT* pas couper simplement ici car c'est du texte brut,
315 // qui inclus raccourcis et modeles
316 // un simple <articlexx> peut etre ensuite transforme en 1000 lignes ...
317 // par ailleurs le nettoyage des raccourcis ne tient pas compte
318 // des surcharges et enrichissement de propre
319 // couper doit se faire apres propre
320 //$texte = nettoyer_raccourcis_typo($intro ? $intro : $texte, $connect);
321
322 // Cependant pour des questions de perfs on coupe quand meme, en prenant
323 // large et en se mefiant des tableaux #1323
324
325 if (strlen($intro)) {
326 $texte = $intro;
327 } else {
328 if (strpos("\n" . $texte, "\n|") === false
329 and strlen($texte) > 2.5 * $longueur
330 ) {
331 if (strpos($texte, "<multi") !== false) {
332 $texte = extraire_multi($texte);
333 }
334 $texte = couper($texte, 2 * $longueur);
335 }
336 }
337
338 // ne pas tenir compte des notes
339 if ($notes = charger_fonction('notes', 'inc', true)) {
340 $notes('', 'empiler');
341 }
342 // Supprimer les modèles avant le propre afin d'éviter qu'ils n'ajoutent du texte indésirable
343 // dans l'introduction.
344 $texte = supprime_img($texte, '');
345 $texte = appliquer_traitement_champ($texte, 'introduction', '', array(), $connect);
346
347 if ($notes) {
348 $notes('', 'depiler');
349 }
350
351 if (is_null($suite) and defined('_INTRODUCTION_SUITE')) {
352 $suite = _INTRODUCTION_SUITE;
353 }
354 $texte = couper($texte, $longueur, $suite);
355 // comme on a coupe il faut repasser la typo (on a perdu les insecables)
356 $texte = typo($texte, true, $connect, array());
357
358 // et reparagrapher si necessaire (coherence avec le cas descriptif)
359 // une introduction a tojours un <p>
360 if ($GLOBALS['toujours_paragrapher']) // Fermer les paragraphes
361 {
362 $texte = paragrapher($texte, $GLOBALS['toujours_paragrapher']);
363 }
364
365 return $texte;
366 }
367
368 //
369 // Balises dynamiques
370 //
371
372 /** Code PHP pour inclure une balise dynamique à l'exécution d'une page */
373 define('CODE_INCLURE_BALISE', '<' . '?php
374 include_once("%s");
375 if ($lang_select = "%s") $lang_select = lang_select($lang_select);
376 inserer_balise_dynamique(balise_%s_dyn(%s), array(%s));
377 if ($lang_select) lang_select();
378 ?'
379 . '>');
380
381 /**
382 * Synthétise une balise dynamique : crée l'appel à l'inclusion
383 * en transmettant les arguments calculés et le contexte de compilation.
384 *
385 * @uses argumenter_squelette() Pour calculer les arguments de l'inclusion
386 *
387 * @param string $nom
388 * Nom de la balise dynamique
389 * @param array $args
390 * Liste des arguments calculés
391 * @param string $file
392 * Chemin du fichier de squelette à inclure
393 * @param array $context_compil
394 * Tableau d'informations sur la compilation
395 * @return string
396 * Code PHP pour inclure le squelette de la balise dynamique
397 **/
398 function synthetiser_balise_dynamique($nom, $args, $file, $context_compil) {
399 if (strncmp($file, "/", 1) !== 0) {
400 $file = './" . _DIR_RACINE . "' . $file;
401 }
402 $r = sprintf(CODE_INCLURE_BALISE,
403 $file,
404 $context_compil[4] ? $context_compil[4] : '',
405 $nom,
406 join(', ', array_map('argumenter_squelette', $args)),
407 join(', ', array_map('_q', $context_compil)));
408
409 return $r;
410 }
411
412 /**
413 * Crée le code PHP pour transmettre des arguments (généralement pour une inclusion)
414 *
415 * @param array|string $v
416 * Arguments à transmettre :
417 *
418 * - string : un simple texte à faire écrire
419 * - array : couples ('nom' => 'valeur') liste des arguments et leur valeur
420 * @return string
421 *
422 * - Code PHP créant le tableau des arguments à transmettre,
423 * - ou texte entre quote `'` (si `$v` était une chaîne)
424 **/
425 function argumenter_squelette($v) {
426
427 if (!is_array($v)) {
428 return "'" . texte_script($v) . "'";
429 } else {
430 $out = array();
431 foreach ($v as $k => $val) {
432 $out [] = argumenter_squelette($k) . '=>' . argumenter_squelette($val);
433 }
434
435 return 'array(' . join(", ", $out) . ')';
436 }
437 }
438
439
440 /**
441 * Calcule et retourne le code PHP retourné par l'exécution d'une balise
442 * dynamique.
443 *
444 * Vérifier les arguments et filtres et calcule le code PHP à inclure.
445 *
446 * - charge le fichier PHP de la balise dynamique dans le répertoire
447 * `balise/`, soit du nom complet de la balise, soit d'un nom générique
448 * (comme 'formulaire_.php'). Dans ce dernier cas, le nom de la balise
449 * est ajouté en premier argument.
450 * - appelle une éventuelle fonction de traitement des arguments `balise_NOM_stat()`
451 * - crée le code PHP de la balise si une fonction `balise_NOM_dyn()` (ou variantes)
452 * est effectivement trouvée.
453 *
454 * @uses synthetiser_balise_dynamique()
455 * Pour calculer le code PHP d'inclusion produit
456 *
457 * @param string $nom
458 * Nom de la balise dynamique
459 * @param array $args
460 * Liste des arguments calculés de la balise
461 * @param array $context_compil
462 * Tableau d'informations sur la compilation
463 * @return string
464 * Code PHP d'exécutant l'inclusion du squelette (ou texte) de la balise dynamique
465 **/
466 function executer_balise_dynamique($nom, $args, $context_compil) {
467 $nomfonction = $nom;
468 $nomfonction_generique = "";
469
470 // Calculer un nom générique (ie. 'formulaire_' dans 'formulaire_editer_article')
471 if (false !== ($p = strpos($nom, "_"))) {
472 $nomfonction_generique = substr($nom, 0, $p + 1);
473 }
474
475 if (!$fonction_balise = charger_fonction($nomfonction, 'balise', true)) {
476 if ($nomfonction_generique and $fonction_balise = charger_fonction($nomfonction_generique, 'balise', true)) {
477 // et injecter en premier arg le nom de la balise
478 array_unshift($args, $nom);
479 $nomfonction = $nomfonction_generique;
480 }
481 }
482
483 if (!$fonction_balise) {
484 $msg = array('zbug_balise_inexistante', array('from' => 'CVT', 'balise' => $nom));
485 erreur_squelette($msg, $context_compil);
486
487 return '';
488 }
489
490 // retrouver le fichier qui a déclaré la fonction
491 // même si la fonction dynamique est déclarée dans un fichier de fonctions.
492 // Attention sous windows, getFileName() retourne un antislash.
493 $reflector = new ReflectionFunction($fonction_balise);
494 $file = str_replace('\\', '/', $reflector->getFileName());
495 if (strncmp($file, str_replace('\\', '/', _ROOT_RACINE), strlen(_ROOT_RACINE)) === 0) {
496 $file = substr($file, strlen(_ROOT_RACINE));
497 }
498
499 // Y a-t-il une fonction de traitement des arguments ?
500 $f = 'balise_' . $nomfonction . '_stat';
501
502 $r = !function_exists($f) ? $args : $f($args, $context_compil);
503
504 if (!is_array($r)) {
505 return $r;
506 }
507
508 // verifier que la fonction dyn est la,
509 // sinon se replier sur la generique si elle existe
510 if (!function_exists('balise_' . $nomfonction . '_dyn')) {
511 if ($nomfonction_generique
512 and $file = include_spip("balise/" . strtolower($nomfonction_generique))
513 and function_exists('balise_' . $nomfonction_generique . '_dyn')
514 ) {
515 // et lui injecter en premier arg le nom de la balise
516 array_unshift($r, $nom);
517 $nomfonction = $nomfonction_generique;
518 if (!_DIR_RESTREINT) {
519 $file = _DIR_RESTREINT_ABS . $file;
520 }
521 } else {
522 $msg = array('zbug_balise_inexistante', array('from' => 'CVT', 'balise' => $nom));
523 erreur_squelette($msg, $context_compil);
524
525 return '';
526 }
527 }
528
529 return synthetiser_balise_dynamique($nomfonction, $r, $file, $context_compil);
530
531 }
532
533 /**
534 * Retourne pour une clé primaire d'objet donnée les identifiants ayant un logo
535 *
536 * @uses type_du_logo() Pour calculer le nom du logo
537 *
538 * @param string $type
539 * Nom de la clé primaire de l'objet
540 * @return string
541 * Liste des identifiants ayant un logo (séparés par une virgule)
542 **/
543 function lister_objets_avec_logos($type) {
544
545 $logos = array();
546 $chercher_logo = charger_fonction('chercher_logo', 'inc');
547 $type = '/'
548 . type_du_logo($type)
549 . "on(\d+)\.("
550 . join('|', $GLOBALS['formats_logos'])
551 . ")$/";
552
553 if ($d = opendir(_DIR_LOGOS)) {
554 while (($f = readdir($d)) !== false) {
555 if (preg_match($type, $f, $r)) {
556 $logos[] = $r[1];
557 }
558 }
559 }
560 @closedir($d);
561
562 return join(',', $logos);
563 }
564
565
566 /**
567 * Renvoie l'état courant des notes, le purge et en prépare un nouveau
568 *
569 * Fonction appelée par la balise `#NOTES`
570 *
571 * @see balise_NOTES_dist()
572 * @uses inc_notes_dist()
573 *
574 * @return string
575 * Code HTML des notes
576 **/
577 function calculer_notes() {
578 $r = '';
579 if ($notes = charger_fonction('notes', 'inc', true)) {
580 $r = $notes(array());
581 $notes('', 'depiler');
582 $notes('', 'empiler');
583 }
584
585 return $r;
586 }
587
588 /**
589 * Selectionner la langue de l'objet dans la boucle
590 *
591 * Applique sur un item de boucle la langue de l'élément qui est parcourru.
592 * Sauf dans les cas ou il ne le faut pas !
593 *
594 * La langue n'est pas modifiée lorsque :
595 * - la globale 'forcer_lang' est définie à true
596 * - l'objet ne définit pas de langue
597 * - le titre contient une balise multi.
598 *
599 * @param string $lang
600 * Langue de l'objet
601 * @param string $lang_select
602 * 'oui' si critère lang_select est présent, '' sinon.
603 * @param null|string $titre
604 * Titre de l'objet
605 * @return null;
606 **/
607 function lang_select_public($lang, $lang_select, $titre = null) {
608 // Cas 1. forcer_lang = true et pas de critere {lang_select}
609 if (isset($GLOBALS['forcer_lang']) and $GLOBALS['forcer_lang']
610 and $lang_select !== 'oui'
611 ) {
612 $lang = $GLOBALS['spip_lang'];
613 } // Cas 2. l'objet n'a pas de langue definie (ou definie a '')
614 elseif (!strlen($lang)) {
615 $lang = $GLOBALS['spip_lang'];
616 } // Cas 3. l'objet est multilingue !
617 elseif ($lang_select !== 'oui'
618 and strlen($titre) > 10
619 and strpos($titre, '<multi>') !== false
620 and strpos(echappe_html($titre), '<multi>') !== false
621 ) {
622 $lang = $GLOBALS['spip_lang'];
623 }
624
625 // faire un lang_select() eventuellement sur la langue inchangee
626 lang_select($lang);
627
628 return;
629 }
630
631
632 // Si un tableau &doublons[articles] est passe en parametre,
633 // il faut le nettoyer car il pourrait etre injecte en SQL
634 // http://code.spip.net/@nettoyer_env_doublons
635 function nettoyer_env_doublons($envd) {
636 foreach ($envd as $table => $liste) {
637 $n = '';
638 foreach (explode(',', $liste) as $val) {
639 if ($a = intval($val) and $val === strval($a)) {
640 $n .= ',' . $val;
641 }
642 }
643 if (strlen($n)) {
644 $envd[$table] = $n;
645 } else {
646 unset($envd[$table]);
647 }
648 }
649
650 return $envd;
651 }
652
653 /**
654 * Cherche la présence d'un opérateur SELF ou SUBSELECT
655 *
656 * Cherche dans l'index 0 d'un tableau, la valeur SELF ou SUBSELECT
657 * indiquant pour une expression WHERE de boucle que nous sommes
658 * face à une sous-requête.
659 *
660 * Cherche de manière récursive également dans les autres valeurs si celles-ci
661 * sont des tableaux
662 *
663 * @param string|array $w
664 * Description d'une condition WHERE de boucle (ou une partie de cette description)
665 * @return string|bool
666 * Opérateur trouvé (SELF ou SUBSELECT) sinon false.
667 **/
668 function match_self($w) {
669 if (is_string($w)) {
670 return false;
671 }
672 if (is_array($w)) {
673 if (in_array(reset($w), array("SELF", "SUBSELECT"))) {
674 return $w;
675 }
676 foreach (array_filter($w, 'is_array') as $sw) {
677 if ($m = match_self($sw)) {
678 return $m;
679 }
680 }
681 }
682
683 return false;
684 }
685
686 /**
687 * Remplace une condition décrivant une sous requête par son code
688 *
689 * @param array|string $w
690 * Description d'une condition WHERE de boucle (ou une partie de cette description)
691 * qui possède une description de sous-requête
692 * @param string $sousrequete
693 * Code PHP de la sous requête (qui doit remplacer la description)
694 * @return array|string
695 * Tableau de description du WHERE dont la description de sous-requête
696 * est remplacée par son code.
697 **/
698 function remplace_sous_requete($w, $sousrequete) {
699 if (is_array($w)) {
700 if (in_array(reset($w), array("SELF", "SUBSELECT"))) {
701 return $sousrequete;
702 }
703 foreach ($w as $k => $sw) {
704 $w[$k] = remplace_sous_requete($sw, $sousrequete);
705 }
706 }
707
708 return $w;
709 }
710
711 /**
712 * Sépare les conditions de boucles simples de celles possédant des sous-requêtes.
713 *
714 * @param array $where
715 * Description d'une condition WHERE de boucle
716 * @return array
717 * Liste de 2 tableaux :
718 * - Conditions simples (ne possédant pas de sous requêtes)
719 * - Conditions avec des sous requêtes
720 **/
721 function trouver_sous_requetes($where) {
722 $where_simples = array();
723 $where_sous = array();
724 foreach ($where as $k => $w) {
725 if (match_self($w)) {
726 $where_sous[$k] = $w;
727 } else {
728 $where_simples[$k] = $w;
729 }
730 }
731
732 return array($where_simples, $where_sous);
733 }
734
735
736 /**
737 * Calcule une requête et l’exécute
738 *
739 * Cette fonction est présente dans les squelettes compilés.
740 * Elle peut permettre de générer des requêtes avec jointure.
741 *
742 * @param array $select
743 * @param array $from
744 * @param array $from_type
745 * @param array $where
746 * @param array $join
747 * @param array $groupby
748 * @param array $orderby
749 * @param string $limit
750 * @param array $having
751 * @param string $table
752 * @param string $id
753 * @param string $serveur
754 * @param bool $requeter
755 * @return resource
756 */
757 function calculer_select(
758 $select = array(),
759 $from = array(),
760 $from_type = array(),
761 $where = array(),
762 $join = array(),
763 $groupby = array(),
764 $orderby = array(),
765 $limit = '',
766 $having = array(),
767 $table = '',
768 $id = '',
769 $serveur = '',
770 $requeter = true
771 ) {
772
773 // retirer les criteres vides:
774 // {X ?} avec X absent de l'URL
775 // {par #ENV{X}} avec X absent de l'URL
776 // IN sur collection vide (ce dernier devrait pouvoir etre fait a la compil)
777 $menage = false;
778 foreach ($where as $k => $v) {
779 if (is_array($v)) {
780 if ((count($v) >= 2) && ($v[0] == 'REGEXP') && ($v[2] == "'.*'")) {
781 $op = false;
782 } elseif ((count($v) >= 2) && ($v[0] == 'LIKE') && ($v[2] == "'%'")) {
783 $op = false;
784 } else {
785 $op = $v[0] ? $v[0] : $v;
786 }
787 } else {
788 $op = $v;
789 }
790 if ((!$op) or ($op == 1) or ($op == '0=0')) {
791 unset($where[$k]);
792 $menage = true;
793 }
794 }
795
796 // evacuer les eventuels groupby vide issus d'un calcul dynamique
797 $groupby = array_diff($groupby, array(''));
798
799 // remplacer les sous requetes recursives au calcul
800 list($where_simples, $where_sous) = trouver_sous_requetes($where);
801 foreach ($where_sous as $k => $w) {
802 $menage = true;
803 // on recupere la sous requete
804 $sous = match_self($w);
805 if ($sous[0] == 'SELF') {
806 // c'est une sous requete identique a elle meme sous la forme (SELF,$select,$where)
807 array_push($where_simples, $sous[2]);
808 $wheresub = array(
809 $sous[2],
810 '0=0'
811 ); // pour accepter une string et forcer a faire le menage car on a surement simplifie select et where
812 $jsub = $join;
813 // trouver les jointures utiles a
814 // reinjecter dans le where de la sous requete les conditions supplementaires des jointures qui y sont mentionnees
815 // ie L1.objet='article'
816 // on construit le where une fois, puis on ajoute les where complentaires si besoin, et on reconstruit le where en fonction
817 $i = 0;
818 do {
819 $where[$k] = remplace_sous_requete($w, "(" . calculer_select(
820 array($sous[1] . " AS id"),
821 $from,
822 $from_type,
823 $wheresub,
824 $jsub,
825 array(), array(), '',
826 $having, $table, $id, $serveur, false) . ")");
827 if (!$i) {
828 $i = 1;
829 $wherestring = calculer_where_to_string($where[$k]);
830 foreach ($join as $cle => $wj) {
831 if (count($wj) == 4
832 and strpos($wherestring, "{$cle}.") !== false
833 ) {
834 $i = 0;
835 $wheresub[] = $wj[3];
836 unset($jsub[$cle][3]);
837 }
838 }
839 }
840 } while ($i++ < 1);
841 }
842 if ($sous[0] == 'SUBSELECT') {
843 // c'est une sous requete explicite sous la forme identique a sql_select : (SUBSELECT,$select,$from,$where,$groupby,$orderby,$limit,$having)
844 array_push($where_simples, $sous[3]); // est-ce utile dans ce cas ?
845 $where[$k] = remplace_sous_requete($w, "(" . calculer_select(
846 $sous[1], # select
847 $sous[2], #from
848 array(), #from_type
849 $sous[3] ? (is_array($sous[3]) ? $sous[3] : array($sous[3])) : array(),
850 #where, qui peut etre de la forme string comme dans sql_select
851 array(), #join
852 $sous[4] ? $sous[4] : array(), #groupby
853 $sous[5] ? $sous[5] : array(), #orderby
854 $sous[6], #limit
855 $sous[7] ? $sous[7] : array(), #having
856 $table, $id, $serveur, false
857 ) . ")");
858 }
859 array_pop($where_simples);
860 }
861
862 foreach ($having as $k => $v) {
863 if ((!$v) or ($v == 1) or ($v == '0=0')) {
864 unset($having[$k]);
865 }
866 }
867
868 // Installer les jointures.
869 // Retirer celles seulement utiles aux criteres finalement absents mais
870 // parcourir de la plus recente a la moins recente pour pouvoir eliminer Ln
871 // si elle est seulement utile a Ln+1 elle meme inutile
872
873 $afrom = array();
874 $equiv = array();
875 $k = count($join);
876 foreach (array_reverse($join, true) as $cledef => $j) {
877 $cle = $cledef;
878 // le format de join est :
879 // array(table depart, cle depart [,cle arrivee[,condition optionnelle and ...]])
880 $join[$cle] = array_values($join[$cle]); // recalculer les cles car des unset ont pu perturber
881 if (count($join[$cle]) == 2) {
882 $join[$cle][] = $join[$cle][1];
883 }
884 if (count($join[$cle]) == 3) {
885 $join[$cle][] = '';
886 }
887 list($t, $c, $carr, $and) = $join[$cle];
888 // si le nom de la jointure n'a pas ete specifiee, on prend Lx avec x sont rang dans la liste
889 // pour compat avec ancienne convention
890 if (is_numeric($cle)) {
891 $cle = "L$k";
892 }
893 if (!$menage
894 or isset($afrom[$cle])
895 or calculer_jointnul($cle, $select)
896 or calculer_jointnul($cle, array_diff_key($join, array($cle => $join[$cle])))
897 or calculer_jointnul($cle, $having)
898 or calculer_jointnul($cle, $where_simples)
899 ) {
900 // corriger les references non explicites dans select
901 // ou groupby
902 foreach ($select as $i => $s) {
903 if ($s == $c) {
904 $select[$i] = "$cle.$c AS $c";
905 break;
906 }
907 }
908 foreach ($groupby as $i => $g) {
909 if ($g == $c) {
910 $groupby[$i] = "$cle.$c";
911 break;
912 }
913 }
914 // on garde une ecriture decomposee pour permettre une simplification ulterieure si besoin
915 // sans recours a preg_match
916 // un implode(' ',..) est fait dans reinjecte_joint un peu plus bas
917 $afrom[$t][$cle] = array(
918 "\n" .
919 (isset($from_type[$cle]) ? $from_type[$cle] : "INNER") . " JOIN",
920 $from[$cle],
921 "AS $cle",
922 "ON (",
923 "$cle.$c",
924 "=",
925 "$t.$carr",
926 ($and ? "AND " . $and : "") .
927 ")"
928 );
929 if (isset($afrom[$cle])) {
930 $afrom[$t] = $afrom[$t] + $afrom[$cle];
931 unset($afrom[$cle]);
932 }
933 $equiv[] = $carr;
934 } else {
935 unset($join[$cledef]);
936 }
937 unset($from[$cle]);
938 $k--;
939 }
940
941 if (count($afrom)) {
942 // Regarder si la table principale ne sert finalement a rien comme dans
943 //<BOUCLE3(MOTS){id_article}{id_mot}> class='on'</BOUCLE3>
944 //<BOUCLE2(MOTS){id_article} />#TOTAL_BOUCLE<//B2>
945 //<BOUCLE5(RUBRIQUES){id_mot}{tout} />#TOTAL_BOUCLE<//B5>
946 // ou dans
947 //<BOUCLE8(HIERARCHIE){id_rubrique}{tout}{type='Squelette'}{inverse}{0,1}{lang_select=non} />#TOTAL_BOUCLE<//B8>
948 // qui comporte plusieurs jointures
949 // ou dans
950 // <BOUCLE6(ARTICLES){id_mot=2}{statut==.*} />#TOTAL_BOUCLE<//B6>
951 // <BOUCLE7(ARTICLES){id_mot>0}{statut?} />#TOTAL_BOUCLE<//B7>
952 // penser a regarder aussi la clause orderby pour ne pas simplifier abusivement
953 // <BOUCLE9(ARTICLES){recherche truc}{par titre}>#ID_ARTICLE</BOUCLE9>
954 // penser a regarder aussi la clause groubpy pour ne pas simplifier abusivement
955 // <BOUCLE10(EVENEMENTS){id_rubrique} />#TOTAL_BOUCLE<//B10>
956
957 list($t, $c) = each($from);
958 reset($from);
959 $e = '/\b(' . "$t\\." . join("|" . $t . '\.', $equiv) . ')\b/';
960 if (!(strpos($t, ' ') or // jointure des le depart cf boucle_doc
961 calculer_jointnul($t, $select, $e) or
962 calculer_jointnul($t, $join, $e) or
963 calculer_jointnul($t, $where, $e) or
964 calculer_jointnul($t, $orderby, $e) or
965 calculer_jointnul($t, $groupby, $e) or
966 calculer_jointnul($t, $having, $e))
967 && count($afrom[$t])
968 ) {
969 reset($afrom[$t]);
970 list($nt, $nfrom) = each($afrom[$t]);
971 unset($from[$t]);
972 $from[$nt] = $nfrom[1];
973 unset($afrom[$t][$nt]);
974 $afrom[$nt] = $afrom[$t];
975 unset($afrom[$t]);
976 $e = '/\b' . preg_quote($nfrom[6]) . '\b/';
977 $t = $nfrom[4];
978 $alias = "";
979 // verifier que les deux cles sont homonymes, sinon installer un alias dans le select
980 $oldcle = explode('.', $nfrom[6]);
981 $oldcle = end($oldcle);
982 $newcle = explode('.', $nfrom[4]);
983 $newcle = end($newcle);
984 if ($newcle != $oldcle) {
985 // si l'ancienne cle etait deja dans le select avec un AS
986 // reprendre simplement ce AS
987 $as = '/\b' . preg_quote($nfrom[6]) . '\s+(AS\s+\w+)\b/';
988 if (preg_match($as, implode(',', $select), $m)) {
989 $alias = "";
990 } else {
991 $alias = ", " . $nfrom[4] . " AS $oldcle";
992 }
993 }
994 $select = remplacer_jointnul($t . $alias, $select, $e);
995 $join = remplacer_jointnul($t, $join, $e);
996 $where = remplacer_jointnul($t, $where, $e);
997 $having = remplacer_jointnul($t, $having, $e);
998 $groupby = remplacer_jointnul($t, $groupby, $e);
999 $orderby = remplacer_jointnul($t, $orderby, $e);
1000 }
1001 $from = reinjecte_joint($afrom, $from);
1002 }
1003 $GLOBALS['debug']['aucasou'] = array($table, $id, $serveur, $requeter);
1004 $r = sql_select($select, $from, $where,
1005 $groupby, array_filter($orderby), $limit, $having, $serveur, $requeter);
1006 unset($GLOBALS['debug']['aucasou']);
1007
1008 return $r;
1009 }
1010
1011 /**
1012 * Analogue a calculer_mysql_expression et autre (a unifier ?)
1013 *
1014 * @param string|array $v
1015 * @param string $join
1016 * @return string
1017 */
1018 function calculer_where_to_string($v, $join = 'AND') {
1019 if (empty($v)) {
1020 return '';
1021 }
1022
1023 if (!is_array($v)) {
1024 return $v;
1025 } else {
1026 $exp = "";
1027 if (strtoupper($join) === 'AND') {
1028 return $exp . join(" $join ", array_map('calculer_where_to_string', $v));
1029 } else {
1030 return $exp . join($join, $v);
1031 }
1032 }
1033 }
1034
1035
1036 //condition suffisante (mais non necessaire) pour qu'une table soit utile
1037
1038 // http://code.spip.net/@calculer_jointnul
1039 function calculer_jointnul($cle, $exp, $equiv = '') {
1040 if (!is_array($exp)) {
1041 if ($equiv) {
1042 $exp = preg_replace($equiv, '', $exp);
1043 }
1044
1045 return preg_match("/\\b$cle\\./", $exp);
1046 } else {
1047 foreach ($exp as $v) {
1048 if (calculer_jointnul($cle, $v, $equiv)) {
1049 return true;
1050 }
1051 }
1052
1053 return false;
1054 }
1055 }
1056
1057 // http://code.spip.net/@reinjecte_joint
1058 function reinjecte_joint($afrom, $from) {
1059 $from_synth = array();
1060 foreach ($from as $k => $v) {
1061 $from_synth[$k] = $from[$k];
1062 if (isset($afrom[$k])) {
1063 foreach ($afrom[$k] as $kk => $vv) {
1064 $afrom[$k][$kk] = implode(' ', $afrom[$k][$kk]);
1065 }
1066 $from_synth["$k@"] = implode(' ', $afrom[$k]);
1067 unset($afrom[$k]);
1068 }
1069 }
1070
1071 return $from_synth;
1072 }
1073
1074 // http://code.spip.net/@remplacer_jointnul
1075 function remplacer_jointnul($cle, $exp, $equiv = '') {
1076 if (!is_array($exp)) {
1077 return preg_replace($equiv, $cle, $exp);
1078 } else {
1079 foreach ($exp as $k => $v) {
1080 $exp[$k] = remplacer_jointnul($cle, $v, $equiv);
1081 }
1082
1083 return $exp;
1084 }
1085 }
1086
1087 // calcul du nom du squelette
1088 // http://code.spip.net/@calculer_nom_fonction_squel
1089 function calculer_nom_fonction_squel($skel, $mime_type = 'html', $connect = '') {
1090 // ne pas doublonner les squelette selon qu'ils sont calcules depuis ecrire/ ou depuis la racine
1091 if ($l = strlen(_DIR_RACINE) and strncmp($skel, _DIR_RACINE, $l) == 0) {
1092 $skel = substr($skel, strlen(_DIR_RACINE));
1093 }
1094
1095 return $mime_type
1096 . (!$connect ? '' : preg_replace('/\W/', "_", $connect)) . '_'
1097 . md5($GLOBALS['spip_version_code'] . ' * ' . $skel . (isset($GLOBALS['marqueur_skel']) ? '*' . $GLOBALS['marqueur_skel'] : ''));
1098 }