. * * Horde_Template provides a basic template engine with tags, loops, * and if conditions. However, it is also a simple interface with * several essential functions: set(), fetch(), and * parse(). Subclasses or decorators can implement (or delegate) these * three methods, plus the options api, and easily implement other * template engines (PHP code, XSLT, etc.) without requiring usage * changes. * * Compilation code adapted from code written by Bruno Pedro . * * Copyright 2002-2016 Horde LLC (http://www.horde.org/) * * See the enclosed file COPYING for license information (LGPL). If you * did not receive this file, see http://www.horde.org/licenses/lgpl21. * * @author Chuck Hagenbuch * @author Michael Slusarz * @package Template */ class Horde_Template { /** The identifier to use for memory-only templates. */ const TEMPLATE_STRING = '**string'; /** * The Horde_Cache object to use. * * @var Horde_Cache */ protected $_cache; /** * Logger. * * @var Horde_Log_Logger */ protected $_logger; /** * Option values. * * @var array */ protected $_options = array(); /** * Directory that templates should be read from. * * @var string */ protected $_basepath = ''; /** * Tag (scalar) values. * * @var array */ protected $_scalars = array(); /** * Loop tag values. * * @var array */ protected $_arrays = array(); /** * Path to template source. * * @var string */ protected $_templateFile = null; /** * Template source. * * @var string */ protected $_template = null; /** * Foreach variable mappings. * * @var array */ protected $_foreachMap = array(); /** * Foreach variable incrementor. * * @var integer */ protected $_foreachVar = 0; /** * preg_match() cache. * * @var array */ protected $_pregcache = array(); /** * Constructor. * * @param array $params The following configuration options: *
     * 'basepath' - (string) The directory where templates are read from.
     * 'cacheob' - (Horde_Cache) A caching object used to cache the output.
     * 'logger' - (Horde_Log_Logger) A logger object.
     * 
*/ public function __construct($params = array()) { if (isset($params['basepath'])) { $this->_basepath = $params['basepath']; } if (isset($params['cacheob'])) { $this->_cache = $params['cacheob']; } if (isset($params['logger'])) { $this->_logger = $params['logger']; } } /** * Sets an option. * Currently available options are: *
     * 'debug' - Output debugging information to screen
     * 'forcecompile' - Force a compilation on every page load
     * 'gettext' - Activate gettext detection
     * 
     *
     * @param string $option  The option name.
     * @param mixed $val      The option's value.
     */
    public function setOption($option, $val)
    {
        $this->_options[$option] = $val;
    }

    /**
     * Set the template contents to a string.
     *
     * @param string $template  The template text.
     */
    public function setTemplate($template)
    {
        $this->_template = $template;
        $this->_parse();
        $this->_templateFile = self::TEMPLATE_STRING;
    }

    /**
     * Returns an option's value.
     *
     * @param string $option  The option name.
     *
     * @return mixed  The option's value.
     */
    public function getOption($option)
    {
        return isset($this->_options[$option])
            ? $this->_options[$option]
            : null;
    }

    /**
     * Sets a tag, loop, or if variable.
     *
     * @param string|array $tag   Either the tag name or a hash with tag names
     *                            as keys and tag values as values.
     * @param mixed        $var   The value to replace the tag with.
     */
    public function set($tag, $var)
    {
        if (is_array($tag)) {
            foreach ($tag as $tTag => $tVar) {
                $this->set($tTag, $tVar);
            }
        } elseif (is_array($var)) {
            $this->_arrays[$tag] = $var;
        } else {
            $this->_scalars[$tag] = (string) $var;
        }
    }

    /**
     * Returns the value of a tag or loop.
     *
     * @param string $tag  The tag name.
     *
     * @return mixed  The tag value or null if the tag hasn't been set yet.
     */
    public function get($tag)
    {
        if (isset($this->_arrays[$tag])) {
            return $this->_arrays[$tag];
        }
        if (isset($this->_scalars[$tag])) {
            return $this->_scalars[$tag];
        }
        return null;
    }

    /**
     * Fetches a template from the specified file and return the parsed
     * contents.
     *
     * @param string $filename  The file to fetch the template from.
     *
     * @return string  The parsed template.
     */
    public function fetch($filename = null)
    {
        $file = $this->_basepath . $filename;
        $force = $this->getOption('forcecompile');

        if (!is_null($filename) && ($file != $this->_templateFile)) {
            $this->_template = $this->_templateFile = null;
        }

        /* First, check for a cached compiled version. */
        $parts = array(
            'horde_template',
            filemtime($file),
            $file
        );
        if ($this->getOption('gettext')) {
            $parts[] = setlocale(LC_ALL, 0);
        }
        $cacheid = implode('|', $parts);

        if (!$force && is_null($this->_template) && $this->_cache) {
            $this->_template = $this->_cache->get($cacheid, 0);
            if ($this->_template === false) {
                $this->_template = null;
            }
        }

        /* Parse and compile the template. */
        if ($force || is_null($this->_template)) {
            $this->_template = str_replace("\n", " \n", file_get_contents($file));
            $this->_parse();
            if ($this->_cache) {
                $this->_cache->set($cacheid, $this->_template);
                if ($this->_logger) {
                    $this->_logger->log(sprintf('Saved compiled template file for "%s".', $file), 'DEBUG');
                }
            }
        }

        $this->_templateFile = $file;

        /* Template debugging. */
        if ($this->getOption('debug')) {
            echo '
' . htmlspecialchars($this->_template) . '
'; } return $this->parse(); } /** * Parses all variables/tags in the template. * * @param string $contents The unparsed template. * * @return string The parsed template. */ public function parse($contents = null) { if (!is_null($contents)) { $this->setTemplate(str_replace("\n", " \n", $contents)); } /* Evaluate the compiled template and return the output. */ ob_start(); eval('?>' . $this->_template); return is_null($contents) ? ob_get_clean() : str_replace(" \n", "\n", ob_get_clean()); } /** * Parses all variables/tags in the template. */ protected function _parse() { // Escape XML instructions. $this->_template = preg_replace('/\?>|<\?/', '', $this->_template); // Parse gettext tags, if the option is enabled. if ($this->getOption('gettext')) { $this->_parseGettext(); } // Process ifs. $this->_parseIf(); // Process loops and arrays. $this->_parseLoop(); // Process base scalar tags. Needs to be after _parseLoop() as we // rely on _foreachMap(). $this->_parseTags(); // Finally, process any associative array scalar tags. $this->_parseAssociativeTags(); } /** * Parses gettext tags. * * @todo Convert to use Horde_Translation. */ protected function _parseGettext() { if (preg_match_all("/(.+?)<\/gettext>/s", $this->_template, $matches, PREG_SET_ORDER)) { $replace = array(); foreach ($matches as $val) { // eval gettext independently so we can embed tempate tags $code = 'echo _(\'' . str_replace("'", "\\'", $val[1]) . '\');'; ob_start(); eval($code); $replace[$val[0]] = ob_get_clean(); } $this->_doReplace($replace); } } /** * Parses 'if' statements. * * @param string $key The key prefix to parse. */ protected function _parseIf($key = null) { $replace = array(); foreach ($this->_doSearch('if', $key) as $val) { $replace[$val[0]] = '_generatePHPVar('scalars', $val[1]) . ') || !empty(' . $this->_generatePHPVar('arrays', $val[1]) . ')): ?>'; $replace[$val[2]] = ''; // Check for else statement. foreach ($this->_doSearch('else', $key) as $val2) { $replace[$val2[0]] = ''; $replace[$val2[2]] = ''; } } $this->_doReplace($replace); } /** * Parses the given array for any loops or other uses of the array. * * @param string $key The key prefix to parse. */ protected function _parseLoop($key = null) { $replace = array(); foreach ($this->_doSearch('loop', $key) as $val) { $divider = null; // See if we have a divider. if (preg_match("/(.*)<\/divider:" . $val[1] . ">/sU", $this->_template, $m)) { $divider = $m[1]; $replace[$m[0]] = ''; } if (!isset($this->_foreachMap[$val[1]])) { $this->_foreachMap[$val[1]] = ++$this->_foreachVar; } $varId = $this->_foreachMap[$val[1]]; $var = $this->_generatePHPVar('arrays', $val[1]); $replace[$val[0]] = '_generatePHPVar('arrays', $val[1]) . ' as $k' . $varId . ' => $v' . $varId . '): ?>'; $replace[$val[2]] = ''; // Parse ifs. $this->_parseIf($val[1]); // Parse interior loops. $this->_parseLoop($val[1]); // Replace scalars. $this->_parseTags($val[1]); } $this->_doReplace($replace); } /** * Replaces 'tag' tags with their PHP equivalents. * * @param string $key The key prefix to parse. */ protected function _parseTags($key = null) { $replace = array(); foreach ($this->_doSearch('tag', $key, true) as $val) { $replace_text = '_foreachMap[$val[1]])) { $var = $this->_foreachMap[$val[1]]; $replace_text .= 'if (isset($v' . $var . ')) { echo is_array($v' . $var . ') ? $k' . $var . ' : $v' . $var . '; } else'; } $var = $this->_generatePHPVar('scalars', $val[1]); $replace[$val[0]] = $replace_text . 'if (isset(' . $var . ')) { echo ' . $var . '; } ?>'; } $this->_doReplace($replace); } /** * Parse associative tags (i.e. ). */ protected function _parseAssociativeTags() { $replace = array(); foreach ($this->_pregcache['tag'] as $key => $val) { $parts = explode('.', $val[1]); $var = '$this->_arrays[\'' . $parts[0] . '\'][\'' . $parts[1] . '\']'; $replace[$val[0]] = ''; unset($this->_pregcache['tag'][$key]); } $this->_doReplace($replace); } /** * Output the correct PHP variable string for use in template space. */ protected function _generatePHPVar($tag, $key) { $out = ''; $a = explode('.', $key); $a_count = count($a); if ($a_count == 1) { switch ($tag) { case 'arrays': $out = '$this->_arrays'; break; case 'scalars': $out = '$this->_scalars'; break; } } else { $out = '$v' . $this->_foreachMap[implode('.', array_slice($a, 0, -1))]; } return $out . '[\'' . end($a) . '\']'; } /** * TODO */ protected function _doSearch($tag, $key, $noclose = false) { $out = array(); $level = (is_null($key)) ? 0 : substr_count($key, '.') + 1; if (!isset($this->_pregcache[$key])) { $regex = ($noclose) ? "/<" . $tag . ":(.+?)\s\/>/" : "/<" . $tag . ":([^>]+)>/"; preg_match_all($regex, $this->_template, $this->_pregcache[$tag], PREG_SET_ORDER); } foreach ($this->_pregcache[$tag] as $pkey => $val) { $val_level = substr_count($val[1], '.'); $add = false; if (is_null($key)) { $add = !$val_level; } else { $add = (($val_level == $level) && (strpos($val[1], $key . '.') === 0)); } if ($add) { if (!$noclose) { $val[2] = ''; } $out[] = $val; unset($this->_pregcache[$tag][$pkey]); } } return $out; } /** * TODO */ protected function _doReplace($replace) { if (empty($replace)) { return; } $search = array(); foreach (array_keys($replace) as $val) { $search[] = '/' . preg_quote($val, '/') . '/'; } $this->_template = preg_replace($search, array_values($replace), $this->_template); } }