Seditio Source
Root |
./othercms/dotclear-2.22/inc/libs/clearbricks/template/class.template.php
<?php
/**
 * @class template
 *
 * @package Clearbricks
 * @subpackage Template
 *
 * @copyright Olivier Meunier & Association Dotclear
 * @copyright GPL-2.0-only
 */
class template
{
    private
$self_name;

    public
$use_cache = true;

    protected
$blocks = [];
    protected
$values = [];

    protected
$remove_php = true;

    protected
$unknown_value_handler = null;
    protected
$unknown_block_handler = null;

    protected
$tpl_path = [];
    protected
$cache_dir;
    protected
$parent_file;

    protected
$compile_stack = [];
    protected
$parent_stack  = [];

   
# Inclusion variables
   
protected static $superglobals = ['GLOBALS', '_SERVER', '_GET', '_POST', '_COOKIE', '_FILES', '_ENV', '_REQUEST', '_SESSION'];
    protected static
$_k;
    protected static
$_n;
    protected static
$_r;

    public function
__construct(string $cache_dir, string $self_name)
    {
       
$this->setCacheDir($cache_dir);

       
$this->self_name = $self_name;
       
$this->addValue('include', [$this, 'includeFile']);
       
$this->addBlock('Block', [$this, 'blockSection']);
    }

    public function
includeFile($attr)
    {
        if (!isset(
$attr['src'])) {
            return;
        }

       
$src = path::clean($attr['src']);

       
$tpl_file = $this->getFilePath($src);
        if (!
$tpl_file) {
            return;
        }
        if (
in_array($tpl_file, $this->compile_stack)) {
            return;
        }

        return
       
'<?php try { ' .
       
'echo ' . $this->self_name . "->getData('" . str_replace("'", "\'", $src) . "'); " .
           
'} catch (Exception $e) {} ?>' . "\n";
    }

    public function
blockSection($attr, string $content)
    {
        return
$content;
    }

    public function
setPath()
    {
       
$path = [];

        foreach (
func_get_args() as $v) {
            if (
is_array($v)) {
               
$path = array_merge($path, array_values($v));
            } else {
               
$path[] = $v;
            }
        }

        foreach (
$path as $k => $v) {
            if ((
$v = path::real($v)) === false) {
                unset(
$path[$k]);
            }
        }

       
$this->tpl_path = array_unique($path);
    }

    public function
getPath(): array
    {
        return
$this->tpl_path;
    }

    public function
setCacheDir(string $dir)
    {
        if (!
is_dir($dir)) {
            throw new
Exception($dir . ' is not a valid directory.');
        }

        if (!
is_writable($dir)) {
            throw new
Exception($dir . ' is not writable.');
        }

       
$this->cache_dir = path::real($dir) . '/';
    }

    public function
addBlock(string $name, $callback)
    {
        if (!
is_callable($callback)) {
            throw new
Exception('No valid callback for ' . $name);
        }

       
$this->blocks[$name] = $callback;
    }

    public function
addValue(string $name, $callback)
    {
        if (!
is_callable($callback)) {
            throw new
Exception('No valid callback for ' . $name);
        }

       
$this->values[$name] = $callback;
    }

    public function
blockExists(string $name): bool
   
{
        return isset(
$this->blocks[$name]);
    }

    public function
valueExists(string $name): bool
   
{
        return isset(
$this->values[$name]);
    }

    public function
tagExists(string $name): bool
   
{
        return
$this->blockExists($name) || $this->valueExists($name);
    }

    public function
getValueCallback(string $name)
    {
        if (
$this->valueExists($name)) {
            return
$this->values[$name];
        }

        return
false;
    }

    public function
getBlockCallback(string $name)
    {
        if (
$this->blockExists($name)) {
            return
$this->blocks[$name];
        }

        return
false;
    }

    public function
getBlocksList(): array
    {
        return
array_keys($this->blocks);
    }

    public function
getValuesList(): array
    {
        return
array_keys($this->values);
    }

    public function
getFile(string $file)
    {
       
$tpl_file = $this->getFilePath($file);

        if (!
$tpl_file) {
            throw new
Exception('No template found for ' . $file);
        }

       
$file_md5  = md5($tpl_file);
       
$dest_file = sprintf(
           
'%s/%s/%s/%s/%s.php',
           
$this->cache_dir,
           
'cbtpl',
           
substr($file_md5, 0, 2),
           
substr($file_md5, 2, 2),
           
$file_md5
       
);

       
clearstatcache();
       
$stat_f = $stat_d = false;
        if (
file_exists($dest_file)) {
           
$stat_f = stat($tpl_file);
           
$stat_d = stat($dest_file);
        }

       
# We create template if:
        # - dest_file doest not exists
        # - we don't want cache
        # - dest_file size == 0
        # - tpl_file is more recent thant dest_file
       
if (!$stat_d || !$this->use_cache || $stat_d['size'] == 0 || $stat_f['mtime'] > $stat_d['mtime']) {
           
files::makeDir(dirname($dest_file), true);

            if ((
$fp = @fopen($dest_file, 'wb')) === false) {
                throw new
Exception('Unable to create cache file');
            }

           
$fc = $this->compileFile($tpl_file);
           
fwrite($fp, $fc);
           
fclose($fp);
           
files::inheritChmod($dest_file);
        }

        return
$dest_file;
    }

    public function
getFilePath(string $file)
    {
        foreach (
$this->tpl_path as $p) {
            if (
file_exists($p . '/' . $file)) {
                return
$p . '/' . $file;
            }
        }

        return
false;
    }

    public function
getParentFilePath(string $previous_path, string $file)
    {
       
$check_file = false;
        foreach (
$this->tpl_path as $p) {
            if (
$check_file && file_exists($p . '/' . $file)) {
                return
$p . '/' . $file;
            }
            if (
$p == $previous_path) {
               
$check_file = true;
            }
        }

        return
false;
    }

    public function
getData(string $________): string
   
{
       
self::$_k = array_keys($GLOBALS);

        foreach (
self::$_k as self::$_n) {
            if (!
in_array(self::$_n, self::$superglobals)) {
                global ${
self::$_n};
            }
        }
       
$dest_file = $this->getFile($________);
       
ob_start();
        if (
ini_get('display_errors') == true) {
            include
$dest_file;
        } else {
            @include
$dest_file;
        }
       
self::$_r = ob_get_contents();
       
ob_end_clean();

        return
self::$_r;
    }

    protected function
getCompiledTree(string $file, &$err)
    {
       
$fc = file_get_contents($file);

       
$this->compile_stack[] = $file;

       
# Remove every PHP tags
       
if ($this->remove_php) {
           
$fc = preg_replace('/<\?(?=php|=|\s).*?\?>/ms', '', $fc);
        }

       
# Transform what could be considered as PHP short tags
       
$fc = preg_replace(
           
'/(<\?(?!php|=|\s))(.*?)(\?>)/ms',
           
'<?php echo "$1"; ?>$2<?php echo "$3"; ?>',
           
$fc
       
);

       
# Remove template comments <!-- #... -->
       
$fc = preg_replace('/(^\s*)?<!-- #(.*?)-->/ms', '', $fc);

       
# Lexer part : split file into small pieces
        # each array entry will be either a tag or plain text
       
$blocks = preg_split(
           
'#(<tpl:\w+[^>]*>)|(</tpl:\w+>)|({{tpl:\w+[^}]*}})#msu',
           
$fc,
            -
1,
           
PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY
       
);

       
# Next : build semantic tree from tokens.
       
$rootNode          = new tplNode();
       
$node              = $rootNode;
       
$errors            = [];
       
$this->parent_file = '';
        foreach (
$blocks as $id => $block) {
           
$isblock = preg_match('#<tpl:(\w+)(?:(\s+.*?)>|>)|</tpl:(\w+)>|{{tpl:(\w+)(\s(.*?))?}}#ms', $block, $match);
            if (
$isblock == 1) {
                if (
substr($match[0], 1, 1) == '/') {
                   
// Closing tag, check if it matches current opened node
                   
$tag = $match[3];
                    if ((
$node instanceof tplNodeBlock) && $node->getTag() == $tag) {
                       
$node->setClosing();
                       
$node = $node->getParent();
                    } else {
                       
// Closing tag does not match opening tag
                        // Search if it closes a parent tag
                       
$search = $node;
                        while (
$search->getTag() != 'ROOT' && $search->getTag() != $tag) {
                           
$search = $search->getParent();
                        }
                        if (
$search->getTag() == $tag) {
                           
$errors[] = sprintf(
                               
__('Did not find closing tag for block <tpl:%s>. Content has been ignored.'),
                               
html::escapeHTML($node->getTag())
                            );
                           
$search->setClosing();
                           
$node = $search->getParent();
                        } else {
                           
$errors[] = sprintf(
                               
__('Unexpected closing tag </tpl:%s> found.'),
                               
$tag
                           
);
                        }
                    }
                } elseif (
substr($match[0], 0, 1) == '{') {
                   
// Value tag
                   
$tag      = $match[4];
                   
$str_attr = '';
                   
$attr     = [];
                    if (isset(
$match[6])) {
                       
$str_attr = $match[6];
                       
$attr     = $this->getAttrs($match[6]);
                    }
                    if (
strtolower($tag) == 'extends') {
                        if (isset(
$attr['parent']) && $this->parent_file == '') {
                           
$this->parent_file = $attr['parent'];
                        }
                    } elseif (
strtolower($tag) == 'parent') {
                       
$node->addChild(new tplNodeValueParent($tag, $attr, $str_attr));
                    } else {
                       
$node->addChild(new tplNodeValue($tag, $attr, $str_attr));
                    }
                } else {
                   
// Opening tag, create new node and dive into it
                   
$tag = $match[1];
                    if (
$tag == 'Block') {
                       
$newnode = new tplNodeBlockDefinition($tag, isset($match[2]) ? $this->getAttrs($match[2]) : []);
                    } else {
                       
$newnode = new tplNodeBlock($tag, isset($match[2]) ? $this->getAttrs($match[2]) : []);
                    }
                   
$node->addChild($newnode);
                   
$node = $newnode;
                }
            } else {
               
// Simple text
               
$node->addChild(new tplNodeText($block));
            }
        }

        if ((
$node instanceof tplNodeBlock) && !$node->isClosed()) {
           
$errors[] = sprintf(
               
__('Did not find closing tag for block <tpl:%s>. Content has been ignored.'),
               
html::escapeHTML($node->getTag())
            );
        }

       
$err = '';
        if (
count($errors) > 0) {
           
$err = "\n\n<!-- \n" .
           
__('WARNING: the following errors have been found while parsing template file :') .
           
"\n * " .
           
join("\n * ", $errors) .
               
"\n -->\n";
        }

        return
$rootNode;
    }

    protected function
compileFile(string $file)
    {
       
$tree = null;
       
$err  = '';
        while (
true) {
            if (
$file && !in_array($file, $this->parent_stack)) {
               
$tree = $this->getCompiledTree($file, $err);

                if (
$this->parent_file == '__parent__') {
                   
$this->parent_stack[] = $file;
                   
$newfile              = $this->getParentFilePath(dirname($file), basename($file));
                    if (!
$newfile) {
                        throw new
Exception('No template found for ' . basename($file));
                    }
                   
$file = $newfile;
                } elseif (
$this->parent_file != '') {
                   
$this->parent_stack[] = $file;
                   
$file                 = $this->getFilePath($this->parent_file);
                    if (!
$file) {
                        throw new
Exception('No template found for ' . $this->parent_file);
                    }
                } else {
                    return
$tree->compile($this) . $err;
                }
            } else {
                if (
$tree != null) {
                    return
$tree->compile($this) . $err;
                }

                return
'';
            }
        }
    }

    public function
compileBlockNode(string $tag, $attr, string $content)
    {
       
$res = '';
        if (isset(
$this->blocks[$tag])) {
           
$res .= call_user_func($this->blocks[$tag], $attr, $content);
        } elseif (
$this->unknown_block_handler != null) {
           
$res .= call_user_func($this->unknown_block_handler, $tag, $attr, $content);
        }

        return
$res;
    }

    public function
compileValueNode(string $tag, $attr, string $str_attr)
    {
       
$res = '';
        if (isset(
$this->values[$tag])) {
           
$res .= call_user_func($this->values[$tag], $attr, ltrim((string) $str_attr));
        } elseif (
$this->unknown_value_handler != null) {
           
$res .= call_user_func($this->unknown_value_handler, $tag, $attr, $str_attr);
        }

        return
$res;
    }

    protected function
compileValue(array $match)
    {
       
$v        = $match[1];
       
$attr     = isset($match[2]) ? $this->getAttrs($match[2]) : [];
       
$str_attr = $match[2] ?? null;

        return
call_user_func($this->values[$v], $attr, ltrim((string) $str_attr));
    }

    public function
setUnknownValueHandler($callback)
    {
        if (
is_callable($callback)) {
           
$this->unknown_value_handler = $callback;
        }
    }

    public function
setUnknownBlockHandler($callback)
    {
        if (
is_callable($callback)) {
           
$this->unknown_block_handler = $callback;
        }
    }

    protected function
getAttrs(string $str): array
    {
       
$res = [];
        if (
preg_match_all('|([a-zA-Z0-9_:-]+)="([^"]*)"|ms', $str, $m) > 0) {
            foreach (
$m[1] as $i => $v) {
               
$res[$v] = $m[2][$i];
            }
        }

        return
$res;
    }
}