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