File src/include/lib/Garradin/Squelette.php artifact 00efdbaaf6 part of check-in dd8df93b0f


<?php

namespace Garradin;

use Garradin\Entities\Web\Page;

class Squelette_Snippet
{
    const TEXT = 0;
    const PHP = 1;
    const GUESS = 2;
    const OBJ = 3;

    protected $_content = [];

    protected function _getType($type, $value)
    {
        if ($type == self::GUESS)
        {
            if ($value instanceof Squelette_Snippet)
                return self::OBJ;
            else
                return self::TEXT;
        }

        return $type;
    }

    public function __construct($type = self::TEXT, $value = '')
    {
        $type = $this->_getType($type, $value);

        if ($type == self::OBJ)
        {
            $this->_content = $value->get();
        }
        else
        {
            $this->_content[] = (string) (int) $type . $value;
        }

        unset($value);
    }

    public function prepend($type = self::TEXT, $value, $pos = false)
    {
        $type = $this->_getType($type, $value);

        if ($type == self::OBJ)
        {
            if ($pos)
            {
                array_splice($this->_content, $pos, 0, $value->get());
            }
            else
            {
                $this->_content = array_merge($value->get(), $this->_content);
            }
        }
        else
        {
            $value = (string) (int) $type . $value;

            if ($pos)
            {
                array_splice($this->_content, $pos, 0, $value);
            }
            else
            {
                array_unshift($this->_content, $value);
            }
        }

        unset($value);
    }

    public function append($type = self::TEXT, $value, $pos = false)
    {
        $type = $this->_getType($type, $value);

        if ($type == self::OBJ)
        {
            if ($pos)
            {
                array_splice($this->_content, $pos + 1, 0, $value->get());
            }
            else
            {
                $this->_content = array_merge($this->_content, $value->get());
            }
        }
        else
        {
            $value = (string) (int) $type . $value;

            if ($pos)
            {
                array_splice($this->_content, $pos + 1, 0, $value);
            }
            else
            {
                array_push($this->_content, $value);
            }
        }

        unset($value);
    }

    public function output($in_php = false)
    {
        $out = '';
        $php = $in_php ?: false;

        foreach ($this->_content as $line)
        {
            if ($line[0] == self::PHP && !$php)
            {
                $php = true;
                $out .= '<?php ';
            }
            elseif ($line[0] == self::TEXT && $php)
            {
                $php = false;
                $out .= ' ?>';
            }

            $out .= substr($line, 1);

            if ($line[0] == self::PHP)
            {
                $out .= "\n";
            }
        }

        if ($php && !$in_php)
        {
            $out .= ' ?>';
        }

        $this->_content = [];

        return $out;
    }

    public function __toString()
    {
        return $this->output(false);
    }

    public function get()
    {
        return $this->_content;
    }

    public function replace($key, $type = self::TEXT, $value)
    {
        $type = $this->_getType($type, $value);

        if ($type == self::OBJ)
        {
            array_splice($this->_content, $key, 1, $value->get());
        }
        else
        {
            $this->_content[$key] = (string) (int) $type . $value;
        }

        unset($value);
    }
}

class Squelette extends \KD2\MiniSkel
{
    private $parent = null;
    private $current = null;
    private $_vars = [];

    private function _registerDefaultModifiers()
    {
        foreach (Squelette_Filtres::$filtres_php as $func=>$name)
        {
            if (is_string($func))
                $this->register_modifier($name, $func);
            else
                $this->register_modifier($name, $name);
        }

        foreach (get_class_methods('Garradin\Squelette_Filtres') as $name)
        {
            $this->register_modifier($name, ['Garradin\Squelette_Filtres', $name]);
        }

        foreach (Squelette_Filtres::$filtres_alias as $name=>$func)
        {
            $this->register_modifier($name, ['Garradin\Squelette_Filtres', $func]);
        }

        if (file_exists(DATA_ROOT . '/www/squelettes/mes_filtres.php'))
        {
            require_once DATA_ROOT . '/www/squelettes/mes_filtres.php';
        }
    }

    private function _registerDefaultTags()
    {
        $config = Config::getInstance();

        $this->assign('nom_asso', $config->get('nom_asso'));
        $this->assign('adresse_asso', $config->get('adresse_asso'));
        $this->assign('email_asso', $config->get('email_asso'));
        $this->assign('site_asso', $config->get('site_asso'));

        $this->assign('url_racine', WWW_URL);
        $this->assign('url_site', WWW_URL);
        $this->assign('url_atom', WWW_URL . 'feed/atom/');
        $this->assign('url_elements', WWW_URL . 'squelettes/');
        $this->assign('url_admin', ADMIN_URL);

        $url = file_exists(DATA_ROOT . '/www/squelettes/default.css')
            ? WWW_URL . 'squelettes/default.css'
            : WWW_URL . 'squelettes-dist/default.css';

        $this->assign('url_css_defaut', $url);

        if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE']))
        {
            if (function_exists('locale_accept_from_http'))
            {
	           $lang = locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);
            }
            else
            {
                $lang = preg_replace('/[^a-z]/i', '', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
                $lang = strtolower(substr($lang, 0, 2));
            }

	        $lang = strtolower(substr($lang, 0, 2));
	    }
	    else
	    {
	    	$lang = '';
	    }

        $this->assign('langue_visiteur', $lang);
    }

    public function __construct()
    {
        $this->_registerDefaultModifiers();
        $this->_registerDefaultTags();
    }

    protected function processInclude($args)
    {
        if (empty($args))
            throw new \KD2\MiniSkelMarkupException("Le tag INCLURE demande à préciser le fichier à inclure.");

        $file = key($args);

        if (empty($file) || !preg_match('!^[\w\d_-]+(?:\.[\w\d_-]+)*$!', $file))
            throw new \KD2\MiniSkelMarkupException("INCLURE: le nom de fichier ne peut contenir que des caractères alphanumériques.");

        return new Squelette_Snippet(1, '$this->fetch("'.$file.'", false);');
    }

    protected function processVariable($name, $applyDefault, $modifiers, $pre, $post, $context)
    {
        if ($context == self::CONTEXT_IN_ARG)
        {
            $out = new Squelette_Snippet(1, '$this->getVariable(\''.$name.'\')');

            if ($pre)
            {
                $out->prepend(2, $pre);
            }

            if ($post)
            {
                $out->append(2, $post);
            }

            return $out;
        }

        $out = new Squelette_Snippet(1, '$value = $this->getVariable(\''.$name.'\');');

        // We process modifiers
        foreach ($modifiers as &$modifier)
        {
            if (!isset($this->modifiers[$modifier['name']]))
            {
                throw new \KD2\MiniSkelMarkupException('Filtre '.$modifier['name'].' inconnu !');
            }

            $out->append(1, '$value = call_user_func_array('.var_export($this->modifiers[$modifier['name']], true).', [$value, ');

            foreach ($modifier['arguments'] as $arg)
            {
                if ($arg == 'debut_liste')
                {
                    $out->append(1, '$this->getVariable(\'debut_liste\')');
                }
                elseif ($arg instanceOf Squelette_Snippet)
                {
                    $out->append(3, $arg);
                }
                else
                {
                    //if (preg_match('!getVariable!', $arg)) throw new Exception("lol");
                    $out->append(1, '"'.str_replace('"', '\\"', $arg).'"');
                }

                $out->append(1, ', ');
            }

            $out->append(1, ']);');

            if (in_array($modifier['name'], Squelette_Filtres::$desactiver_defaut))
            {
                $applyDefault = false;
            }
        }

        if ($applyDefault)
        {
            $out->append(1, 'if (is_string($value) && trim($value)) $value = htmlspecialchars($value, ENT_QUOTES, \'UTF-8\', false);');
        }

        $out->append(1, 'if ($value === true || trim($value) !== \'\'):');

        // Getting pre-content
        if ($pre)
        {
            $out->append(2, $pre);
        }

        $out->append(1, 'echo is_bool($value) ? "" : $value;');

        // Getting post-content
        if ($post)
        {
            $out->append(2, $post);
        }

        $out->append(1, 'endif;');

        return $out;
    }

    protected function processLoop($loopName, $loopType, $loopCriterias, $loopContent, $preContent, $postContent, $altContent)
    {
        $query = $loop_start = '';
        $query_args = [];
        $db = DB::getInstance();

        $statement_code = '';
        $type = null;

        // Types de boucles natifs
        if ($loopType == 'articles' || $loopType == 'rubriques' || $loopType == 'pages')
        {
            $type = Page::class;
            $order = null;
            $limit = $begin = 0;

            $select = 'w.*, strftime(\'%s\', w.modified) AS date_modification, strftime(\'%s\', f.created) AS date_creation';
            $tables = 'web_pages w
                INNER JOIN files f USING (id)
                INNER JOIN files_search s USING (id)';

            $where = sprintf(' AND w.status = %d', Page::STATUS_ONLINE);

            if ($loopType == 'articles') {
                $where .= sprintf(' AND w.type = %d', Page::TYPE_PAGE);
            }
            elseif ($loopType == 'rubriques') {
                $where .= sprintf(' AND w.type = %d ', Page::TYPE_CATEGORY);
            }

            $allowed_fields = [
                'id'                => 'w.id',
                'uri'               => 'w.uri',
                'titre'             => 'w.title',
                'date'              => 'f.created',
                'date_creation'     => 'f.created',
                'date_modification' => 'w.modified',
                'parent'            => 'w.parent_id',
                'rubrique'          => 'w.parent_id',
                'points'            => 'points',
                'recherche'         => 'search',
                'texte'             => 's.content',
                'age'               => 'age'
            ];

            $search = $search_rank = false;

            foreach ($loopCriterias as $criteria_id => $criteria)
            {
                if (isset($criteria['field']))
                {
                    if (!array_key_exists($criteria['field'], $allowed_fields)) {
                        throw new \KD2\MiniSkelMarkupException("Critère '".$criteria['field']."' invalide pour la boucle ".$loopName." de type ".$loopType.".");
                    }

                    $criteria['field'] = $allowed_fields[$criteria['field']];

                    if ($criteria['field'] == 'points') {
                        if ($criteria['action'] != \KD2\MiniSkel::ACTION_ORDER_BY)
                        {
                            throw new \KD2\MiniSkelMarkupException("Le critère 'points' n\'est pas valide dans ce contexte.");
                        }

                        $search_rank = true;
                    }
                    elseif ($criteria['field'] == 'age') {
                        $criteria['field'] = 'julianday() - julianday(f.created)';
                        $criteria['value'] = (int)$criteria['value'];
                    }
                }

                switch ($criteria['action'])
                {
                    case \KD2\MiniSkel::ACTION_ORDER_BY:
                        if (!$order)
                            $order = 'ORDER BY '.$criteria['field'].'';
                        else
                            $order .= ', '.$criteria['field'].'';
                        break;
                    case \KD2\MiniSkel::ACTION_ORDER_DESC:
                        if ($order)
                            $order .= ' DESC';
                        break;
                    case \KD2\MiniSkel::ACTION_LIMIT:
                        $begin = $criteria['begin'];
                        $limit = $criteria['number'];
                        break;
                    case \KD2\MiniSkel::ACTION_MATCH_FIELD_BY_VALUE:
                        $where .= ' AND '.$criteria['field'].' '.$criteria['comparison'].' ?';
                        $query_args[] = $criteria['value'];
                        break;
                    case \KD2\MiniSkel::ACTION_MATCH_FIELD:
                    {
                        if ($criteria['field'] == 'search') {
                            $where .= ' AND s MATCH ?';
                            $select .= ', rank(matchinfo(s), 0, 1.0, 1.0) AS points';
                            $search = true;
                        }
                        else
                        {
                            $where .= ' AND '.$criteria['field'].' = ?';

                            if ($criteria['field'] == 'w.parent_id')
                            {
                                $criteria['field'] = 'id';
                            }
                        }

                        $query_args[] = ['$this->getVariable(\'' . $criteria['field'] . '\')'];
                        break;
                    }
                    default:
                        break;
                }
            }

            if ($search_rank && !$search)
            {
                throw new \KD2\MiniSkelMarkupException("Le critère par points n'est possible que dans les boucles de recherche.");
            }

            if (trim($loopContent))
            {
                $loop_start .= '$row = $p->asArray(); $row[\'url\'] = $p->url(); ';
            }

            if (!$limit || $limit > 100) {
                $limit = 100;
            }

            if ($limit)
            {
                if (is_numeric($begin)) {
                    $begin = (int) $begin;
                }
                else {
                    $begin = '?';
                    $query_args[] = ['\'.$this->variables[\'debut_liste\'].\''];
                }
            }

            $query = sprintf('SELECT %s FROM %s WHERE 1 %s ORDER BY %s LIMIT %s, %d;', $select, $tables, $where, $order ?: 'w.id', $begin, $limit);
        }
        else if ($loopType == 'documents' || $loopType == 'images' || $loopType == 'fichiers')
        {
            $where = $order = '';
            $limit = $begin = 0;

            $query = 'SELECT f.*, fc.hash, fc.taille, strftime(\'%s\', f.datetime) AS date ';
            $query.= ' FROM fichiers AS f INNER JOIN fichiers_contenu AS fc ON fc.id = f.id_contenu ';
            $query.= ' INNER JOIN fichiers_wiki_pages AS fwp ON fwp.fichier = f.id ';
            $query.= ' INNER JOIN wiki_pages AS w ON w.id = fwp.id AND w.droit_lecture = -1 ';
            $where = 'WHERE 1 ';

            $allowed_fields = ['id', 'nom', 'type', 'date', 'image', 'hash', 'taille',
                'parent', 'page', 'rubrique', 'article', 'sauf_mention'];

            if ($loopType == 'images')
            {
                $where .= ' AND image = 1 ';
            }
            else if ($loopType == 'documents')
            {
                $where .= ' AND image = 0 ';
            }

            foreach ($loopCriterias as $criteria_id => $criteria)
            {
                if (isset($criteria['field']))
                {
                    if (!in_array($criteria['field'], $allowed_fields))
                    {
                        throw new \KD2\MiniSkelMarkupException("Critère '".$criteria['field']."' invalide pour la boucle ".$loopName." de type ".$loopType.".");
                    }
                    elseif ($criteria['field'] == 'rubrique' || $criteria['field'] == 'page' 
                        || $criteria['field'] == 'article' || $criteria['field'] == 'parent')
                    {
                        $criteria['field'] = 'w.id';
                    }
                    elseif ($criteria['field'] == 'date')
                    {
                        $criteria['field'] = 'datetime';
                    }
                }

                switch ($criteria['action'])
                {
                    case \KD2\MiniSkel::ACTION_ORDER_BY:
                        if (!$order)
                            $order = 'ORDER BY '.$criteria['field'].'';
                        else
                            $order .= ', '.$criteria['field'].'';
                        break;
                    case \KD2\MiniSkel::ACTION_ORDER_DESC:
                        if ($order)
                            $order .= ' DESC';
                        break;
                    case \KD2\MiniSkel::ACTION_LIMIT:
                        $begin = $criteria['begin'];
                        $limit = $criteria['number'];
                        break;
                    case \KD2\MiniSkel::ACTION_MATCH_FIELD_BY_VALUE:
                        $where .= ' AND '.$criteria['field'].' '.$criteria['comparison'].' ?';
                        $query_args[] = $criteria['value'];
                        break;
                    case \KD2\MiniSkel::ACTION_MATCH_FIELD:
                    {
                        if ($criteria['field'] == 'sauf_mention')
                        {
                            $where .= " AND f.id NOT IN (:php_implode) ";
                            $query_args[':php_implode'] = '\'.implode(",", Fichiers::listFilesUsedInText($this->getVariable(\'texte\'))).\'';
                            break;
                        }

                        $where .= ' AND '.$criteria['field'].' = ?';

                        if ($criteria['field'] == 'w.id')
                        {
                            $criteria['field'] = 'id';
                        }

                        $query_args[] = ['$this->getVariable(\'' . $criteria['field'] . '\')'];
                        break;
                    }
                    default:
                        break;
                }
            }

            if (trim($loopContent))
            {
                $loop_start .= '$row[\'url\'] = Fichiers::_getURL($row[\'id\'], $row[\'nom\'], $row[\'hash\']); ';
                $loop_start .= '$row[\'miniature\'] = $row[\'image\'] ? Fichiers::_getURL($row[\'id\'], $row[\'nom\'], $row[\'hash\'], 200) : \'\'; ';
                $loop_start .= '$row[\'moyenne\'] = $row[\'image\'] ? Fichiers::_getURL($row[\'id\'], $row[\'nom\'], $row[\'hash\'], 500) : \'\'; ';
            }

            $query .= $where . ' ' . $order;

            if (!$limit || $limit > 100)
                $limit = 100;

            if ($limit)
            {
                $query .= ' LIMIT ';

                if (is_numeric($begin))
                {
                    $query .= (int) $begin;
                }
                else
                {
                    $query .= '?';
                    $query_args[] = ['\'.$this->variables[\'debut_liste\'].\''];
                }

                $query .= ','.(int)$limit;
            }
        }
        else
        {
            $params = [
                'loopName'  =>  $loopName,
                'loopType'  =>  $loopType,
                'loopCriterias' =>  $loopCriterias,
                'loopContent'   =>  $loopContent,
                'preContent'    =>  $preContent,
                'postContent'   =>  $postContent,
                'altContent'    =>  $altContent,
            ];

            $callback_return = [
                'query'         =>  &$query,
                'query_args'    =>  &$query_args,
                'loop_start'    =>  &$loop_start,
                'code'          =>  &$statement_code,
            ];

            // Appel du plugin lié à cette boucle, si ça existe
            $return = Plugin::fireSignal('boucle.' . $loopType, $params, $callback_return);

            // Si aucun plugin n'a été appelé c'est que le type de boucle n'est pas défini
            if (!$return)
            {
                throw new \KD2\MiniSkelMarkupException("Le type de boucle '".$loopType."' est inconnu.");
            }

            if (empty($query))
            {
                $query = 'SELECT 0 LIMIT 0;';
            }
        }

        try {
            // Sécurité anti injection, à la compilation seulement
            $statement = $db->protectSelect(null, $query);
        }
        catch (\Exception $e)
        {
            throw new \KD2\MiniSkelMarkupException("Erreur SQL dans la requête : ".$e->getMessage() . "\n " . $query);
        }

        $hash = sha1(uniqid(mt_rand(), true));
        $out = new Squelette_Snippet();
        $out->append(1, '$parent_hash = @$this->current[\'_self_hash\'];');
        $out->append(1, '$this->parent =& $parent_hash ? $this->_vars[$parent_hash] : null;');

        if (!empty($search))
        {
            $out->append(1, 'if (trim($this->getVariable(\'recherche\'))) { ');
        }

        $query = var_export($query, true);

        foreach ($query_args as $k=>$arg)
        {
            if (substr($k, 0, 4) == ':php')
            {
                $query = str_replace($k, $arg, $query);
                unset($query_args[$k]);
            }
        }

        // Si le plugin a donné du code à exécuter à la place d'une requête SQL
        if ($statement_code)
        {
            $out->append(1, str_replace('$OBJ_VAR', '$result_' . $hash, $statement_code));
            $out->append(1, '$nb_rows = $result_'.$hash.'->countRows();');
        }
        else
        {
            $out->append(1, '$statement = $db->prepare('.$query.'); ');

            foreach ($query_args as $k=>$arg)
            {
                $out->append(1, '$value = ' . (is_array($arg) ? $arg[0] : var_export($arg, true)) . ';');
                $out->append(1, '$statement->bindValue(' . ($k+1) . ', $value, $db->getArgType($value));');
            }

            $out->append(1, '$result_'.$hash.' = $statement->execute(); ');
            $out->append(1, '$nb_rows = $db->countRows($result_'.$hash.'); ');
        }

        if (!empty($search))
        {
            $out->append(1, '} else { $result_'.$hash.' = false; $nb_rows = 0; }');
        }

        $out->append(1, '$this->_vars[\''.$hash.'\'] = [\'_self_hash\' => \''.$hash.'\', \'_parent_hash\' => $parent_hash, \'total_boucle\' => $nb_rows, \'compteur_boucle\' => 0];');
        $out->append(1, '$this->current =& $this->_vars[\''.$hash.'\']; ');
        $out->append(1, 'if ($nb_rows > 0):');

        // Ajout contenu avant tag
        if ($preContent)
        {
            $out->append(2, $this->parse($preContent, $loopName, self::PRE_CONTENT));
        }

        $out->append(1, 'while ($row = $result_'.$hash.'->fetchArray(SQLITE3_ASSOC)): ');
        $out->append(1, sprintf('$e = new %s; $e->import($row); ', $class::class));
        $out->append(1, '$this->_vars[\''.$hash.'\'][\'compteur_boucle\'] += 1; ');
        $out->append(1, $loop_start);
        $out->append(1, '$this->_vars[\''.$hash.'\'] = array_merge($this->_vars[\''.$hash.'\'], $row); ');

        $out->append(2, $this->parseVariables($loopContent));

        $out->append(1, 'endwhile;');

        // we put the post-content after the loop content
        if ($postContent)
        {
            $out->append(2, $this->parse($postContent, $loopName, self::POST_CONTENT));
        }

        if ($altContent)
        {
            $out->append(1, 'else:');
            $out->append(2, $this->parse($altContent, $loopName, self::ALT_CONTENT));
        }

        $out->append(1, 'endif; ');
        $out->append(1, '$parent_hash = $this->_vars[\''.$hash.'\'][\'_parent_hash\']; ');
        $out->append(1, 'unset($result_'.$hash.', $nb_rows, $this->_vars[\''.$hash.'\']); ');
        $out->append(1, 'if ($parent_hash) { $this->current =& $this->_vars[$parent_hash]; $parent_hash = $this->current[\'_parent_hash\']; } ');
        $out->append(1, 'else { $this->current = null; }');
        $out->append(1, '$this->parent =& $parent_hash ? $this->_vars[$parent_hash] : null;');

        return $out;
    }

    public function fetch($template, $no_display = false)
    {
        $this->currentTemplate = $template;

        $path = file_exists(DATA_ROOT . '/www/squelettes/' . $template)
            ? DATA_ROOT . '/www/squelettes/' . $template
            : ROOT . '/www/squelettes-dist/' . $template;

        $tpl_id = basename(dirname($path)) . '/' . $template;

        if (!self::compile_check($tpl_id, $path))
        {
            if (!file_exists($path))
            {
                throw new \KD2\MiniSkelMarkupException('Le squelette "'.$tpl_id.'" n\'existe pas.');
            }

            $content = file_get_contents($path);
            $content = strtr($content, ['<?php' => '&lt;?php', '<?' => '<?php echo \'<?\'; ?>']);
            $out = new Squelette_Snippet(2, $this->parse($content));
            $out->prepend(1, '/* '.$tpl_id.' */ '.
                'namespace Garradin; $db = DB::getInstance(); '.
                'if ($this->parent) $parent_hash = $this->parent[\'_self_hash\']; '. // For included files
                'else $parent_hash = false;');

            if (!$no_display)
            {
                self::compile_store($tpl_id, $out);
            }
        }


        if (!$no_display)
        {
            require self::compile_get_path($tpl_id);
        }
        else
        {
            eval($tpl_id);
        }

        return null;
    }