96e3963191d9aa5de033c52cd59de98cb854340c
[lhc/web/www.git] / www / ecrire / iterateur / data.php
1 <?php
2
3 /***************************************************************************\
4 * SPIP, Systeme de publication pour l'internet *
5 * *
6 * Copyright (c) 2001-2017 *
7 * Arnaud Martin, Antoine Pitrou, Philippe Riviere, Emmanuel Saint-James *
8 * *
9 * Ce programme est un logiciel libre distribue sous licence GNU/GPL. *
10 * Pour plus de details voir le fichier COPYING.txt ou l'aide en ligne. *
11 \***************************************************************************/
12
13 /**
14 * Gestion de l'itérateur DATA
15 *
16 * @package SPIP\Core\Iterateur\DATA
17 **/
18
19 if (!defined('_ECRIRE_INC_VERSION')) {
20 return;
21 }
22
23 if (!defined('_DATA_SOURCE_MAX_SIZE')) {
24 define('_DATA_SOURCE_MAX_SIZE', 2 * 1048576);
25 }
26
27
28 /**
29 * Créer une boucle sur un itérateur DATA
30 *
31 * Annonce au compilateur les "champs" disponibles, c'est à dire
32 * 'cle', 'valeur' et '*' (tout nom possible).
33 *
34 * On ne peut effectivement pas connaître à la compilation la structure
35 * des données qui seront obtenues. On indique donc au compilateur que
36 * toute balise utilisée dans la boucle est possiblement un champ
37 * des données reçues.
38 *
39 * @param Boucle $b
40 * Description de la boucle
41 * @return Boucle
42 * Description de la boucle complétée des champs
43 */
44 function iterateur_DATA_dist($b) {
45 $b->iterateur = 'DATA'; # designe la classe d'iterateur
46 $b->show = array(
47 'field' => array(
48 'cle' => 'STRING',
49 'valeur' => 'STRING',
50 '*' => 'ALL' // Champ joker *
51 )
52 );
53 $b->select[] = '.valeur';
54
55 return $b;
56 }
57
58
59 /**
60 * Itérateur DATA
61 *
62 * Pour itérer sur des données quelconques (transformables en tableau)
63 */
64 class IterateurDATA implements Iterator {
65 /**
66 * tableau de donnees
67 *
68 * @var array
69 */
70 protected $tableau = array();
71
72 /**
73 * Conditions de filtrage
74 * ie criteres de selection
75 *
76 * @var array
77 */
78 protected $filtre = array();
79
80
81 /**
82 * Cle courante
83 *
84 * @var null
85 */
86 protected $cle = null;
87
88 /**
89 * Valeur courante
90 *
91 * @var null
92 */
93 protected $valeur = null;
94
95 /**
96 * Erreur presente ?
97 *
98 * @var bool
99 **/
100 public $err = false;
101
102 /**
103 * Calcul du total des elements
104 *
105 * @var int|null
106 **/
107 public $total = null;
108
109 /**
110 * Constructeur
111 *
112 * @param $command
113 * @param array $info
114 */
115 public function __construct($command, $info = array()) {
116 $this->type = 'DATA';
117 $this->command = $command;
118 $this->info = $info;
119
120 $this->select($command);
121 }
122
123 /**
124 * Revenir au depart
125 *
126 * @return void
127 */
128 public function rewind() {
129 reset($this->tableau);
130 list($this->cle, $this->valeur) = each($this->tableau);
131 }
132
133 /**
134 * Déclarer les critères exceptions
135 *
136 * @return array
137 */
138 public function exception_des_criteres() {
139 return array('tableau');
140 }
141
142 /**
143 * Récupérer depuis le cache si possible
144 *
145 * @param string $cle
146 * @return mixed
147 */
148 protected function cache_get($cle) {
149 if (!$cle) {
150 return;
151 }
152 # utiliser memoization si dispo
153 if (!function_exists('cache_get')) {
154 return;
155 }
156
157 return cache_get($cle);
158 }
159
160 /**
161 * Stocker en cache si possible
162 *
163 * @param string $cle
164 * @param int $ttl
165 * @param null|mixed $valeur
166 * @return bool
167 */
168 protected function cache_set($cle, $ttl, $valeur = null) {
169 if (!$cle) {
170 return;
171 }
172 if (is_null($valeur)) {
173 $valeur = $this->tableau;
174 }
175 # utiliser memoization si dispo
176 if (!function_exists('cache_set')) {
177 return;
178 }
179
180 return cache_set($cle,
181 array(
182 'data' => $valeur,
183 'time' => time(),
184 'ttl' => $ttl
185 ),
186 3600 + $ttl);
187 # conserver le cache 1h de plus que la validite demandee,
188 # pour le cas ou le serveur distant ne reponde plus
189 }
190
191 /**
192 * Aller chercher les données de la boucle DATA
193 *
194 * @throws Exception
195 * @param array $command
196 * @return void
197 */
198 protected function select($command) {
199
200 // l'iterateur DATA peut etre appele en passant (data:type)
201 // le type se retrouve dans la commande 'from'
202 // dans ce cas la le critere {source}, si present, n'a pas besoin du 1er argument
203 if (isset($this->command['from'][0])) {
204 if (isset($this->command['source']) and is_array($this->command['source'])) {
205 array_unshift($this->command['source'], $this->command['sourcemode']);
206 }
207 $this->command['sourcemode'] = $this->command['from'][0];
208 }
209
210 // cherchons differents moyens de creer le tableau de donnees
211 // les commandes connues pour l'iterateur DATA
212 // sont : {tableau #ARRAY} ; {cle=...} ; {valeur=...}
213
214 // {source format, [URL], [arg2]...}
215 if (isset($this->command['source'])
216 and isset($this->command['sourcemode'])
217 ) {
218 $this->select_source();
219 }
220
221 // Critere {liste X1, X2, X3}
222 if (isset($this->command['liste'])) {
223 $this->select_liste();
224 }
225 if (isset($this->command['enum'])) {
226 $this->select_enum();
227 }
228
229 // Si a ce stade on n'a pas de table, il y a un bug
230 if (!is_array($this->tableau)) {
231 $this->err = true;
232 spip_log("erreur datasource " . var_export($command, true));
233 }
234
235 // {datapath query.results}
236 // extraire le chemin "query.results" du tableau de donnees
237 if (!$this->err
238 and isset($this->command['datapath'])
239 and is_array($this->command['datapath'])
240 ) {
241 $this->select_datapath();
242 }
243
244 // tri {par x}
245 if ($this->command['orderby']) {
246 $this->select_orderby();
247 }
248
249 // grouper les resultats {fusion /x/y/z} ;
250 if ($this->command['groupby']) {
251 $this->select_groupby();
252 }
253
254 $this->rewind();
255 #var_dump($this->tableau);
256 }
257
258
259 /**
260 * Aller chercher les donnees de la boucle DATA
261 * depuis une source
262 * {source format, [URL], [arg2]...}
263 */
264 protected function select_source() {
265 # un peu crado : avant de charger le cache il faut charger
266 # les class indispensables, sinon PHP ne saura pas gerer
267 # l'objet en cache ; cf plugins/icalendar
268 # perf : pas de fonction table_to_array ! (table est deja un array)
269 if (isset($this->command['sourcemode'])
270 and !in_array($this->command['sourcemode'], array('table', 'array', 'tableau'))
271 ) {
272 charger_fonction($this->command['sourcemode'] . '_to_array', 'inc', true);
273 }
274
275 # le premier argument peut etre un array, une URL etc.
276 $src = $this->command['source'][0];
277
278 # avons-nous un cache dispo ?
279 $cle = null;
280 if (is_string($src)) {
281 $cle = 'datasource_' . md5($this->command['sourcemode'] . ':' . var_export($this->command['source'], true));
282 }
283
284 $cache = $this->cache_get($cle);
285 if (isset($this->command['datacache'])) {
286 $ttl = intval($this->command['datacache']);
287 }
288 if ($cache
289 and ($cache['time'] + (isset($ttl) ? $ttl : $cache['ttl'])
290 > time())
291 and !(_request('var_mode') === 'recalcul'
292 and include_spip('inc/autoriser')
293 and autoriser('recalcul')
294 )
295 ) {
296 $this->tableau = $cache['data'];
297 } else {
298 try {
299 # dommage que ca ne soit pas une option de yql_to_array...
300 if ($this->command['sourcemode'] == 'yql') {
301 if (!isset($ttl)) {
302 $ttl = 3600;
303 }
304 }
305
306 if (isset($this->command['sourcemode'])
307 and in_array($this->command['sourcemode'],
308 array('table', 'array', 'tableau'))
309 ) {
310 if (is_array($a = $src)
311 or (is_string($a)
312 and $a = str_replace('&quot;', '"', $a) # fragile!
313 and is_array($a = @unserialize($a)))
314 ) {
315 $this->tableau = $a;
316 }
317 } else {
318 if (tester_url_absolue($src)) {
319 include_spip('inc/distant');
320 $u = recuperer_page($src, false, false, _DATA_SOURCE_MAX_SIZE);
321 if (!$u) {
322 throw new Exception("404");
323 }
324 if (!isset($ttl)) {
325 $ttl = 24 * 3600;
326 }
327 } else {
328 if (@is_dir($src)) {
329 $u = $src;
330 if (!isset($ttl)) {
331 $ttl = 10;
332 }
333 } else {
334 if (@is_readable($src) && @is_file($src)) {
335 $u = spip_file_get_contents($src);
336 if (!isset($ttl)) {
337 $ttl = 10;
338 }
339 } else {
340 $u = $src;
341 if (!isset($ttl)) {
342 $ttl = 10;
343 }
344 }
345 }
346 }
347 if (!$this->err
348 and $g = charger_fonction($this->command['sourcemode'] . '_to_array', 'inc', true)
349 ) {
350 $args = $this->command['source'];
351 $args[0] = $u;
352 if (is_array($a = call_user_func_array($g, $args))) {
353 $this->tableau = $a;
354 }
355 }
356 }
357
358 if (!is_array($this->tableau)) {
359 $this->err = true;
360 }
361
362 if (!$this->err and isset($ttl) and $ttl > 0) {
363 $this->cache_set($cle, $ttl);
364 }
365
366 } catch (Exception $e) {
367 $e = $e->getMessage();
368 $err = sprintf("[%s, %s] $e",
369 $src,
370 $this->command['sourcemode']);
371 erreur_squelette(array($err, array()));
372 $this->err = true;
373 }
374 }
375
376 # en cas d'erreur, utiliser le cache si encore dispo
377 if ($this->err
378 and $cache
379 ) {
380 $this->tableau = $cache['data'];
381 $this->err = false;
382 }
383 }
384
385
386 /**
387 * Retourne un tableau donne depuis un critère liste
388 *
389 * Critère `{liste X1, X2, X3}`
390 *
391 * @see critere_DATA_liste_dist()
392 *
393 **/
394 protected function select_liste() {
395 # s'il n'y a qu'une valeur dans la liste, sans doute une #BALISE
396 if (!isset($this->command['liste'][1])) {
397 if (!is_array($this->command['liste'][0])) {
398 $this->command['liste'] = explode(',', $this->command['liste'][0]);
399 } else {
400 $this->command['liste'] = $this->command['liste'][0];
401 }
402 }
403 $this->tableau = $this->command['liste'];
404 }
405
406 /**
407 * Retourne un tableau donne depuis un critere liste
408 * Critere {enum Xmin, Xmax}
409 *
410 **/
411 protected function select_enum() {
412 # s'il n'y a qu'une valeur dans la liste, sans doute une #BALISE
413 if (!isset($this->command['enum'][1])) {
414 if (!is_array($this->command['enum'][0])) {
415 $this->command['enum'] = explode(',', $this->command['enum'][0]);
416 } else {
417 $this->command['enum'] = $this->command['enum'][0];
418 }
419 }
420 if (count($this->command['enum']) >= 3) {
421 $enum = range(array_shift($this->command['enum']), array_shift($this->command['enum']),
422 array_shift($this->command['enum']));
423 } else {
424 $enum = range(array_shift($this->command['enum']), array_shift($this->command['enum']));
425 }
426 $this->tableau = $enum;
427 }
428
429
430 /**
431 * extraire le chemin "query.results" du tableau de donnees
432 * {datapath query.results}
433 *
434 **/
435 protected function select_datapath() {
436 list(, $base) = each($this->command['datapath']);
437 if (strlen($base = ltrim(trim($base), "/"))) {
438 $this->tableau = table_valeur($this->tableau, $base);
439 if (!is_array($this->tableau)) {
440 $this->tableau = array();
441 $this->err = true;
442 spip_log("datapath '$base' absent");
443 }
444 }
445 }
446
447 /**
448 * Ordonner les resultats
449 * {par x}
450 *
451 **/
452 protected function select_orderby() {
453 $sortfunc = '';
454 $aleas = 0;
455 foreach ($this->command['orderby'] as $tri) {
456 // virer le / initial pour les criteres de la forme {par /xx}
457 if (preg_match(',^\.?([/\w]+)( DESC)?$,iS', ltrim($tri, '/'), $r)) {
458 $r = array_pad($r, 3, null);
459
460 // tri par cle
461 if ($r[1] == 'cle') {
462 if (isset($r[2]) and $r[2]) {
463 krsort($this->tableau);
464 } else {
465 ksort($this->tableau);
466 }
467 } # {par hasard}
468 else {
469 if ($r[1] == 'alea') {
470 $k = array_keys($this->tableau);
471 shuffle($k);
472 $v = array();
473 foreach ($k as $cle) {
474 $v[$cle] = $this->tableau[$cle];
475 }
476 $this->tableau = $v;
477 } else {
478 # {par valeur}
479 if ($r[1] == 'valeur') {
480 $tv = '%s';
481 } # {par valeur/xx/yy} ??
482 else {
483 $tv = 'table_valeur(%s, ' . var_export($r[1], true) . ')';
484 }
485 $sortfunc .= '
486 $a = ' . sprintf($tv, '$aa') . ';
487 $b = ' . sprintf($tv, '$bb') . ';
488 if ($a <> $b)
489 return ($a ' . ((isset($r[2]) and $r[2]) ? '>' : '<') . ' $b) ? -1 : 1;';
490 }
491 }
492 }
493 }
494
495 if ($sortfunc) {
496 uasort($this->tableau, create_function('$aa,$bb',
497 $sortfunc . '
498 return 0;'
499 ));
500 }
501 }
502
503
504 /**
505 * Grouper les resultats
506 * {fusion /x/y/z}
507 *
508 **/
509 protected function select_groupby() {
510 // virer le / initial pour les criteres de la forme {fusion /xx}
511 if (strlen($fusion = ltrim($this->command['groupby'][0], '/'))) {
512 $vu = array();
513 foreach ($this->tableau as $k => $v) {
514 $val = table_valeur($v, $fusion);
515 if (isset($vu[$val])) {
516 unset($this->tableau[$k]);
517 } else {
518 $vu[$val] = true;
519 }
520 }
521 }
522 }
523
524
525 /**
526 * L'iterateur est-il encore valide ?
527 *
528 * @return bool
529 */
530 public function valid() {
531 return !is_null($this->cle);
532 }
533
534 /**
535 * Retourner la valeur
536 *
537 * @return null
538 */
539 public function current() {
540 return $this->valeur;
541 }
542
543 /**
544 * Retourner la cle
545 *
546 * @return null
547 */
548 public function key() {
549 return $this->cle;
550 }
551
552 /**
553 * Passer a la valeur suivante
554 *
555 * @return void
556 */
557 public function next() {
558 if ($this->valid()) {
559 list($this->cle, $this->valeur) = each($this->tableau);
560 }
561 }
562
563 /**
564 * Compter le nombre total de resultats
565 *
566 * @return int
567 */
568 public function count() {
569 if (is_null($this->total)) {
570 $this->total = count($this->tableau);
571 }
572
573 return $this->total;
574 }
575 }
576
577 /*
578 * Fonctions de transformation donnee => tableau
579 */
580
581 /**
582 * file -> tableau
583 *
584 * @param string $u
585 * @return array
586 */
587 function inc_file_to_array_dist($u) {
588 return preg_split('/\r?\n/', $u);
589 }
590
591 /**
592 * plugins -> tableau
593 *
594 * @return unknown
595 */
596 function inc_plugins_to_array_dist() {
597 include_spip('inc/plugin');
598
599 return liste_chemin_plugin_actifs();
600 }
601
602 /**
603 * xml -> tableau
604 *
605 * @param string $u
606 * @return array
607 */
608 function inc_xml_to_array_dist($u) {
609 return @XMLObjectToArray(new SimpleXmlIterator($u));
610 }
611
612 /**
613 *
614 * object -> tableau
615 *
616 * @param object $object The object to convert
617 * @return array
618 *
619 */
620 function inc_object_to_array($object) {
621 if (!is_object($object) && !is_array($object)) {
622 return $object;
623 }
624 if (is_object($object)) {
625 $object = get_object_vars($object);
626 }
627
628 return array_map('inc_object_to_array', $object);
629 }
630
631 /**
632 * yql -> tableau
633 *
634 * @throws Exception
635 * @param string $u
636 * @return array|bool
637 */
638 function inc_yql_to_array_dist($u) {
639 define('_YQL_ENDPOINT', 'http://query.yahooapis.com/v1/public/yql?&env=store%3A%2F%2Fdatatables.org%2Falltableswithkeys&q=');
640 $v = recuperer_url($url = _YQL_ENDPOINT . urlencode($u) . '&format=json');
641 if (!$v['page']
642 or !$w = json_decode($v['page'], true)
643 ) {
644 throw new Exception('YQL: r&#233;ponse vide ou mal form&#233;e');
645 }
646 if (isset($w['error'])) {
647 throw new Exception($w['error']['description']);
648 }
649
650 return inc_object_to_array($w);
651 }
652
653 /**
654 * sql -> tableau
655 *
656 * @param string $u
657 * @return array|bool
658 */
659 function inc_sql_to_array_dist($u) {
660 # sortir le connecteur de $u
661 preg_match(',^(?:(\w+):)?(.*)$,S', $u, $v);
662 $serveur = (string)$v[1];
663 $req = trim($v[2]);
664 if ($s = sql_query($req, $serveur)) {
665 $r = array();
666 while ($t = sql_fetch($s)) {
667 $r[] = $t;
668 }
669
670 return $r;
671 }
672
673 return false;
674 }
675
676 /**
677 * json -> tableau
678 *
679 * @param string $u
680 * @return array|bool
681 */
682 function inc_json_to_array_dist($u) {
683 if (is_array($json = json_decode($u))
684 or is_object($json)
685 ) {
686 return (array)$json;
687 }
688 }
689
690 /**
691 * csv -> tableau
692 *
693 * @param string $u
694 * @return array|bool
695 */
696 function inc_csv_to_array_dist($u) {
697 include_spip('inc/csv');
698 list($entete, $csv) = analyse_csv($u);
699 array_unshift($csv, $entete);
700
701 include_spip('inc/charsets');
702 $i = 1;
703 foreach ($entete as $k => $v) {
704 if (trim($v) == "") {
705 $v = "col" . $i;
706 } // reperer des eventuelles cases vides
707 if (is_numeric($v) and $v < 0) {
708 $v = "__" . $v;
709 } // ne pas risquer d'ecraser une cle numerique
710 if (is_numeric($v)) {
711 $v = "_" . $v;
712 } // ne pas risquer d'ecraser une cle numerique
713 $v = strtolower(preg_replace(',\W+,', '_', translitteration($v)));
714 foreach ($csv as &$item) {
715 $item[$v] = &$item[$k];
716 }
717 $i++;
718 }
719
720 return $csv;
721 }
722
723 /**
724 * RSS -> tableau
725 *
726 * @param string $u
727 * @return array|bool
728 */
729 function inc_rss_to_array_dist($u) {
730 include_spip('inc/syndic');
731 if (is_array($rss = analyser_backend($u))) {
732 $tableau = $rss;
733 }
734
735 return $tableau;
736 }
737
738 /**
739 * atom, alias de rss -> tableau
740 *
741 * @param string $u
742 * @return array|bool
743 */
744 function inc_atom_to_array_dist($u) {
745 $g = charger_fonction('rss_to_array', 'inc');
746
747 return $g($u);
748 }
749
750 /**
751 * glob -> tableau
752 * lister des fichiers selon un masque, pour la syntaxe cf php.net/glob
753 *
754 * @param string $u
755 * @return array|bool
756 */
757 function inc_glob_to_array_dist($u) {
758 $a = glob($u,
759 GLOB_MARK | GLOB_NOSORT | GLOB_BRACE
760 );
761
762 return $a ? $a : array();
763 }
764
765 /**
766 * YAML -> tableau
767 *
768 * @param string $u
769 * @return bool|array
770 * @throws Exception
771 */
772 function inc_yaml_to_array_dist($u) {
773 include_spip('inc/yaml-mini');
774 if (!function_exists("yaml_decode")) {
775 throw new Exception('YAML: impossible de trouver la fonction yaml_decode');
776
777 return false;
778 }
779
780 return yaml_decode($u);
781 }
782
783
784 /**
785 * pregfiles -> tableau
786 * lister des fichiers a partir d'un dossier de base et selon une regexp.
787 * pour la syntaxe cf la fonction spip preg_files
788 *
789 * @param string $dir
790 * @param string $regexp
791 * @param int $limit
792 * @return array|bool
793 */
794 function inc_pregfiles_to_array_dist($dir, $regexp = -1, $limit = 10000) {
795 return (array)preg_files($dir, $regexp, $limit);
796 }
797
798 /**
799 * ls -> tableau
800 * ls : lister des fichiers selon un masque glob
801 * et renvoyer aussi leurs donnees php.net/stat
802 *
803 * @param string $u
804 * @return array|bool
805 */
806 function inc_ls_to_array_dist($u) {
807 $glob = charger_fonction('glob_to_array', 'inc');
808 $a = $glob($u);
809 foreach ($a as &$v) {
810 $b = (array)@stat($v);
811 foreach ($b as $k => $ignore) {
812 if (is_numeric($k)) {
813 unset($b[$k]);
814 }
815 }
816 $b['file'] = basename($v);
817 $v = array_merge(
818 pathinfo($v),
819 $b
820 );
821 }
822
823 return $a;
824 }
825
826 /**
827 * Object -> tableau
828 *
829 * @param Object $object
830 * @return array|bool
831 */
832 function XMLObjectToArray($object) {
833 $xml_array = array();
834 for ($object->rewind(); $object->valid(); $object->next()) {
835 if (array_key_exists($key = $object->key(), $xml_array)) {
836 $key .= '-' . uniqid();
837 }
838 $vars = get_object_vars($object->current());
839 if (isset($vars['@attributes'])) {
840 foreach ($vars['@attributes'] as $k => $v) {
841 $xml_array[$key][$k] = $v;
842 }
843 }
844 if ($object->hasChildren()) {
845 $xml_array[$key][] = XMLObjectToArray(
846 $object->current());
847 } else {
848 $xml_array[$key][] = strval($object->current());
849 }
850 }
851
852 return $xml_array;
853 }