Ajout : ./garradin
[garradin.git] / include / class.squelette.php
1 <?php
2
3 namespace Garradin;
4
5 require_once ROOT . '/include/libs/miniskel/class.miniskel.php';
6
7 class Squelette_Snippet
8 {
9 const TEXT = 0;
10 const PHP = 1;
11 const GUESS = 2;
12 const OBJ = 3;
13
14 protected $_content = [];
15
16 protected function _getType($type, $value)
17 {
18 if ($type == self::GUESS)
19 {
20 if ($value instanceof Squelette_Snippet)
21 return self::OBJ;
22 else
23 return self::TEXT;
24 }
25
26 return $type;
27 }
28
29 public function __construct($type = self::TEXT, $value = '')
30 {
31 $type = $this->_getType($type, $value);
32
33 if ($type == self::OBJ)
34 {
35 $this->_content = $value->get();
36 }
37 else
38 {
39 $this->_content[] = (string) (int) $type . $value;
40 }
41
42 unset($value);
43 }
44
45 public function prepend($type = self::TEXT, $value, $pos = false)
46 {
47 $type = $this->_getType($type, $value);
48
49 if ($type == self::OBJ)
50 {
51 if ($pos)
52 {
53 array_splice($this->_content, $pos, 0, $value->get());
54 }
55 else
56 {
57 $this->_content = array_merge($value->get(), $this->_content);
58 }
59 }
60 else
61 {
62 $value = (string) (int) $type . $value;
63
64 if ($pos)
65 {
66 array_splice($this->_content, $pos, 0, $value);
67 }
68 else
69 {
70 array_unshift($this->_content, $value);
71 }
72 }
73
74 unset($value);
75 }
76
77 public function append($type = self::TEXT, $value, $pos = false)
78 {
79 $type = $this->_getType($type, $value);
80
81 if ($type == self::OBJ)
82 {
83 if ($pos)
84 {
85 array_splice($this->_content, $pos + 1, 0, $value->get());
86 }
87 else
88 {
89 $this->_content = array_merge($this->_content, $value->get());
90 }
91 }
92 else
93 {
94 $value = (string) (int) $type . $value;
95
96 if ($pos)
97 {
98 array_splice($this->_content, $pos + 1, 0, $value);
99 }
100 else
101 {
102 array_push($this->_content, $value);
103 }
104 }
105
106 unset($value);
107 }
108
109 public function output($in_php = false)
110 {
111 $out = '';
112 $php = $in_php ?: false;
113
114 foreach ($this->_content as $line)
115 {
116 if ($line[0] == self::PHP && !$php)
117 {
118 $php = true;
119 $out .= '<?php ';
120 }
121 elseif ($line[0] == self::TEXT && $php)
122 {
123 $php = false;
124 $out .= ' ?>';
125 }
126
127 $out .= substr($line, 1);
128
129 if ($line[0] == self::PHP)
130 {
131 $out .= "\n";
132 }
133 }
134
135 if ($php && !$in_php)
136 {
137 $out .= ' ?>';
138 }
139
140 $this->_content = [];
141
142 return $out;
143 }
144
145 public function __toString()
146 {
147 return $this->output(false);
148 }
149
150 public function get()
151 {
152 return $this->_content;
153 }
154
155 public function replace($key, $type = self::TEXT, $value)
156 {
157 $type = $this->_getType($type, $value);
158
159 if ($type == self::OBJ)
160 {
161 array_splice($this->_content, $key, 1, $value->get());
162 }
163 else
164 {
165 $this->_content[$key] = (string) (int) $type . $value;
166 }
167
168 unset($value);
169 }
170 }
171
172 class Squelette extends \miniSkel
173 {
174 private $parent = null;
175 private $current = null;
176 private $_vars = [];
177
178 private function _registerDefaultModifiers()
179 {
180 foreach (Squelette_Filtres::$filtres_php as $func=>$name)
181 {
182 if (is_string($func))
183 $this->register_modifier($name, $func);
184 else
185 $this->register_modifier($name, $name);
186 }
187
188 foreach (get_class_methods('Garradin\Squelette_Filtres') as $name)
189 {
190 $this->register_modifier($name, ['Garradin\Squelette_Filtres', $name]);
191 }
192
193 foreach (Squelette_Filtres::$filtres_alias as $name=>$func)
194 {
195 $this->register_modifier($name, ['Garradin\Squelette_Filtres', $func]);
196 }
197 }
198
199 public function __construct()
200 {
201 $this->_registerDefaultModifiers();
202
203 $config = Config::getInstance();
204
205 $this->assign('nom_asso', $config->get('nom_asso'));
206 $this->assign('adresse_asso', $config->get('adresse_asso'));
207 $this->assign('email_asso', $config->get('email_asso'));
208 $this->assign('site_asso', $config->get('site_asso'));
209
210 $this->assign('url_racine', WWW_URL);
211 $this->assign('url_site', WWW_URL);
212 $this->assign('url_atom', WWW_URL . 'feed/atom/');
213 $this->assign('url_elements', WWW_URL . 'squelettes/');
214 $this->assign('url_admin', WWW_URL . 'admin/');
215 }
216
217 protected function processInclude($args)
218 {
219 if (empty($args))
220 throw new \miniSkelMarkupException("Le tag INCLURE demande à préciser le fichier à inclure.");
221
222 $file = key($args);
223
224 if (empty($file) || !preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $file))
225 throw new \miniSkelMarkupException("INCLURE: le nom de fichier ne peut contenir que des caractères alphanumériques.");
226
227 return new Squelette_Snippet(1, '$this->fetch("'.$file.'", false);');
228 }
229
230 protected function processVariable($name, $value, $applyDefault, $modifiers, $pre, $post, $context)
231 {
232 if ($context == self::CONTEXT_IN_ARG)
233 {
234 $out = new Squelette_Snippet(1, '$this->getVariable(\''.$name.'\')');
235
236 if ($pre)
237 {
238 $out->prepend(2, $pre);
239 }
240
241 if ($post)
242 {
243 $out->append(2, $post);
244 }
245
246 return $out;
247 }
248
249 $out = new Squelette_Snippet(1, '$value = $this->getVariable(\''.$name.'\');');
250
251 // We process modifiers
252 foreach ($modifiers as &$modifier)
253 {
254 if (!isset($this->modifiers[$modifier['name']]))
255 {
256 throw new \miniSkelMarkupException('Filtre '.$modifier['name'].' inconnu !');
257 }
258
259 $out->append(1, '$value = call_user_func_array('.var_export($this->modifiers[$modifier['name']], true).', [$value, ');
260
261 foreach ($modifier['arguments'] as $arg)
262 {
263 if ($arg == 'debut_liste')
264 {
265 $out->append(1, '$this->getVariable(\'debut_liste\')');
266 }
267 elseif ($arg instanceOf Squelette_Snippet)
268 {
269 $out->append(3, $arg);
270 }
271 else
272 {
273 //if (preg_match('!getVariable!', $arg)) throw new Exception("lol");
274 $out->append(1, '"'.str_replace('"', '\\"', $arg).'"');
275 }
276
277 $out->append(1, ', ');
278 }
279
280 $out->append(1, ']);');
281
282 if (in_array($modifier['name'], Squelette_Filtres::$desactiver_defaut))
283 {
284 $applyDefault = false;
285 }
286 }
287
288 if ($applyDefault)
289 {
290 $out->append(1, 'if (is_string($value) && trim($value)) $value = htmlspecialchars($value, ENT_QUOTES, \'UTF-8\', false);');
291 }
292
293 $out->append(1, 'if ($value === true || trim($value) !== \'\'):');
294
295 // Getting pre-content
296 if ($pre)
297 {
298 $out->append(2, $pre);
299 }
300
301 $out->append(1, 'echo is_bool($value) ? "" : $value;');
302
303 // Getting post-content
304 if ($post)
305 {
306 $out->append(2, $post);
307 }
308
309 $out->append(1, 'endif;');
310
311 return $out;
312 }
313
314 protected function processLoop($loopName, $loopType, $loopCriterias, $loopContent, $preContent, $postContent, $altContent)
315 {
316 if ($loopType != 'articles' && $loopType != 'rubriques' && $loopType != 'pages')
317 {
318 throw new \miniSkelMarkupException("Le type de boucle '".$loopType."' est inconnu.");
319 }
320
321 $loopStart = '';
322 $query = $where = $order = '';
323 $limit = $begin = 0;
324
325 $query = 'SELECT w.*, strftime(\\\'%s\\\', w.date_creation) AS date_creation, strftime(\\\'%s\\\', w.date_modification) AS date_modification';
326
327 if (trim($loopContent))
328 {
329 $query .= ', r.contenu AS texte FROM wiki_pages AS w LEFT JOIN wiki_revisions AS r ON (w.id = r.id_page AND w.revision = r.revision) ';
330 }
331 else
332 {
333 $query .= '\'\' AS texte ';
334 }
335
336 $where = 'WHERE w.droit_lecture = -1 ';
337
338 if ($loopType == 'articles')
339 {
340 $where .= 'AND (SELECT COUNT(id) FROM wiki_pages WHERE parent = w.id) = 0 ';
341 }
342 elseif ($loopType == 'rubriques')
343 {
344 $where .= 'AND (SELECT COUNT(id) FROM wiki_pages WHERE parent = w.id) > 0 ';
345 }
346
347 $allowed_fields = ['id', 'uri', 'titre', 'date', 'date_creation', 'date_modification',
348 'parent', 'rubrique', 'revision', 'points', 'recherche', 'texte'];
349 $search = $search_rank = false;
350
351 foreach ($loopCriterias as $criteria)
352 {
353 if (isset($criteria['field']))
354 {
355 if (!in_array($criteria['field'], $allowed_fields))
356 {
357 throw new \miniSkelMarkupException("Critère '".$criteria['field']."' invalide pour la boucle '$loopName' de type '$loopType'.");
358 }
359 elseif ($criteria['field'] == 'rubrique')
360 {
361 $criteria['field'] = 'parent';
362 }
363 elseif ($criteria['field'] == 'date')
364 {
365 $criteria['field'] = 'date_creation';
366 }
367 elseif ($criteria['field'] == 'points')
368 {
369 if ($criteria['action'] != \miniSkel::ACTION_ORDER_BY)
370 {
371 throw new \miniSkelMarkupException("Le critère 'points' n\'est pas valide dans ce contexte.");
372 }
373
374 $search_rank = true;
375 }
376 }
377
378 switch ($criteria['action'])
379 {
380 case \miniSkel::ACTION_ORDER_BY:
381 if (!$order)
382 $order = 'ORDER BY '.$criteria['field'].'';
383 else
384 $order .= ', '.$criteria['field'].'';
385 break;
386 case \miniSkel::ACTION_ORDER_DESC:
387 if ($order)
388 $order .= ' DESC';
389 break;
390 case \miniSkel::ACTION_LIMIT:
391 $begin = $criteria['begin'];
392 $limit = $criteria['number'];
393 break;
394 case \miniSkel::ACTION_MATCH_FIELD_BY_VALUE:
395 $where .= ' AND '.$criteria['field'].' '.$criteria['comparison'].' \\\'\'.$db->escapeString(\''.$criteria['value'].'\').\'\\\'';
396 break;
397 case \miniSkel::ACTION_MATCH_FIELD:
398 {
399 if ($criteria['field'] == 'recherche')
400 {
401 $query = 'SELECT w.*, r.contenu AS texte, rank(matchinfo(wiki_recherche), 0, 1.0, 1.0) AS points FROM wiki_pages AS w INNER JOIN wiki_recherche AS r ON (w.id = r.id) ';
402 $where .= ' AND wiki_recherche MATCH \\\'\'.$db->escapeString($this->getVariable(\''.$criteria['field'].'\')).\'\\\'';
403 $search = true;
404 }
405 else
406 {
407 if ($criteria['field'] == 'parent')
408 $field = 'id';
409 else
410 $field = $criteria['field'];
411
412 $where .= ' AND '.$criteria['field'].' = \\\'\'.$db->escapeString($this->getVariable(\''.$field.'\')).\'\\\'';
413 }
414 break;
415 }
416 default:
417 break;
418 }
419 }
420
421 if ($search_rank && !$search)
422 {
423 throw new \miniSkelMarkupException("Le critère par points n'est possible que dans les boucles de recherche.");
424 }
425
426 if (trim($loopContent))
427 {
428 $loopStart .= '$row[\'url\'] = WWW_URL . $row[\'uri\']; ';
429 }
430
431 $query .= $where . ' ' . $order;
432
433 if (!$limit || $limit > 100)
434 $limit = 100;
435
436 if ($limit)
437 {
438 $query .= ' LIMIT '.(is_numeric($begin) ? (int) $begin : '\'.$this->variables[\'debut_liste\'].\'').','.(int)$limit;
439 }
440
441 $hash = sha1(uniqid(mt_rand(), true));
442 $out = new Squelette_Snippet();
443 $out->append(1, '$parent_hash = $this->current[\'_self_hash\'];');
444 $out->append(1, '$this->parent =& $parent_hash ? $this->_vars[$parent_hash] : null;');
445
446 if ($search)
447 {
448 $out->append(1, 'if (trim($this->getVariable(\'recherche\'))) { ');
449 }
450
451 $out->append(1, '$statement = $db->prepare(\''.$query.'\'); ');
452 // Sécurité anti injection
453 $out->append(1, 'if (!$statement->readOnly()) { throw new \\miniSkelMarkupException("Requête en écriture illégale: '.$query.'"); } ');
454 $out->append(1, '$result_'.$hash.' = $statement->execute(); ');
455 $out->append(1, '$nb_rows = $db->countRows($result_'.$hash.'); ');
456
457 if ($search)
458 {
459 $out->append(1, '} else { $result_'.$hash.' = false; $nb_rows = 0; }');
460 }
461
462 $out->append(1, '$this->_vars[\''.$hash.'\'] = [\'_self_hash\' => \''.$hash.'\', \'_parent_hash\' => $parent_hash, \'total_boucle\' => $nb_rows, \'compteur_boucle\' => 0];');
463 $out->append(1, '$this->current =& $this->_vars[\''.$hash.'\']; ');
464 $out->append(1, 'if ($nb_rows > 0):');
465
466 if ($preContent)
467 {
468 $out->append(2, $this->parse($preContent, $loopName, self::PRE_CONTENT));
469 }
470
471 $out->append(1, 'while ($row = $result_'.$hash.'->fetchArray(SQLITE3_ASSOC)): ');
472 $out->append(1, '$this->_vars[\''.$hash.'\'][\'compteur_boucle\'] += 1; ');
473 $out->append(1, $loopStart);
474 $out->append(1, '$this->_vars[\''.$hash.'\'] = array_merge($this->_vars[\''.$hash.'\'], $row); ');
475
476 $out->append(2, $this->parseVariables($loopContent));
477
478 $out->append(1, 'endwhile;');
479
480 // we put the post-content after the loop content
481 if ($postContent)
482 {
483 $out->append(2, $this->parse($postContent, $loopName, self::POST_CONTENT));
484 }
485
486 if ($altContent)
487 {
488 $out->append(1, 'else:');
489 $out->append(2, $this->parse($altContent, $loopName, self::ALT_CONTENT));
490 }
491
492 $out->append(1, 'endif; ');
493 $out->append(1, '$parent_hash = $this->_vars[\''.$hash.'\'][\'_parent_hash\']; ');
494 $out->append(1, 'unset($result_'.$hash.', $nb_rows, $this->_vars[\''.$hash.'\']); ');
495 $out->append(1, 'if ($parent_hash) { $this->current =& $this->_vars[$parent_hash]; $parent_hash = $this->current[\'_parent_hash\']; } ');
496 $out->append(1, 'else { $this->current = null; }');
497 $out->append(1, '$this->parent =& $parent_hash ? $this->_vars[$_parent_hash] : null;');
498
499 return $out;
500 }
501
502 public function fetch($template, $no_display = false)
503 {
504 $this->currentTemplate = $template;
505
506 $path = file_exists(DATA_ROOT . '/www/squelettes/' . $template)
507 ? DATA_ROOT . '/www/squelettes/' . $template
508 : ROOT . '/www/squelettes-dist/' . $template;
509
510 $tpl_id = basename(dirname($path)) . '/' . $template;
511
512 if (!self::compile_check($tpl_id, $path))
513 {
514 if (!file_exists($path))
515 {
516 throw new \miniSkelMarkupException('Le squelette "'.$tpl_id.'" n\'existe pas.');
517 }
518
519 $content = file_get_contents($path);
520 $content = strtr($content, ['<?php' => '&lt;?php', '<?' => '<?php echo \'<?\'; ?>']);
521
522 $out = new Squelette_Snippet(2, $this->parse($content));
523 $out->prepend(1, '/* '.$tpl_id.' */ '.
524 'namespace Garradin; $db = DB::getInstance(); '.
525 'if ($this->parent) $parent_hash = $this->parent[\'_self_hash\']; '. // For included files
526 'else $parent_hash = false;');
527
528 if (!$no_display)
529 {
530 self::compile_store($tpl_id, $out);
531 }
532 }
533
534 if (!$no_display)
535 {
536 require self::compile_get_path($tpl_id);
537 }
538 else
539 {
540 eval($tpl_id);
541 }
542
543 return null;
544 }
545
546 public function dispatchURI()
547 {
548 $uri = !empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';
549
550 header('HTTP/1.1 200 OK', 200, true);
551
552 if ($pos = strpos($uri, '?'))
553 {
554 $uri = substr($uri, 0, $pos);
555 }
556 else
557 {
558 // WWW_URI inclus toujours le slash final, mais on veut le conserver ici
559 $uri = substr($uri, strlen(WWW_URI) - 1);
560 }
561
562 if ($uri == '/')
563 {
564 $skel = 'sommaire.html';
565 }
566 elseif ($uri == '/feed/atom/')
567 {
568 header('Content-Type: application/atom+xml');
569 $skel = 'atom.xml';
570 }
571 elseif (substr($uri, -1) == '/')
572 {
573 $skel = 'rubrique.html';
574 $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1, -1);
575 }
576 elseif (preg_match('!^/admin/!', $uri))
577 {
578 throw new UserException('Cette page n\'existe pas.');
579 }
580 else
581 {
582 $_GET['uri'] = $_REQUEST['uri'] = substr($uri, 1);
583
584 if (preg_match('!^[\w\d_-]+$!i', $_GET['uri'])
585 && file_exists(DATA_ROOT . '/www/squelettes/' . strtolower($_GET['uri']) . '.html'))
586 {
587 $skel = strtolower($_GET['uri']) . '.html';
588 }
589 else
590 {
591 $skel = 'article.html';
592 }
593 }
594
595 $this->display($skel);
596 }
597
598 static private function compile_get_path($path)
599 {
600 $hash = sha1($path);
601 return DATA_ROOT . '/cache/compiled/s_' . $hash . '.php';
602 }
603
604 static private function compile_check($tpl, $check)
605 {
606 if (!file_exists(self::compile_get_path($tpl)))
607 return false;
608
609 $time = filemtime(self::compile_get_path($tpl));
610
611 if (empty($time))
612 {
613 return false;
614 }
615
616 if ($time < filemtime($check))
617 return false;
618 return $time;
619 }
620
621 static private function compile_store($tpl, $content)
622 {
623 $path = self::compile_get_path($tpl);
624
625 if (!file_exists(dirname($path)))
626 {
627 mkdir(dirname($path));
628 }
629
630 file_put_contents($path, $content);
631 return true;
632 }
633
634 static public function compile_clear($tpl)
635 {
636 $path = self::compile_get_path($tpl);
637
638 if (file_exists($path))
639 unlink($path);
640
641 return true;
642 }
643
644 protected function getVariable($var)
645 {
646 if (isset($this->current[$var]))
647 {
648 return $this->current[$var];
649 }
650 elseif (isset($this->parent[$var]))
651 {
652 return $this->parent[$var];
653 }
654 elseif (isset($this->variables[$var]))
655 {
656 return $this->variables[$var];
657 }
658 elseif (isset($_REQUEST[$var]))
659 {
660 return $_REQUEST[$var];
661 }
662 else
663 {
664 return null;
665 }
666 }
667
668 static public function getSource($template)
669 {
670 if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $template))
671 return false;
672
673 $path = file_exists(DATA_ROOT . '/www/squelettes/' . $template)
674 ? DATA_ROOT . '/www/squelettes/' . $template
675 : ROOT . '/www/squelettes-dist/' . $template;
676
677 if (!file_exists($path))
678 return false;
679
680 return file_get_contents($path);
681 }
682
683 static public function editSource($template, $content)
684 {
685 if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $template))
686 return false;
687
688 $path = DATA_ROOT . '/www/squelettes/' . $template;
689
690 return file_put_contents($path, $content);
691 }
692
693 static public function resetSource($template)
694 {
695 if (!preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $template))
696 return false;
697
698 if (file_exists(DATA_ROOT . '/www/squelettes/' . $template))
699 {
700 unlink(DATA_ROOT . '/www/squelettes/' . $template);
701 }
702
703 return true;
704 }
705
706 static public function listSources()
707 {
708 if (!file_exists(DATA_ROOT . '/www/squelettes'))
709 {
710 mkdir(DATA_ROOT . '/www/squelettes');
711 }
712
713 $sources = [];
714
715 $dir = dir(ROOT . '/www/squelettes-dist');
716
717 while ($file = $dir->read())
718 {
719 if ($file[0] == '.')
720 continue;
721
722 if (!preg_match('/\.(?:css|x?html?|atom|rss|xml|svg|txt)$/i', $file))
723 continue;
724
725 $sources[] = $file;
726 }
727
728 $dir->close();
729
730 $dir = dir(DATA_ROOT . '/www/squelettes');
731
732 while ($file = $dir->read())
733 {
734 if ($file[0] == '.')
735 continue;
736
737 if (!preg_match('/\.(?:css|x?html?|atom|rss|xml|svg|txt)$/i', $file))
738 continue;
739
740 $sources[] = $file;
741 }
742
743 $dir->close();
744
745 $sources = array_unique($sources);
746 sort($sources);
747
748 return $sources;
749 }
750
751 }
752
753 ?>