7781fd6b56fdc74f08ffad292b091c18370e6e40
[lhc/web/www.git] / www / ecrire / public / iterateur.php
1 <?php
2
3
4 /***************************************************************************\
5 * SPIP, Systeme de publication pour l'internet *
6 * *
7 * Copyright (c) 2001-2019 *
8 * Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James *
9 * *
10 * Ce programme est un logiciel libre distribue sous licence GNU/GPL. *
11 * Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne. *
12 \***************************************************************************/
13
14 if (!defined('_ECRIRE_INC_VERSION')) {
15 return;
16 }
17
18 /**
19 * Fabrique d'iterateur
20 * permet de charger n'importe quel iterateur IterateurXXX
21 * fourni dans le fichier iterateurs/xxx.php
22 *
23 */
24 class IterFactory {
25 public static function create($iterateur, $command, $info = null) {
26
27 // cas des SI {si expression} analises tres tot
28 // pour eviter le chargement de tout iterateur
29 if (isset($command['si'])) {
30 foreach ($command['si'] as $si) {
31 if (!$si) {
32 // $command pour boucle SQL peut generer des erreurs de compilation
33 // s'il est transmis alors qu'on est dans un iterateur vide
34 return new IterDecorator(new EmptyIterator(), array(), $info);
35 }
36 }
37 }
38
39 // chercher un iterateur PHP existant (par exemple dans SPL)
40 // (il faudrait passer l'argument ->sql_serveur
41 // pour etre certain qu'on est sur un "php:")
42 if (class_exists($iterateur)) {
43 $a = isset($command['args']) ? $command['args'] : array();
44
45 // permettre de passer un Iterateur directement {args #ITERATEUR} :
46 // si on recoit deja un iterateur en argument, on l'utilise
47 if (count($a) == 1 and is_object($a[0]) and is_subclass_of($a[0], 'Iterator')) {
48 $iter = $a[0];
49
50 // sinon, on cree un iterateur du type donne
51 } else {
52 // arguments de creation de l'iterateur...
53 // (pas glop)
54 try {
55 switch (count($a)) {
56 case 0:
57 $iter = new $iterateur();
58 break;
59 case 1:
60 $iter = new $iterateur($a[0]);
61 break;
62 case 2:
63 $iter = new $iterateur($a[0], $a[1]);
64 break;
65 case 3:
66 $iter = new $iterateur($a[0], $a[1], $a[2]);
67 break;
68 case 4:
69 $iter = new $iterateur($a[0], $a[1], $a[2], $a[3]);
70 break;
71 }
72 } catch (Exception $e) {
73 spip_log("Erreur de chargement de l'iterateur $iterateur");
74 spip_log($e->getMessage());
75 $iter = new EmptyIterator();
76 }
77 }
78 } else {
79 // chercher la classe d'iterateur
80 // IterateurXXX
81 // definie dans le fichier iterateurs/xxx.php
82 $class = "Iterateur" . $iterateur;
83 if (!class_exists($class)) {
84 if (!include_spip("iterateur/" . strtolower($iterateur))
85 or !class_exists($class)
86 ) {
87 die("Iterateur $iterateur non trouv&#233;");
88 // si l'iterateur n'existe pas, on se rabat sur le generique
89 # $iter = new EmptyIterator();
90 }
91 }
92 $iter = new $class($command, $info);
93 }
94
95 return new IterDecorator($iter, $command, $info);
96 }
97 }
98
99
100 class IterDecorator extends FilterIterator {
101 private $iter;
102
103 /**
104 * Conditions de filtrage
105 * ie criteres de selection
106 *
107 * @var array
108 */
109 protected $filtre = array();
110
111 /**
112 * Fonction de filtrage compilee a partir des criteres de filtre
113 *
114 * @var string
115 */
116 protected $func_filtre = null;
117
118 /**
119 * Critere {offset, limit}
120 *
121 * @var int
122 * @var int
123 */
124 protected $offset = null;
125 protected $limit = null;
126
127 /**
128 * nombre d'elements recuperes depuis la position 0,
129 * en tenant compte des filtres
130 *
131 * @var int
132 */
133 protected $fetched = 0;
134
135 /**
136 * Y a t'il une erreur ?
137 *
138 * @var bool
139 **/
140 protected $err = false;
141
142 /**
143 * Drapeau a activer en cas d'echec
144 * (select SQL errone, non chargement des DATA, etc)
145 */
146 public function err() {
147 if (method_exists($this->iter, 'err')) {
148 return $this->iter->err();
149 }
150 if (property_exists($this->iter, 'err')) {
151 return $this->iter->err;
152 }
153
154 return false;
155 }
156
157 public function __construct(Iterator $iter, $command, $info) {
158 parent::__construct($iter);
159 parent::rewind(); // remettre a la premiere position (bug? connu de FilterIterator)
160
161 // recuperer l'iterateur transmis
162 $this->iter = $this->getInnerIterator();
163 $this->command = $command;
164 $this->info = $info;
165 $this->pos = 0;
166 $this->fetched = 0;
167
168 // chercher la liste des champs a retourner par
169 // fetch si l'objet ne les calcule pas tout seul
170 if (!method_exists($this->iter, 'fetch')) {
171 $this->calculer_select();
172 $this->calculer_filtres();
173 }
174
175 // emptyIterator critere {si} faux n'a pas d'erreur !
176 if (isset($this->iter->err)) {
177 $this->err = $this->iter->err;
178 }
179
180 // pas d'init a priori, le calcul ne sera fait qu'en cas de besoin (provoque une double requete souvent inutile en sqlite)
181 //$this->total = $this->count();
182 }
183
184
185 // calcule les elements a retournes par fetch()
186 // enleve les elements inutiles du select()
187 //
188 private function calculer_select() {
189 if ($select = &$this->command['select']) {
190 foreach ($select as $s) {
191 // /!\ $s = '.nom'
192 if ($s[0] == '.') {
193 $s = substr($s, 1);
194 }
195 $this->select[] = $s;
196 }
197 }
198 }
199
200 // recuperer la valeur d'une balise #X
201 // en fonction des methodes
202 // et proprietes disponibles
203 public function get_select($nom) {
204 if (is_object($this->iter)
205 and method_exists($this->iter, $nom)
206 ) {
207 try {
208 return $this->iter->$nom();
209 } catch (Exception $e) {
210 // #GETCHILDREN sur un fichier de DirectoryIterator ...
211 spip_log("Methode $nom en echec sur " . get_class($this->iter));
212 spip_log("Cela peut être normal : retour d'une ligne de resultat ne pouvant pas calculer cette methode");
213
214 return '';
215 }
216 }
217 /*
218 if (property_exists($this->iter, $nom)) {
219 return $this->iter->$nom;
220 }*/
221 // cle et valeur par defaut
222 // ICI PLANTAGE SI ON NE CONTROLE PAS $nom
223 if (in_array($nom, array('cle', 'valeur'))
224 and method_exists($this, $nom)
225 ) {
226 return $this->$nom();
227 }
228
229 // Par defaut chercher en xpath dans la valeur()
230 return table_valeur($this->valeur(), $nom, null);
231 }
232
233
234 private function calculer_filtres() {
235
236 // Issu de calculer_select() de public/composer L.519
237 // TODO: externaliser...
238 //
239 // retirer les criteres vides:
240 // {X ?} avec X absent de l'URL
241 // {par #ENV{X}} avec X absent de l'URL
242 // IN sur collection vide (ce dernier devrait pouvoir etre fait a la compil)
243 if ($where = &$this->command['where']) {
244 $menage = false;
245 foreach ($where as $k => $v) {
246 if (is_array($v)) {
247 if ((count($v) >= 2) && ($v[0] == 'REGEXP') && ($v[2] == "'.*'")) {
248 $op = false;
249 } elseif ((count($v) >= 2) && ($v[0] == 'LIKE') && ($v[2] == "'%'")) {
250 $op = false;
251 } else {
252 $op = $v[0] ? $v[0] : $v;
253 }
254 } else {
255 $op = $v;
256 }
257 if ((!$op) or ($op == 1) or ($op == '0=0')) {
258 unset($where[$k]);
259 $menage = true;
260 }
261 // traiter {cle IN a,b} ou {valeur !IN a,b}
262 // prendre en compte le cas particulier de sous-requetes
263 // produites par sql_in quand plus de 255 valeurs passees a IN
264 if (preg_match_all(',\s+IN\s+(\(.*\)),', $op, $s_req)) {
265 $req = '';
266 foreach ($s_req[1] as $key => $val) {
267 $req .= trim($val, '(,)') . ',';
268 }
269 $req = '(' . rtrim($req, ',') . ')';
270 }
271 if (preg_match(',^\(\(([\w/]+)(\s+NOT)?\s+IN\s+(\(.*\))\)(?:\s+(AND|OR)\s+\(([\w/]+)(\s+NOT)?\s+IN\s+(\(.*\))\))*\)$,',
272 $op, $regs)) {
273 $this->ajouter_filtre($regs[1], 'IN', strlen($req) ? $req : $regs[3], $regs[2]);
274 unset($op);
275 }
276 }
277 foreach ($where as $k => $v) {
278 // 3 possibilites : count($v) =
279 // * 1 : {x y} ; on recoit $v[0] = y
280 // * 2 : {x !op y} ; on recoit $v[0] = 'NOT', $v[1] = array() // array du type {x op y}
281 // * 3 : {x op y} ; on recoit $v[0] = 'op', $v[1] = x, $v[2] = y
282
283 // 1 : forcement traite par un critere, on passe
284 if (count($v) == 1) {
285 continue;
286 }
287 if (count($v) == 2 and is_array($v[1])) {
288 $this->ajouter_filtre($v[1][1], $v[1][0], $v[1][2], 'NOT');
289 }
290 if (count($v) == 3) {
291 $this->ajouter_filtre($v[1], $v[0], $v[2]);
292 }
293 }
294 }
295
296 // critere {2,7}
297 if (isset($this->command['limit']) and $this->command['limit']) {
298 $limit = explode(',', $this->command['limit']);
299 $this->offset = $limit[0];
300 $this->limit = $limit[1];
301 }
302
303 // Creer la fonction de filtrage sur $this
304 if ($this->filtre) {
305 $this->func_filtre = create_function('$me', $b = 'return (' . join(') AND (', $this->filtre) . ');');
306 }
307 }
308
309
310 protected function ajouter_filtre($cle, $op, $valeur, $not = false) {
311 if (method_exists($this->iter, 'exception_des_criteres')) {
312 if (in_array($cle, $this->iter->exception_des_criteres())) {
313 return;
314 }
315 }
316 // TODO: analyser le filtre pour refuser ce qu'on ne sait pas traiter ?
317 # mais c'est normalement deja opere par calculer_critere_infixe()
318 # qui regarde la description 'desc' (en casse reelle d'ailleurs : {isDir=1}
319 # ne sera pas vu si l'on a defini desc['field']['isdir'] pour que #ISDIR soit present.
320 # il faudrait peut etre definir les 2 champs isDir et isdir... a reflechir...
321
322 # if (!in_array($cle, array('cle', 'valeur')))
323 # return;
324
325 $a = '$me->get_select(\'' . $cle . '\')';
326
327 $filtre = '';
328
329 if ($op == 'REGEXP') {
330 $filtre = 'match(' . $a . ', ' . str_replace('\"', '"', $valeur) . ')';
331 $op = '';
332 } else {
333 if ($op == 'LIKE') {
334 $valeur = str_replace(array('\"', '_', '%'), array('"', '.', '.*'), preg_quote($valeur));
335 $filtre = 'match(' . $a . ', ' . $valeur . ')';
336 $op = '';
337 } else {
338 if ($op == '=') {
339 $op = '==';
340 } else {
341 if ($op == 'IN') {
342 $filtre = 'in_array(' . $a . ', array' . $valeur . ')';
343 $op = '';
344 } else {
345 if (!in_array($op, array('<', '<=', '>', '>='))) {
346 spip_log('operateur non reconnu ' . $op); // [todo] mettre une erreur de squelette
347 $op = '';
348 }
349 }
350 }
351 }
352 }
353
354 if ($op) {
355 $filtre = $a . $op . str_replace('\"', '"', $valeur);
356 }
357
358 if ($not) {
359 $filtre = "!($filtre)";
360 }
361
362 if ($filtre) {
363 $this->filtre[] = $filtre;
364 }
365 }
366
367
368 public function next() {
369 $this->pos++;
370 parent::next();
371 }
372
373 /**
374 * revient au depart
375 *
376 * @return void
377 */
378 public function rewind() {
379 $this->pos = 0;
380 $this->fetched = 0;
381 parent::rewind();
382 }
383
384
385 # Extension SPIP des iterateurs PHP
386 /**
387 * type de l'iterateur
388 *
389 * @var string
390 */
391 protected $type;
392
393 /**
394 * parametres de l'iterateur
395 *
396 * @var array
397 */
398 protected $command;
399
400 /**
401 * infos de compilateur
402 *
403 * @var array
404 */
405 protected $info;
406
407 /**
408 * position courante de l'iterateur
409 *
410 * @var int
411 */
412 protected $pos = null;
413
414 /**
415 * nombre total resultats dans l'iterateur
416 *
417 * @var int
418 */
419 protected $total = null;
420
421 /**
422 * nombre maximal de recherche pour $total
423 * si l'iterateur n'implemente pas de fonction specifique
424 */
425 protected $max = 100000;
426
427
428 /**
429 * Liste des champs a inserer dans les $row
430 * retournes par ->fetch()
431 */
432 protected $select = array();
433
434
435 /**
436 * aller a la position absolue n,
437 * comptee depuis le debut
438 *
439 * @param int $n
440 * absolute pos
441 * @param string $continue
442 * param for sql_ api
443 * @return bool
444 * success or fail if not implemented
445 */
446 public function seek($n = 0, $continue = null) {
447 if ($this->func_filtre or !method_exists($this->iter, 'seek') or !$this->iter->seek($n)) {
448 $this->seek_loop($n);
449 }
450 $this->pos = $n;
451 $this->fetched = $n;
452
453 return true;
454 }
455
456 /*
457 * aller a la position $n en parcourant
458 * un par un tous les elements
459 */
460 private function seek_loop($n) {
461 if ($this->pos > $n) {
462 $this->rewind();
463 }
464
465 while ($this->pos < $n and $this->valid()) {
466 $this->next();
467 }
468
469 return true;
470 }
471
472 /**
473 * Avancer de $saut pas
474 *
475 * @param $saut
476 * @param $max
477 * @return int
478 */
479 public function skip($saut, $max = null) {
480 // pas de saut en arriere autorise pour cette fonction
481 if (($saut = intval($saut)) <= 0) {
482 return $this->pos;
483 }
484 $seek = $this->pos + $saut;
485 // si le saut fait depasser le maxi, on libere la resource
486 // et on sort
487 if (is_null($max)) {
488 $max = $this->count();
489 }
490
491 if ($seek >= $max or $seek >= $this->count()) {
492 // sortie plus rapide que de faire next() jusqu'a la fin !
493 $this->free();
494
495 return $max;
496 }
497
498 $this->seek($seek);
499
500 return $this->pos;
501 }
502
503 /**
504 * Renvoyer un tableau des donnees correspondantes
505 * a la position courante de l'iterateur
506 * en controlant si on respecte le filtre
507 * Appliquer aussi le critere {offset,limit}
508 *
509 * @return array|bool
510 */
511 public function fetch() {
512 if (method_exists($this->iter, 'fetch')) {
513 return $this->iter->fetch();
514 } else {
515
516 while ($this->valid()
517 and (
518 !$this->accept()
519 or (isset($this->offset) and $this->fetched++ < $this->offset)
520 )) {
521 $this->next();
522 }
523
524 if (!$this->valid()) {
525 return false;
526 }
527
528 if (isset($this->limit)
529 and $this->fetched > $this->offset + $this->limit
530 ) {
531 return false;
532 }
533
534 $r = array();
535 foreach ($this->select as $nom) {
536 $r[$nom] = $this->get_select($nom);
537 }
538 $this->next();
539
540 return $r;
541 }
542 }
543
544 // retourner la cle pour #CLE
545 public function cle() {
546 return $this->key();
547 }
548
549 // retourner la valeur pour #VALEUR
550 public function valeur() {
551 # attention PHP est mechant avec les objets, parfois il ne les
552 # clone pas proprement (directoryiterator sous php 5.2.2)
553 # on se rabat sur la version __toString()
554 if (is_object($v = $this->current())) {
555 if (method_exists($v, '__toString')) {
556 $v = $v->__toString();
557 } else {
558 $v = (array)$v;
559 }
560 }
561
562 return $v;
563 }
564
565 /**
566 * Accepte-t-on l'entree courante lue ?
567 * On execute les filtres pour le savoir.
568 **/
569 public function accept() {
570 if ($f = $this->func_filtre) {
571 return $f($this);
572 }
573
574 return true;
575 }
576
577 /**
578 * liberer la ressource
579 *
580 * @return bool
581 */
582 public function free() {
583 if (method_exists($this->iter, 'free')) {
584 $this->iter->free();
585 }
586 $this->pos = $this->total = 0;
587
588 return true;
589 }
590
591 /**
592 * Compter le nombre total de resultats
593 * pour #TOTAL_BOUCLE
594 *
595 * @return int
596 */
597 public function count() {
598 if (is_null($this->total)) {
599 if (method_exists($this->iter, 'count')
600 and !$this->func_filtre
601 ) {
602 return $this->total = $this->iter->count();
603 } else {
604 // compter les lignes et rembobiner
605 $total = 0;
606 $pos = $this->pos; // sauver la position
607 $this->rewind();
608 while ($this->fetch() and $total < $this->max) {
609 $total++;
610 }
611 $this->seek($pos);
612 $this->total = $total;
613 }
614 }
615
616 return $this->total;
617 }
618
619 }