1
0
mirror of https://github.com/phpbb/phpbb.git synced 2025-02-25 04:23:38 +01:00

[feature/template-engine] Improved template engine.

PHPBB3-9726
This commit is contained in:
Marek A. Ruszczynski 2011-03-07 16:59:41 +01:00 committed by Oleg Pudeyev
parent 4b646c6c80
commit 2d11e1c095
3 changed files with 1204 additions and 877 deletions

View File

@ -1,812 +0,0 @@
<?php
/**
*
* @package phpBB3
* @version $Id$
* @copyright (c) 2005 phpBB Group, sections (c) 2001 ispi of Lincoln Inc
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*
*/
/**
* @ignore
*/
if (!defined('IN_PHPBB'))
{
exit;
}
/**
* Extension of template class - Functions needed for compiling templates only.
*
* psoTFX, phpBB Development Team - Completion of file caching, decompilation
* routines and implementation of conditionals/keywords and associated changes
*
* The interface was inspired by PHPLib templates, and the template file (formats are
* quite similar)
*
* The keyword/conditional implementation is currently based on sections of code from
* the Smarty templating engine (c) 2001 ispi of Lincoln, Inc. which is released
* (on its own and in whole) under the LGPL. Section 3 of the LGPL states that any code
* derived from an LGPL application may be relicenced under the GPL, this applies
* to this source
*
* DEFINE directive inspired by a request by Cyberalien
*
* @package phpBB3
*/
class template_compile
{
var $template;
// Various storage arrays
var $block_names = array();
var $block_else_level = array();
/**
* constuctor
*/
function template_compile(&$template)
{
$this->template = &$template;
}
/**
* Load template source from file
* @access private
*/
function _tpl_load_file($handle, $store_in_db = false)
{
// Try and open template for read
if (!file_exists($this->template->files[$handle]))
{
trigger_error("template->_tpl_load_file(): File {$this->template->files[$handle]} does not exist or is empty", E_USER_ERROR);
}
$this->template->compiled_code[$handle] = $this->compile(trim(@file_get_contents($this->template->files[$handle])));
// Actually compile the code now.
$this->compile_write($handle, $this->template->compiled_code[$handle]);
// Store in database if required...
if ($store_in_db)
{
global $db, $user;
$sql_ary = array(
'template_id' => $this->template->files_template[$handle],
'template_filename' => $this->template->filename[$handle],
'template_included' => '',
'template_mtime' => time(),
'template_data' => trim(@file_get_contents($this->template->files[$handle])),
);
$sql = 'INSERT INTO ' . STYLES_TEMPLATE_DATA_TABLE . ' ' . $db->sql_build_array('INSERT', $sql_ary);
$db->sql_query($sql);
}
}
/**
* Remove any PHP tags that do not belong, these regular expressions are derived from
* the ones that exist in zend_language_scanner.l
* @access private
*/
function remove_php_tags(&$code)
{
// This matches the information gathered from the internal PHP lexer
$match = array(
'#<([\?%])=?.*?\1>#s',
'#<script\s+language\s*=\s*(["\']?)php\1\s*>.*?</script\s*>#s',
'#<\?php(?:\r\n?|[ \n\t]).*?\?>#s'
);
$code = preg_replace($match, '', $code);
}
/**
* The all seeing all doing compile method. Parts are inspired by or directly from Smarty
* @access private
*/
function compile($code, $no_echo = false, $echo_var = '')
{
global $config;
if ($echo_var)
{
global $$echo_var;
}
// Remove any "loose" php ... we want to give admins the ability
// to switch on/off PHP for a given template. Allowing unchecked
// php is a no-no. There is a potential issue here in that non-php
// content may be removed ... however designers should use entities
// if they wish to display < and >
$this->remove_php_tags($code);
// Pull out all block/statement level elements and separate plain text
preg_match_all('#<!-- PHP -->(.*?)<!-- ENDPHP -->#s', $code, $matches);
$php_blocks = $matches[1];
$code = preg_replace('#<!-- PHP -->.*?<!-- ENDPHP -->#s', '<!-- PHP -->', $code);
preg_match_all('#<!-- INCLUDE (\{\$?[A-Z0-9\-_]+\}|[a-zA-Z0-9\_\-\+\./]+) -->#', $code, $matches);
$include_blocks = $matches[1];
$code = preg_replace('#<!-- INCLUDE (?:\{\$?[A-Z0-9\-_]+\}|[a-zA-Z0-9\_\-\+\./]+) -->#', '<!-- INCLUDE -->', $code);
preg_match_all('#<!-- INCLUDEPHP ([a-zA-Z0-9\_\-\+\./]+) -->#', $code, $matches);
$includephp_blocks = $matches[1];
$code = preg_replace('#<!-- INCLUDEPHP [a-zA-Z0-9\_\-\+\./]+ -->#', '<!-- INCLUDEPHP -->', $code);
preg_match_all('#<!-- ([^<].*?) (.*?)? ?-->#', $code, $blocks, PREG_SET_ORDER);
$text_blocks = preg_split('#<!-- [^<].*? (?:.*?)? ?-->#', $code);
for ($i = 0, $j = sizeof($text_blocks); $i < $j; $i++)
{
$this->compile_var_tags($text_blocks[$i]);
}
$compile_blocks = array();
for ($curr_tb = 0, $tb_size = sizeof($blocks); $curr_tb < $tb_size; $curr_tb++)
{
$block_val = &$blocks[$curr_tb];
switch ($block_val[1])
{
case 'BEGIN':
$this->block_else_level[] = false;
$compile_blocks[] = '<?php ' . $this->compile_tag_block($block_val[2]) . ' ?>';
break;
case 'BEGINELSE':
$this->block_else_level[sizeof($this->block_else_level) - 1] = true;
$compile_blocks[] = '<?php }} else { ?>';
break;
case 'END':
array_pop($this->block_names);
$compile_blocks[] = '<?php ' . ((array_pop($this->block_else_level)) ? '}' : '}}') . ' ?>';
break;
case 'IF':
$compile_blocks[] = '<?php ' . $this->compile_tag_if($block_val[2], false) . ' ?>';
break;
case 'ELSE':
$compile_blocks[] = '<?php } else { ?>';
break;
case 'ELSEIF':
$compile_blocks[] = '<?php ' . $this->compile_tag_if($block_val[2], true) . ' ?>';
break;
case 'ENDIF':
$compile_blocks[] = '<?php } ?>';
break;
case 'DEFINE':
$compile_blocks[] = '<?php ' . $this->compile_tag_define($block_val[2], true) . ' ?>';
break;
case 'UNDEFINE':
$compile_blocks[] = '<?php ' . $this->compile_tag_define($block_val[2], false) . ' ?>';
break;
case 'INCLUDE':
$temp = array_shift($include_blocks);
// Dynamic includes
// Cheap match rather than a full blown regexp, we already know
// the format of the input so just use string manipulation.
if ($temp[0] == '{')
{
$file = false;
if ($temp[1] == '$')
{
$var = substr($temp, 2, -1);
//$file = $this->template->_tpldata['DEFINE']['.'][$var];
$temp = "\$this->_tpldata['DEFINE']['.']['$var']";
}
else
{
$var = substr($temp, 1, -1);
//$file = $this->template->_rootref[$var];
$temp = "\$this->_rootref['$var']";
}
}
else
{
$file = $temp;
}
$compile_blocks[] = '<?php ' . $this->compile_tag_include($temp) . ' ?>';
// No point in checking variable includes
if ($file)
{
$this->template->_tpl_include($file, false);
}
break;
case 'INCLUDEPHP':
$compile_blocks[] = ($config['tpl_allow_php']) ? '<?php ' . $this->compile_tag_include_php(array_shift($includephp_blocks)) . ' ?>' : '';
break;
case 'PHP':
$compile_blocks[] = ($config['tpl_allow_php']) ? '<?php ' . array_shift($php_blocks) . ' ?>' : '';
break;
default:
$this->compile_var_tags($block_val[0]);
$trim_check = trim($block_val[0]);
$compile_blocks[] = (!$no_echo) ? ((!empty($trim_check)) ? $block_val[0] : '') : ((!empty($trim_check)) ? $block_val[0] : '');
break;
}
}
$template_php = '';
for ($i = 0, $size = sizeof($text_blocks); $i < $size; $i++)
{
$trim_check_text = trim($text_blocks[$i]);
$template_php .= (!$no_echo) ? (($trim_check_text != '') ? $text_blocks[$i] : '') . ((isset($compile_blocks[$i])) ? $compile_blocks[$i] : '') : (($trim_check_text != '') ? $text_blocks[$i] : '') . ((isset($compile_blocks[$i])) ? $compile_blocks[$i] : '');
}
// Remove unused opening/closing tags
$template_php = str_replace(' ?><?php ', ' ', $template_php);
// Now add a newline after each php closing tag which already has a newline
// PHP itself strips a newline if a closing tag is used (this is documented behaviour) and it is mostly not intended by style authors to remove newlines
$template_php = preg_replace('#\?\>([\r\n])#', '?>\1\1', $template_php);
// There will be a number of occasions where we switch into and out of
// PHP mode instantaneously. Rather than "burden" the parser with this
// we'll strip out such occurences, minimising such switching
if ($no_echo)
{
return "\$$echo_var .= '" . $template_php . "'";
}
return $template_php;
}
/**
* Compile variables
* @access private
*/
function compile_var_tags(&$text_blocks)
{
// change template varrefs into PHP varrefs
$varrefs = array();
// This one will handle varrefs WITH namespaces
preg_match_all('#\{((?:[a-z0-9\-_]+\.)+)(\$)?([A-Z0-9\-_]+)\}#', $text_blocks, $varrefs, PREG_SET_ORDER);
foreach ($varrefs as $var_val)
{
$namespace = $var_val[1];
$varname = $var_val[3];
$new = $this->generate_block_varref($namespace, $varname, true, $var_val[2]);
$text_blocks = str_replace($var_val[0], $new, $text_blocks);
}
// This will handle the remaining root-level varrefs
// transform vars prefixed by L_ into their language variable pendant if nothing is set within the tpldata array
if (strpos($text_blocks, '{L_') !== false)
{
$text_blocks = preg_replace('#\{L_([A-Z0-9\-_]+)\}#', "<?php echo ((isset(\$this->_rootref['L_\\1'])) ? \$this->_rootref['L_\\1'] : ((isset(\$user->lang['\\1'])) ? \$user->lang['\\1'] : '{ \\1 }')); ?>", $text_blocks);
}
// Handle addslashed language variables prefixed with LA_
// If a template variable already exist, it will be used in favor of it...
if (strpos($text_blocks, '{LA_') !== false)
{
$text_blocks = preg_replace('#\{LA_([A-Z0-9\-_]+)\}#', "<?php echo ((isset(\$this->_rootref['LA_\\1'])) ? \$this->_rootref['LA_\\1'] : ((isset(\$this->_rootref['L_\\1'])) ? addslashes(\$this->_rootref['L_\\1']) : ((isset(\$user->lang['\\1'])) ? addslashes(\$user->lang['\\1']) : '{ \\1 }'))); ?>", $text_blocks);
}
// Handle remaining varrefs
$text_blocks = preg_replace('#\{([A-Z0-9\-_]+)\}#', "<?php echo (isset(\$this->_rootref['\\1'])) ? \$this->_rootref['\\1'] : ''; ?>", $text_blocks);
$text_blocks = preg_replace('#\{\$([A-Z0-9\-_]+)\}#', "<?php echo (isset(\$this->_tpldata['DEFINE']['.']['\\1'])) ? \$this->_tpldata['DEFINE']['.']['\\1'] : ''; ?>", $text_blocks);
return;
}
/**
* Compile blocks
* @access private
*/
function compile_tag_block($tag_args)
{
$no_nesting = false;
// Is the designer wanting to call another loop in a loop?
if (strpos($tag_args, '!') === 0)
{
// Count the number of ! occurrences (not allowed in vars)
$no_nesting = substr_count($tag_args, '!');
$tag_args = substr($tag_args, $no_nesting);
}
// Allow for control of looping (indexes start from zero):
// foo(2) : Will start the loop on the 3rd entry
// foo(-2) : Will start the loop two entries from the end
// foo(3,4) : Will start the loop on the fourth entry and end it on the fifth
// foo(3,-4) : Will start the loop on the fourth entry and end it four from last
if (preg_match('#^([^()]*)\(([\-\d]+)(?:,([\-\d]+))?\)$#', $tag_args, $match))
{
$tag_args = $match[1];
if ($match[2] < 0)
{
$loop_start = '($_' . $tag_args . '_count ' . $match[2] . ' < 0 ? 0 : $_' . $tag_args . '_count ' . $match[2] . ')';
}
else
{
$loop_start = '($_' . $tag_args . '_count < ' . $match[2] . ' ? $_' . $tag_args . '_count : ' . $match[2] . ')';
}
if (strlen($match[3]) < 1 || $match[3] == -1)
{
$loop_end = '$_' . $tag_args . '_count';
}
else if ($match[3] >= 0)
{
$loop_end = '(' . ($match[3] + 1) . ' > $_' . $tag_args . '_count ? $_' . $tag_args . '_count : ' . ($match[3] + 1) . ')';
}
else //if ($match[3] < -1)
{
$loop_end = '$_' . $tag_args . '_count' . ($match[3] + 1);
}
}
else
{
$loop_start = 0;
$loop_end = '$_' . $tag_args . '_count';
}
$tag_template_php = '';
array_push($this->block_names, $tag_args);
if ($no_nesting !== false)
{
// We need to implode $no_nesting times from the end...
$block = array_slice($this->block_names, -$no_nesting);
}
else
{
$block = $this->block_names;
}
if (sizeof($block) < 2)
{
// Block is not nested.
$tag_template_php = '$_' . $tag_args . "_count = (isset(\$this->_tpldata['$tag_args'])) ? sizeof(\$this->_tpldata['$tag_args']) : 0;";
$varref = "\$this->_tpldata['$tag_args']";
}
else
{
// This block is nested.
// Generate a namespace string for this block.
$namespace = implode('.', $block);
// Get a reference to the data array for this block that depends on the
// current indices of all parent blocks.
$varref = $this->generate_block_data_ref($namespace, false);
// Create the for loop code to iterate over this block.
$tag_template_php = '$_' . $tag_args . '_count = (isset(' . $varref . ')) ? sizeof(' . $varref . ') : 0;';
}
$tag_template_php .= 'if ($_' . $tag_args . '_count) {';
/**
* The following uses foreach for iteration instead of a for loop, foreach is faster but requires PHP to make a copy of the contents of the array which uses more memory
* <code>
* if (!$offset)
* {
* $tag_template_php .= 'foreach (' . $varref . ' as $_' . $tag_args . '_i => $_' . $tag_args . '_val){';
* }
* </code>
*/
$tag_template_php .= 'for ($_' . $tag_args . '_i = ' . $loop_start . '; $_' . $tag_args . '_i < ' . $loop_end . '; ++$_' . $tag_args . '_i){';
$tag_template_php .= '$_'. $tag_args . '_val = &' . $varref . '[$_'. $tag_args. '_i];';
return $tag_template_php;
}
/**
* Compile IF tags - much of this is from Smarty with
* some adaptions for our block level methods
* @access private
*/
function compile_tag_if($tag_args, $elseif)
{
// Tokenize args for 'if' tag.
preg_match_all('/(?:
"[^"\\\\]*(?:\\\\.[^"\\\\]*)*" |
\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\' |
[(),] |
[^\s(),]+)/x', $tag_args, $match);
$tokens = $match[0];
$is_arg_stack = array();
for ($i = 0, $size = sizeof($tokens); $i < $size; $i++)
{
$token = &$tokens[$i];
switch ($token)
{
case '!==':
case '===':
case '<<':
case '>>':
case '|':
case '^':
case '&':
case '~':
case ')':
case ',':
case '+':
case '-':
case '*':
case '/':
case '@':
break;
case '==':
case 'eq':
$token = '==';
break;
case '!=':
case '<>':
case 'ne':
case 'neq':
$token = '!=';
break;
case '<':
case 'lt':
$token = '<';
break;
case '<=':
case 'le':
case 'lte':
$token = '<=';
break;
case '>':
case 'gt':
$token = '>';
break;
case '>=':
case 'ge':
case 'gte':
$token = '>=';
break;
case '&&':
case 'and':
$token = '&&';
break;
case '||':
case 'or':
$token = '||';
break;
case '!':
case 'not':
$token = '!';
break;
case '%':
case 'mod':
$token = '%';
break;
case '(':
array_push($is_arg_stack, $i);
break;
case 'is':
$is_arg_start = ($tokens[$i-1] == ')') ? array_pop($is_arg_stack) : $i-1;
$is_arg = implode(' ', array_slice($tokens, $is_arg_start, $i - $is_arg_start));
$new_tokens = $this->_parse_is_expr($is_arg, array_slice($tokens, $i+1));
array_splice($tokens, $is_arg_start, sizeof($tokens), $new_tokens);
$i = $is_arg_start;
// no break
default:
if (preg_match('#^((?:[a-z0-9\-_]+\.)+)?(\$)?(?=[A-Z])([A-Z0-9\-_]+)#s', $token, $varrefs))
{
$token = (!empty($varrefs[1])) ? $this->generate_block_data_ref(substr($varrefs[1], 0, -1), true, $varrefs[2]) . '[\'' . $varrefs[3] . '\']' : (($varrefs[2]) ? '$this->_tpldata[\'DEFINE\'][\'.\'][\'' . $varrefs[3] . '\']' : '$this->_rootref[\'' . $varrefs[3] . '\']');
}
else if (preg_match('#^\.((?:[a-z0-9\-_]+\.?)+)$#s', $token, $varrefs))
{
// Allow checking if loops are set with .loopname
// It is also possible to check the loop count by doing <!-- IF .loopname > 1 --> for example
$blocks = explode('.', $varrefs[1]);
// If the block is nested, we have a reference that we can grab.
// If the block is not nested, we just go and grab the block from _tpldata
if (sizeof($blocks) > 1)
{
$block = array_pop($blocks);
$namespace = implode('.', $blocks);
$varref = $this->generate_block_data_ref($namespace, true);
// Add the block reference for the last child.
$varref .= "['" . $block . "']";
}
else
{
$varref = '$this->_tpldata';
// Add the block reference for the last child.
$varref .= "['" . $blocks[0] . "']";
}
$token = "sizeof($varref)";
}
else if (!empty($token))
{
$token = '(' . $token . ')';
}
break;
}
}
// If there are no valid tokens left or only control/compare characters left, we do skip this statement
if (!sizeof($tokens) || str_replace(array(' ', '=', '!', '<', '>', '&', '|', '%', '(', ')'), '', implode('', $tokens)) == '')
{
$tokens = array('false');
}
return (($elseif) ? '} else if (' : 'if (') . (implode(' ', $tokens) . ') { ');
}
/**
* Compile DEFINE tags
* @access private
*/
function compile_tag_define($tag_args, $op)
{
preg_match('#^((?:[a-z0-9\-_]+\.)+)?\$(?=[A-Z])([A-Z0-9_\-]*)(?: = (\'?)([^\']*)(\'?))?$#', $tag_args, $match);
if (empty($match[2]) || (!isset($match[4]) && $op))
{
return '';
}
if (!$op)
{
return 'unset(' . (($match[1]) ? $this->generate_block_data_ref(substr($match[1], 0, -1), true, true) . '[\'' . $match[2] . '\']' : '$this->_tpldata[\'DEFINE\'][\'.\'][\'' . $match[2] . '\']') . ');';
}
// Are we a string?
if ($match[3] && $match[5])
{
$match[4] = str_replace(array('\\\'', '\\\\', '\''), array('\'', '\\', '\\\''), $match[4]);
// Compile reference, we allow template variables in defines...
$match[4] = $this->compile($match[4]);
// Now replace the php code
$match[4] = "'" . str_replace(array('<?php echo ', '; ?>'), array("' . ", " . '"), $match[4]) . "'";
}
else
{
preg_match('#true|false|\.#i', $match[4], $type);
switch (strtolower($type[0]))
{
case 'true':
case 'false':
$match[4] = strtoupper($match[4]);
break;
case '.':
$match[4] = doubleval($match[4]);
break;
default:
$match[4] = intval($match[4]);
break;
}
}
return (($match[1]) ? $this->generate_block_data_ref(substr($match[1], 0, -1), true, true) . '[\'' . $match[2] . '\']' : '$this->_tpldata[\'DEFINE\'][\'.\'][\'' . $match[2] . '\']') . ' = ' . $match[4] . ';';
}
/**
* Compile INCLUDE tag
* @access private
*/
function compile_tag_include($tag_args)
{
// Process dynamic includes
if ($tag_args[0] == '$')
{
return "if (isset($tag_args)) { \$this->_tpl_include($tag_args); }";
}
return "\$this->_tpl_include('$tag_args');";
}
/**
* Compile INCLUDE_PHP tag
* @access private
*/
function compile_tag_include_php($tag_args)
{
return "\$this->_php_include('$tag_args');";
}
/**
* parse expression
* This is from Smarty
* @access private
*/
function _parse_is_expr($is_arg, $tokens)
{
$expr_end = 0;
$negate_expr = false;
if (($first_token = array_shift($tokens)) == 'not')
{
$negate_expr = true;
$expr_type = array_shift($tokens);
}
else
{
$expr_type = $first_token;
}
switch ($expr_type)
{
case 'even':
if (@$tokens[$expr_end] == 'by')
{
$expr_end++;
$expr_arg = $tokens[$expr_end++];
$expr = "!(($is_arg / $expr_arg) % $expr_arg)";
}
else
{
$expr = "!($is_arg & 1)";
}
break;
case 'odd':
if (@$tokens[$expr_end] == 'by')
{
$expr_end++;
$expr_arg = $tokens[$expr_end++];
$expr = "(($is_arg / $expr_arg) % $expr_arg)";
}
else
{
$expr = "($is_arg & 1)";
}
break;
case 'div':
if (@$tokens[$expr_end] == 'by')
{
$expr_end++;
$expr_arg = $tokens[$expr_end++];
$expr = "!($is_arg % $expr_arg)";
}
break;
}
if ($negate_expr)
{
$expr = "!($expr)";
}
array_splice($tokens, 0, $expr_end, $expr);
return $tokens;
}
/**
* Generates a reference to the given variable inside the given (possibly nested)
* block namespace. This is a string of the form:
* ' . $this->_tpldata['parent'][$_parent_i]['$child1'][$_child1_i]['$child2'][$_child2_i]...['varname'] . '
* It's ready to be inserted into an "echo" line in one of the templates.
* NOTE: expects a trailing "." on the namespace.
* @access private
*/
function generate_block_varref($namespace, $varname, $echo = true, $defop = false)
{
// Strip the trailing period.
$namespace = substr($namespace, 0, -1);
// Get a reference to the data block for this namespace.
$varref = $this->generate_block_data_ref($namespace, true, $defop);
// Prepend the necessary code to stick this in an echo line.
// Append the variable reference.
$varref .= "['$varname']";
$varref = ($echo) ? "<?php echo $varref; ?>" : ((isset($varref)) ? $varref : '');
return $varref;
}
/**
* Generates a reference to the array of data values for the given
* (possibly nested) block namespace. This is a string of the form:
* $this->_tpldata['parent'][$_parent_i]['$child1'][$_child1_i]['$child2'][$_child2_i]...['$childN']
*
* If $include_last_iterator is true, then [$_childN_i] will be appended to the form shown above.
* NOTE: does not expect a trailing "." on the blockname.
* @access private
*/
function generate_block_data_ref($blockname, $include_last_iterator, $defop = false)
{
// Get an array of the blocks involved.
$blocks = explode('.', $blockname);
$blockcount = sizeof($blocks) - 1;
// DEFINE is not an element of any referenced variable, we must use _tpldata to access it
if ($defop)
{
$varref = '$this->_tpldata[\'DEFINE\']';
// Build up the string with everything but the last child.
for ($i = 0; $i < $blockcount; $i++)
{
$varref .= "['" . $blocks[$i] . "'][\$_" . $blocks[$i] . '_i]';
}
// Add the block reference for the last child.
$varref .= "['" . $blocks[$blockcount] . "']";
// Add the iterator for the last child if requried.
if ($include_last_iterator)
{
$varref .= '[$_' . $blocks[$blockcount] . '_i]';
}
return $varref;
}
else if ($include_last_iterator)
{
return '$_'. $blocks[$blockcount] . '_val';
}
else
{
return '$_'. $blocks[$blockcount - 1] . '_val[\''. $blocks[$blockcount]. '\']';
}
}
/**
* Write compiled file to cache directory
* @access private
*/
function compile_write($handle, $data)
{
global $phpEx;
$filename = $this->template->cachepath . str_replace('/', '.', $this->template->filename[$handle]) . '.' . $phpEx;
$data = "<?php if (!defined('IN_PHPBB')) exit;" . ((strpos($data, '<?php') === 0) ? substr($data, 5) : ' ?>' . $data);
if ($fp = @fopen($filename, 'wb'))
{
@flock($fp, LOCK_EX);
@fwrite ($fp, $data);
@flock($fp, LOCK_UN);
@fclose($fp);
phpbb_chmod($filename, CHMOD_READ | CHMOD_WRITE);
}
return;
}
}

View File

@ -16,40 +16,75 @@ if (!defined('IN_PHPBB'))
exit; exit;
} }
/**
* @todo
* IMG_ for image substitution?
* {IMG_[key]:[alt]:[type]}
* {IMG_ICON_CONTACT:CONTACT:full} -> $user->img('icon_contact', 'CONTACT', 'full');
*
* More in-depth...
* yadayada
*/
/** /**
* Base Template class. * Base Template class.
* @package phpBB3 * @package phpBB3
*/ */
class template class phpbb_template
{ {
/** variable that holds all the data we'll be substituting into public $phpbb_required = array('user', 'config');
public $phpbb_optional = array();
/**
* variable that holds all the data we'll be substituting into
* the compiled templates. Takes form: * the compiled templates. Takes form:
* --> $this->_tpldata[block][iteration#][child][iteration#][child2][iteration#][variablename] == value * --> $this->_tpldata[block][iteration#][child][iteration#][child2][iteration#][variablename] == value
* if it's a root-level variable, it'll be like this: * if it's a root-level variable, it'll be like this:
* --> $this->_tpldata[.][0][varname] == value * --> $this->_tpldata[.][0][varname] == value
* @var array
*/ */
var $_tpldata = array('.' => array(0 => array())); private $_tpldata = array('.' => array(0 => array()));
var $_rootref;
// Root dir and hash of filenames for each template handle. /**
var $root = ''; * @var array Reference to template->_tpldata['.'][0]
var $cachepath = ''; */
var $files = array(); private $_rootref;
var $filename = array();
var $files_inherit = array(); /**
var $files_template = array(); * @var string Root dir for template.
var $inherit_root = ''; */
var $orig_tpl_storedb; private $root = '';
var $orig_tpl_inherits_id;
/**
* @var string Path of the cache directory for the template
*/
public $cachepath = '';
/**
* @var array Hash of handle => file path pairs
*/
public $files = array();
/**
* @var array Hash of handle => filename pairs
*/
public $filename = array();
public $files_inherit = array();
public $files_template = array();
public $inherit_root = '';
public $orig_tpl_storedb;
public $orig_tpl_inherits_id;
// this will hash handle names to the compiled/uncompiled code for that handle. // this will hash handle names to the compiled/uncompiled code for that handle.
var $compiled_code = array(); public $compiled_code = array();
/** /**
* Set template location * Set template location
* @access public * @access public
*/ */
function set_template() public function set_template()
{ {
global $phpbb_root_path, $user; global $phpbb_root_path, $user;
@ -89,8 +124,11 @@ class template
/** /**
* Set custom template location (able to use directory outside of phpBB) * Set custom template location (able to use directory outside of phpBB)
* @access public * @access public
* @param string $template_path Path to template directory
* @param string $template_name Name of template
* @param string $fallback_template_path Path to fallback template
*/ */
function set_custom_template($template_path, $template_name, $fallback_template_path = false) public function set_custom_template($template_path, $template_name, $fallback_template_path = false)
{ {
global $phpbb_root_path, $user; global $phpbb_root_path, $user;
@ -131,13 +169,10 @@ class template
* Sets the template filenames for handles. $filename_array * Sets the template filenames for handles. $filename_array
* should be a hash of handle => filename pairs. * should be a hash of handle => filename pairs.
* @access public * @access public
* @param array $filname_array Should be a hash of handle => filename pairs.
*/ */
function set_filenames($filename_array) public function set_filenames(array $filename_array)
{ {
if (!is_array($filename_array))
{
return false;
}
foreach ($filename_array as $handle => $filename) foreach ($filename_array as $handle => $filename)
{ {
if (empty($filename)) if (empty($filename))
@ -161,17 +196,26 @@ class template
* Destroy template data set * Destroy template data set
* @access public * @access public
*/ */
function destroy() public function destroy()
{ {
$this->_tpldata = array('.' => array(0 => array())); $this->_tpldata = array('.' => array(0 => array()));
$this->_rootref = &$this->_tpldata['.'][0]; $this->_rootref = &$this->_tpldata['.'][0];
} }
/**
* destroy method kept for compatibility.
*/
public function __destruct()
{
$this->destroy();
}
/** /**
* Reset/empty complete block * Reset/empty complete block
* @access public * @access public
* @param string $blockname Name of block to destroy
*/ */
function destroy_block_vars($blockname) public function destroy_block_vars($blockname)
{ {
if (strpos($blockname, '.') !== false) if (strpos($blockname, '.') !== false)
{ {
@ -200,12 +244,15 @@ class template
/** /**
* Display handle * Display handle
* @access public * @access public
* @param string $handle Handle to display
* @param bool $include_once Allow multiple inclusions
* @return bool True on success, false on failure
*/ */
function display($handle, $include_once = true) public function display($handle, $include_once = true)
{ {
global $user, $phpbb_hook; global $user, $phpbb_hook;
if (!empty($phpbb_hook) && $phpbb_hook->call_hook(array(__CLASS__, __FUNCTION__), $handle, $include_once, $this)) if (!empty($phpbb_hook) && $phpbb_hook->call_hook(array(__CLASS__, __FUNCTION__), $handle, $include_once))
{ {
if ($phpbb_hook->hook_return(array(__CLASS__, __FUNCTION__))) if ($phpbb_hook->hook_return(array(__CLASS__, __FUNCTION__)))
{ {
@ -221,13 +268,23 @@ class template
} }
} }
if ($filename = $this->_tpl_load($handle)) $_tpldata = &$this->_tpldata;
$_rootref = &$this->_rootref;
$_lang = &$user->lang;
if (($filename = $this->_tpl_load($handle)) !== false)
{ {
($include_once) ? include_once($filename) : include($filename); ($include_once) ? include_once($filename) : include($filename);
} }
else if (($code = $this->_tpl_eval($handle)) !== false)
{
$code = ' ?> ' . $code . ' <?php ';
eval($code);
}
else else
{ {
eval(' ?>' . $this->compiled_code[$handle] . '<?php '); // if we could not eval AND the file exists, something horrific has occured
return false;
} }
return true; return true;
@ -236,8 +293,13 @@ class template
/** /**
* Display the handle and assign the output to a template variable or return the compiled result. * Display the handle and assign the output to a template variable or return the compiled result.
* @access public * @access public
* @param string $handle Handle to operate on
* @param string $template_var Template variable to assign compiled handle to
* @param bool $return_content If true return compiled handle, otherwise assign to $template_var
* @param bool $include_once Allow multiple inclusions of the file
* @return bool|string If $return_content is true return string of the compiled handle, otherwise return true
*/ */
function assign_display($handle, $template_var = '', $return_content = true, $include_once = false) public function assign_display($handle, $template_var = '', $return_content = true, $include_once = false)
{ {
ob_start(); ob_start();
$this->display($handle, $include_once); $this->display($handle, $include_once);
@ -256,8 +318,11 @@ class template
/** /**
* Load a compiled template if possible, if not, recompile it * Load a compiled template if possible, if not, recompile it
* @access private * @access private
* @param string $handle Handle of the template to load
* @return string|bool Return filename on success otherwise false
* @uses template_compile is used to compile uncached templates
*/ */
function _tpl_load(&$handle) private function _tpl_load($handle)
{ {
global $user, $phpEx, $config; global $user, $phpEx, $config;
@ -276,7 +341,9 @@ class template
$this->files_template[$handle] = (isset($user->theme['template_id'])) ? $user->theme['template_id'] : 0; $this->files_template[$handle] = (isset($user->theme['template_id'])) ? $user->theme['template_id'] : 0;
$recompile = false; $recompile = false;
if (!file_exists($filename) || @filesize($filename) === 0) $recompile = (!file_exists($filename) || @filesize($filename) === 0 || ($config['load_tplcompile'] && @filemtime($filename) < @filemtime($this->files[$handle]))) ? true : false;
if (defined('DEBUG_EXTRA'))
{ {
$recompile = true; $recompile = true;
} }
@ -288,7 +355,7 @@ class template
$this->files[$handle] = $this->files_inherit[$handle]; $this->files[$handle] = $this->files_inherit[$handle];
$this->files_template[$handle] = $user->theme['template_inherits_id']; $this->files_template[$handle] = $user->theme['template_inherits_id'];
} }
$recompile = (@filemtime($filename) < filemtime($this->files[$handle])) ? true : false; $recompile = (@filemtime($filename) < @filemtime($this->files[$handle])) ? true : false;
} }
// Recompile page if the original template is newer, otherwise load the compiled version // Recompile page if the original template is newer, otherwise load the compiled version
@ -296,14 +363,7 @@ class template
{ {
return $filename; return $filename;
} }
global $db, $phpbb_root_path;
if (!class_exists('template_compile'))
{
include($phpbb_root_path . 'includes/functions_template.' . $phpEx);
}
// Inheritance - we point to another template file for this one. Equality is also used for store_db // Inheritance - we point to another template file for this one. Equality is also used for store_db
if (isset($user->theme['template_inherits_id']) && $user->theme['template_inherits_id'] && !file_exists($this->files[$handle])) if (isset($user->theme['template_inherits_id']) && $user->theme['template_inherits_id'] && !file_exists($this->files[$handle]))
{ {
@ -311,18 +371,51 @@ class template
$this->files_template[$handle] = $user->theme['template_inherits_id']; $this->files_template[$handle] = $user->theme['template_inherits_id'];
} }
$compile = new template_compile($this);
// If we don't have a file assigned to this handle, die. // If we don't have a file assigned to this handle, die.
if (!isset($this->files[$handle])) if (!isset($this->files[$handle]))
{ {
trigger_error("template->_tpl_load(): No file specified for handle $handle", E_USER_ERROR); trigger_error("template->_tpl_load(): No file specified for handle $handle", E_USER_ERROR);
} }
// Just compile if no user object is present (happens within the installer) if (!class_exists('phpbb_template_compile'))
if (!$user) {
include 'template_compile.php';
}
$compile = new phpbb_template_compile($this);
if ($compile->_tpl_load_file($handle) === false)
{
return false;
}
return $filename;
}
/**
* This code should only run when some high level error prevents us from writing to the cache.
* @access private
* @param string $handle Template handle to compile
* @return string|bool Return compiled code on success otherwise false
* @uses template_compile is used to compile template
*/
private function _tpl_eval($handle)
{
if (!class_exists('phpbb_template_compile'))
{
include 'template_compile.php';
}
$compile = new phpbb_template_compile($this);
// If we don't have a file assigned to this handle, die.
if (!isset($this->files[$handle]))
{
trigger_error("template->_tpl_eval(): No file specified for handle $handle", E_USER_ERROR);
}
if (($code = $compile->_tpl_gen_src($handle)) === false)
{ {
$compile->_tpl_load_file($handle);
return false; return false;
} }
@ -337,6 +430,8 @@ class template
} }
$ids[] = $user->theme['template_id']; $ids[] = $user->theme['template_id'];
global $db;
foreach ($ids as $id) foreach ($ids as $id)
{ {
$sql = 'SELECT * $sql = 'SELECT *
@ -381,7 +476,7 @@ class template
$this->files_template[$row['template_filename']] = $user->theme['template_id']; $this->files_template[$row['template_filename']] = $user->theme['template_id'];
} }
if ($force_reload || $row['template_mtime'] < filemtime($file)) if ($force_reload || $row['template_mtime'] < @filemtime($file))
{ {
if ($row['template_filename'] == $this->filename[$handle]) if ($row['template_filename'] == $this->filename[$handle])
{ {
@ -441,8 +536,9 @@ class template
/** /**
* Assign key variable pairs from an array * Assign key variable pairs from an array
* @access public * @access public
* @param array $vararray A hash of variable name => value pairs
*/ */
function assign_vars($vararray) public function assign_vars(array $vararray)
{ {
foreach ($vararray as $key => $val) foreach ($vararray as $key => $val)
{ {
@ -455,8 +551,10 @@ class template
/** /**
* Assign a single variable to a single key * Assign a single variable to a single key
* @access public * @access public
* @param string $varname Variable name
* @param string $varval Value to assign to variable
*/ */
function assign_var($varname, $varval) public function assign_var($varname, $varval)
{ {
$this->_rootref[$varname] = $varval; $this->_rootref[$varname] = $varval;
@ -466,8 +564,10 @@ class template
/** /**
* Assign key variable pairs from an array to a specified block * Assign key variable pairs from an array to a specified block
* @access public * @access public
* @param string $blockname Name of block to assign $vararray to
* @param array $vararray A hash of variable name => value pairs
*/ */
function assign_block_vars($blockname, $vararray) public function assign_block_vars($blockname, array $vararray)
{ {
if (strpos($blockname, '.') !== false) if (strpos($blockname, '.') !== false)
{ {
@ -558,18 +658,51 @@ class template
* @return bool false on error, true on success * @return bool false on error, true on success
* @access public * @access public
*/ */
function alter_block_array($blockname, $vararray, $key = false, $mode = 'insert') public function alter_block_array($blockname, array $vararray, $key = false, $mode = 'insert')
{ {
if (strpos($blockname, '.') !== false) if (strpos($blockname, '.') !== false)
{ {
// Nested blocks are not supported // Nested block.
return false; $blocks = explode('.', $blockname);
$blockcount = sizeof($blocks) - 1;
$block = &$this->_tpldata;
for ($i = 0; $i < $blockcount; $i++)
{
if (($pos = strpos($blocks[$i], '[')) !== false)
{
$name = substr($blocks[$i], 0, $pos);
if (strpos($blocks[$i], '[]') === $pos)
{
$index = sizeof($block[$name]) - 1;
}
else
{
$index = min((int) substr($blocks[$i], $pos + 1, -1), sizeof($block[$name]) - 1);
}
}
else
{
$name = $blocks[$i];
$index = sizeof($block[$name]) - 1;
}
$block = &$block[$name];
$block = &$block[$index];
}
$block = &$block[$blocks[$i]]; // Traverse the last block
}
else
{
// Top-level block.
$block = &$this->_tpldata[$blockname];
} }
// Change key to zero (change first position) if false and to last position if true // Change key to zero (change first position) if false and to last position if true
if ($key === false || $key === true) if ($key === false || $key === true)
{ {
$key = ($key === false) ? 0 : sizeof($this->_tpldata[$blockname]); $key = ($key === false) ? 0 : sizeof($block);
} }
// Get correct position if array given // Get correct position if array given
@ -579,7 +712,7 @@ class template
list($search_key, $search_value) = @each($key); list($search_key, $search_value) = @each($key);
$key = NULL; $key = NULL;
foreach ($this->_tpldata[$blockname] as $i => $val_ary) foreach ($block as $i => $val_ary)
{ {
if ($val_ary[$search_key] === $search_value) if ($val_ary[$search_key] === $search_value)
{ {
@ -612,15 +745,13 @@ class template
} }
// Re-position template blocks // Re-position template blocks
for ($i = sizeof($this->_tpldata[$blockname]); $i > $key; $i--) for ($i = sizeof($block); $i > $key; $i--)
{ {
$this->_tpldata[$blockname][$i] = $this->_tpldata[$blockname][$i-1]; $block[$i] = $block[$i-1];
$this->_tpldata[$blockname][$i]['S_ROW_COUNT'] = $i;
} }
// Insert vararray at given position // Insert vararray at given position
$vararray['S_ROW_COUNT'] = $key; $block[$key] = $vararray;
$this->_tpldata[$blockname][$key] = $vararray;
return true; return true;
} }
@ -628,12 +759,13 @@ class template
// Which block to change? // Which block to change?
if ($mode == 'change') if ($mode == 'change')
{ {
if ($key == sizeof($this->_tpldata[$blockname])) if ($key == sizeof($block))
{ {
$key--; $key--;
} }
$this->_tpldata[$blockname][$key] = array_merge($this->_tpldata[$blockname][$key], $vararray); $block[$key] = array_merge($block[$key], $vararray);
return true; return true;
} }
@ -643,8 +775,11 @@ class template
/** /**
* Include a separate template * Include a separate template
* @access private * @access private
* @param string $filename Template filename to include
* @param bool $include True to include the file, false to just load it
* @uses template_compile is used to compile uncached templates
*/ */
function _tpl_include($filename, $include = true) private function _tpl_include($filename, $include = true)
{ {
$handle = $filename; $handle = $filename;
$this->filename[$handle] = $filename; $this->filename[$handle] = $filename;
@ -654,18 +789,31 @@ class template
$this->files_inherit[$handle] = $this->inherit_root . '/' . $filename; $this->files_inherit[$handle] = $this->inherit_root . '/' . $filename;
} }
$filename = $this->_tpl_load($handle); $filename = $this->_tpl_load($handle);
if ($include) if ($include)
{ {
global $user; global $user;
$_tpldata = &$this->_tpldata;
$_rootref = &$this->_rootref;
$_lang = &$user->lang;
if ($filename) if ($filename)
{ {
include($filename); include($filename);
return; return;
} }
eval(' ?>' . $this->compiled_code[$handle] . '<?php '); else
{
$compile = new phpbb_template_compile($this);
if (($code = $compile->_tpl_gen_src($handle)) !== false)
{
$code = ' ?> ' . $code . ' <?php ';
eval($code);
}
}
} }
} }
@ -673,7 +821,7 @@ class template
* Include a php-file * Include a php-file
* @access private * @access private
*/ */
function _php_include($filename) private function _php_include($filename)
{ {
global $phpbb_root_path; global $phpbb_root_path;
@ -688,3 +836,12 @@ class template
include($file); include($file);
} }
} }
/**
* @todo remove this
*
*/
class template extends phpbb_template
{
// dirty hack
}

View File

@ -0,0 +1,982 @@
<?php
/**
*
* @package phpBB3
* @version $Id$
* @copyright (c) 2005 phpBB Group, sections (c) 2001 ispi of Lincoln Inc
* @license http://opensource.org/licenses/gpl-license.php GNU Public License
*
*/
/**
* @ignore
*/
if (!defined('IN_PHPBB'))
{
exit;
}
/**
* The template filter that does the actual compilation
* @see template_compile
* @package phpBB3
*/
class phpbb_template_filter extends php_user_filter
{
const REGEX_NS = '[a-z][a-z_0-9]+';
const REGEX_VAR = '[A-Z][A-Z_0-9]+';
const REGEX_TAG = '<!-- ([A-Z][A-Z_0-9]+)(?: (.*?) ?)?-->';
const REGEX_TOKENS = '~<!-- ([A-Z][A-Z_0-9]+)(?: (.*?) ?)?-->|{((?:[a-z][a-z_0-9]+\.)*\\$?[A-Z][A-Z_0-9]+)}~';
/**
* @var array
*/
private $block_names = array();
/**
* @var array
*/
private $block_else_level = array();
/**
* @var string
*/
private $chunk;
/**
* @var bool
*/
private $in_php;
public function filter($in, $out, &$consumed, $closing)
{
$written = false;
while ($bucket = stream_bucket_make_writeable($in))
{
$consumed += $bucket->datalen;
$data = $this->chunk . $bucket->data;
$last_nl = strrpos($data, "\n");
$this->chunk = substr($data, $last_nl);
$data = substr($data, 0, $last_nl);
if (!strlen($data))
{
continue;
}
$written = true;
$bucket->data = $this->compile($data);
$bucket->datalen = strlen($bucket->data);
stream_bucket_append($out, $bucket);
}
if ($closing && strlen($this->chunk))
{
$written = true;
$bucket = stream_bucket_new($this->stream, $this->compile($this->chunk));
stream_bucket_append($out, $bucket);
}
return $written ? PSFS_PASS_ON : PSFS_FEED_ME;
}
public function onCreate()
{
$this->chunk = '';
$this->in_php = false;
return true;
}
private function compile($data)
{
$block_start_in_php = $this->in_php;
$data = preg_replace('#<(?:[\\?%]|script)#s', '<?php echo\'\\0\';?>', $data);
$data = preg_replace_callback(self::REGEX_TOKENS, array($this, 'replace'), $data);
global $config;
// Remove php
if (!$config['tpl_allow_php'])
{
if ($block_start_in_php
&& $this->in_php
&& strpos($data, '<!-- PHP -->') === false
&& strpos($data, '<!-- ENDPHP -->') === false)
{
// This is just php code
return '';
}
$data = preg_replace('~^.*?<!-- ENDPHP -->~', '', $data);
$data = preg_replace('~<!-- PHP -->.*?<!-- ENDPHP -->~', '', $data);
$data = preg_replace('~<!-- ENDPHP -->.*?$~', '', $data);
}
"?>/**/";
/*
Preserve whitespace.
PHP removes a newline after the closing tag (if it's there). This is by design.
Consider the following template:
<!-- IF condition -->
some content
<!-- ENDIF -->
If we were to simply preserve all whitespace, we could simply replace all "?>" tags
with "?>\n".
Doing that, would add additional newlines to the compiled tempalte in place of the
IF and ENDIF statements. These newlines are unwanted (and one is conditional).
The IF and ENDIF are usually on their own line for ease of reading.
This replacement preserves newlines only for statements that aren't the only statement on a line.
It will NOT preserve newlines at the end of statements in the above examle.
It will preserve newlines in situations like:
<!-- IF condition -->inline content<!-- ENDIF -->
*/
//*
$data = preg_replace('~(?<!^)(<\?php(?:(?<!\?>).)+(?<!/\*\*/)\?>)$~m', "$1\n", $data);
$data = str_replace('/**/?>', "?>\n", $data);
$data = str_replace('?><?php', '', $data);
return $data;
}
private function replace($matches)
{
if ($this->in_php && $matches[1] != 'ENDPHP')
{
return '';
}
if (isset($matches[3]))
{
return $this->compile_var_tags($matches[0]);
}
global $config;
switch ($matches[1])
{
case 'BEGIN':
$this->block_else_level[] = false;
return '<?php ' . $this->compile_tag_block($matches[2]) . ' ?>';
break;
case 'BEGINELSE':
$this->block_else_level[sizeof($this->block_else_level) - 1] = true;
return '<?php }} else { ?>';
break;
case 'END':
array_pop($this->block_names);
return '<?php ' . ((array_pop($this->block_else_level)) ? '}' : '}}') . ' ?>';
break;
case 'IF':
return '<?php ' . $this->compile_tag_if($matches[2], false) . ' ?>';
break;
case 'ELSE':
return '<?php } else { ?>';
break;
case 'ELSEIF':
return '<?php ' . $this->compile_tag_if($matches[2], true) . ' ?>';
break;
case 'ENDIF':
return '<?php } ?>';
break;
case 'DEFINE':
return '<?php ' . $this->compile_tag_define($matches[2], true) . ' ?>';
break;
case 'UNDEFINE':
return '<?php ' . $this->compile_tag_define($matches[2], false) . ' ?>';
break;
case 'INCLUDE':
return '<?php ' . $this->compile_tag_include($matches[2]) . ' ?>';
break;
case 'INCLUDEPHP':
return ($config['tpl_allow_php']) ? '<?php ' . $this->compile_tag_include_php($matches[2]) . ' ?>' : '';
break;
case 'PHP':
if ($config['tpl_allow_php'])
{
$this->in_php = true;
return '<?php ';
}
return '<!-- PHP -->';
break;
case 'ENDPHP':
if ($config['tpl_allow_php'])
{
$this->in_php = false;
return ' ?>';
}
return '<!-- ENDPHP -->';
break;
default:
return $matches[0];
break;
}
return '';
}
/**
* Compile variables
* @access private
*/
private function compile_var_tags(&$text_blocks)
{
// change template varrefs into PHP varrefs
$varrefs = array();
// This one will handle varrefs WITH namespaces
preg_match_all('#\{((?:' . self::REGEX_NS . '\.)+)(\$)?(' . self::REGEX_VAR . ')\}#', $text_blocks, $varrefs, PREG_SET_ORDER);
foreach ($varrefs as $var_val)
{
$namespace = $var_val[1];
$varname = $var_val[3];
$new = $this->generate_block_varref($namespace, $varname, true, $var_val[2]);
$text_blocks = str_replace($var_val[0], $new, $text_blocks);
}
// Handle special language tags L_ and LA_
$this->compile_language_tags($text_blocks);
// This will handle the remaining root-level varrefs
$text_blocks = preg_replace('#\{(' . self::REGEX_VAR . ')\}#', "<?php echo (isset(\$_rootref['\\1'])) ? \$_rootref['\\1'] : ''; /**/?>", $text_blocks);
$text_blocks = preg_replace('#\{\$(' . self::REGEX_VAR . ')\}#', "<?php echo (isset(\$_tpldata['DEFINE']['.']['\\1'])) ? \$_tpldata['DEFINE']['.']['\\1'] : ''; /**/?>", $text_blocks);
return $text_blocks;
}
/**
* Handles special language tags L_ and LA_
*/
private function compile_language_tags(&$text_blocks)
{
// transform vars prefixed by L_ into their language variable pendant if nothing is set within the tpldata array
if (strpos($text_blocks, '{L_') !== false)
{
$text_blocks = preg_replace('#\{L_(' . self::REGEX_VAR . ')\}#', "<?php echo ((isset(\$_rootref['L_\\1'])) ? \$_rootref['L_\\1'] : ((isset(\$_lang['\\1'])) ? \$_lang['\\1'] : '{ \\1 }')); /**/?>", $text_blocks);
}
// Handle addslashed language variables prefixed with LA_
// If a template variable already exist, it will be used in favor of it...
if (strpos($text_blocks, '{LA_') !== false)
{
$text_blocks = preg_replace('#\{LA_(' . self::REGEX_VAR . '+)\}#', "<?php echo ((isset(\$_rootref['LA_\\1'])) ? \$_rootref['LA_\\1'] : ((isset(\$_rootref['L_\\1'])) ? addslashes(\$_rootref['L_\\1']) : ((isset(\$_lang['\\1'])) ? addslashes(\$_lang['\\1']) : '{ \\1 }'))); /**/?>", $text_blocks);
}
}
/**
* Compile blocks
* @access private
*/
private function compile_tag_block($tag_args)
{
$no_nesting = false;
// Is the designer wanting to call another loop in a loop?
// <!-- BEGIN loop -->
// <!-- BEGIN !loop2 -->
// <!-- END !loop2 -->
// <!-- END loop -->
// 'loop2' is actually on the same nesting level as 'loop' you assign
// variables to it with template->assign_block_vars('loop2', array(...))
if (strpos($tag_args, '!') === 0)
{
// Count the number if ! occurrences (not allowed in vars)
$no_nesting = substr_count($tag_args, '!');
$tag_args = substr($tag_args, $no_nesting);
}
// Allow for control of looping (indexes start from zero):
// foo(2) : Will start the loop on the 3rd entry
// foo(-2) : Will start the loop two entries from the end
// foo(3,4) : Will start the loop on the fourth entry and end it on the fifth
// foo(3,-4) : Will start the loop on the fourth entry and end it four from last
$match = array();
if (preg_match('#^([^()]*)\(([\-\d]+)(?:,([\-\d]+))?\)$#', $tag_args, $match))
{
$tag_args = $match[1];
if ($match[2] < 0)
{
$loop_start = '($_' . $tag_args . '_count ' . $match[2] . ' < 0 ? 0 : $_' . $tag_args . '_count ' . $match[2] . ')';
}
else
{
$loop_start = '($_' . $tag_args . '_count < ' . $match[2] . ' ? $_' . $tag_args . '_count : ' . $match[2] . ')';
}
if (!isset($match[3]) || strlen($match[3]) < 1 || $match[3] == -1)
{
$loop_end = '$_' . $tag_args . '_count';
}
else if ($match[3] >= 0)
{
$loop_end = '(' . ($match[3] + 1) . ' > $_' . $tag_args . '_count ? $_' . $tag_args . '_count : ' . ($match[3] + 1) . ')';
}
else //if ($match[3] < -1)
{
$loop_end = '$_' . $tag_args . '_count' . ($match[3] + 1);
}
}
else
{
$loop_start = 0;
$loop_end = '$_' . $tag_args . '_count';
}
$tag_template_php = '';
array_push($this->block_names, $tag_args);
if ($no_nesting !== false)
{
// We need to implode $no_nesting times from the end...
$block = array_slice($this->block_names, -$no_nesting);
}
else
{
$block = $this->block_names;
}
if (sizeof($block) < 2)
{
// Block is not nested.
$tag_template_php = '$_' . $tag_args . "_count = (isset(\$_tpldata['$tag_args'])) ? sizeof(\$_tpldata['$tag_args']) : 0;";
$varref = "\$_tpldata['$tag_args']";
}
else
{
// This block is nested.
// Generate a namespace string for this block.
$namespace = implode('.', $block);
// Get a reference to the data array for this block that depends on the
// current indices of all parent blocks.
$varref = $this->generate_block_data_ref($namespace, false);
// Create the for loop code to iterate over this block.
$tag_template_php = '$_' . $tag_args . '_count = (isset(' . $varref . ')) ? sizeof(' . $varref . ') : 0;';
}
$tag_template_php .= 'if ($_' . $tag_args . '_count) {';
/**
* The following uses foreach for iteration instead of a for loop, foreach is faster but requires PHP to make a copy of the contents of the array which uses more memory
* <code>
* if (!$offset)
* {
* $tag_template_php .= 'foreach (' . $varref . ' as $_' . $tag_args . '_i => $_' . $tag_args . '_val){';
* }
* </code>
*/
$tag_template_php .= 'for ($_' . $tag_args . '_i = ' . $loop_start . '; $_' . $tag_args . '_i < ' . $loop_end . '; ++$_' . $tag_args . '_i){';
$tag_template_php .= '$_' . $tag_args . '_val = &' . $varref . '[$_' . $tag_args . '_i];';
return $tag_template_php;
}
/**
* Compile a general expression - much of this is from Smarty with
* some adaptions for our block level methods
* @access private
*/
private function compile_expression($tag_args, &$vars = array())
{
$match = array();
preg_match_all('/(?:
"[^"\\\\]*(?:\\\\.[^"\\\\]*)*" |
\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\' |
[(),] |
[^\s(),]+)/x', $tag_args, $match);
$tokens = $match[0];
$is_arg_stack = array();
for ($i = 0, $size = sizeof($tokens); $i < $size; $i++)
{
$token = &$tokens[$i];
switch ($token)
{
case '!==':
case '===':
case '<<':
case '>>':
case '|':
case '^':
case '&':
case '~':
case ')':
case ',':
case '+':
case '-':
case '*':
case '/':
case '@':
break;
case '==':
case 'eq':
$token = '==';
break;
case '!=':
case '<>':
case 'ne':
case 'neq':
$token = '!=';
break;
case '<':
case 'lt':
$token = '<';
break;
case '<=':
case 'le':
case 'lte':
$token = '<=';
break;
case '>':
case 'gt':
$token = '>';
break;
case '>=':
case 'ge':
case 'gte':
$token = '>=';
break;
case '&&':
case 'and':
$token = '&&';
break;
case '||':
case 'or':
$token = '||';
break;
case '!':
case 'not':
$token = '!';
break;
case '%':
case 'mod':
$token = '%';
break;
case '(':
array_push($is_arg_stack, $i);
break;
case 'is':
$is_arg_start = ($tokens[$i-1] == ')') ? array_pop($is_arg_stack) : $i-1;
$is_arg = implode(' ', array_slice($tokens, $is_arg_start, $i - $is_arg_start));
$new_tokens = $this->_parse_is_expr($is_arg, array_slice($tokens, $i+1));
array_splice($tokens, $is_arg_start, sizeof($tokens), $new_tokens);
$i = $is_arg_start;
// no break
default:
$varrefs = array();
if (preg_match('#^((?:' . self::REGEX_NS . '\.)+)?(\$)?(?=[A-Z])([A-Z0-9\-_]+)#s', $token, $varrefs))
{
if (!empty($varrefs[1]))
{
$namespace = substr($varrefs[1], 0, -1);
$namespace = (strpos($namespace, '.') === false) ? $namespace : strrchr($namespace, '.');
// S_ROW_COUNT is deceptive, it returns the current row number not the number of rows
// hence S_ROW_COUNT is deprecated in favour of S_ROW_NUM
switch ($varrefs[3])
{
case 'S_ROW_NUM':
case 'S_ROW_COUNT':
$token = "\$_${namespace}_i";
break;
case 'S_NUM_ROWS':
$token = "\$_${namespace}_count";
break;
case 'S_FIRST_ROW':
$token = "(\$_${namespace}_i == 0)";
break;
case 'S_LAST_ROW':
$token = "(\$_${namespace}_i == \$_${namespace}_count - 1)";
break;
case 'S_BLOCK_NAME':
$token = "'$namespace'";
break;
default:
$token = $this->generate_block_data_ref(substr($varrefs[1], 0, -1), true, $varrefs[2]) . '[\'' . $varrefs[3] . '\']';
$vars[$token] = true;
break;
}
}
else
{
$token = ($varrefs[2]) ? '$_tpldata[\'DEFINE\'][\'.\'][\'' . $varrefs[3] . '\']' : '$_rootref[\'' . $varrefs[3] . '\']';
$vars[$token] = true;
}
}
else if (preg_match('#^\.((?:' . self::REGEX_NS . '\.?)+)$#s', $token, $varrefs))
{
// Allow checking if loops are set with .loopname
// It is also possible to check the loop count by doing <!-- IF .loopname > 1 --> for example
$blocks = explode('.', $varrefs[1]);
// If the block is nested, we have a reference that we can grab.
// If the block is not nested, we just go and grab the block from _tpldata
if (sizeof($blocks) > 1)
{
$block = array_pop($blocks);
$namespace = implode('.', $blocks);
$varref = $this->generate_block_data_ref($namespace, true);
// Add the block reference for the last child.
$varref .= "['" . $block . "']";
}
else
{
$varref = '$_tpldata';
// Add the block reference for the last child.
$varref .= "['" . $blocks[0] . "']";
}
$token = "isset($varref) && sizeof($varref)";
}
break;
}
}
return $tokens;
}
private function compile_tag_if($tag_args, $elseif)
{
$vars = array();
$tokens = $this->compile_expression($tag_args, $vars);
$tpl = ($elseif) ? '} else if (' : 'if (';
if (!empty($vars))
{
$tpl .= '(isset(' . implode(') && isset(', array_keys($vars)) . ')) && ';
}
$tpl .= implode(' ', $tokens);
$tpl .= ') { ';
return $tpl;
}
/**
* Compile DEFINE tags
* @access private
*/
private function compile_tag_define($tag_args, $op)
{
$match = array();
preg_match('#^((?:' . self::REGEX_NS . '\.)+)?\$(?=[A-Z])([A-Z0-9_\-]*)(?: = (.*?))?$#', $tag_args, $match);
if (empty($match[2]) || (!isset($match[3]) && $op))
{
return '';
}
if (!$op)
{
return 'unset(' . (($match[1]) ? $this->generate_block_data_ref(substr($match[1], 0, -1), true, true) . '[\'' . $match[2] . '\']' : '$_tpldata[\'DEFINE\'][\'.\'][\'' . $match[2] . '\']') . ');';
}
$parsed_statement = implode(' ', $this->compile_expression($match[3]));
return (($match[1]) ? $this->generate_block_data_ref(substr($match[1], 0, -1), true, true) . '[\'' . $match[2] . '\']' : '$_tpldata[\'DEFINE\'][\'.\'][\'' . $match[2] . '\']') . ' = ' . $parsed_statement . ';';
}
/**
* Compile INCLUDE tag
* @access private
*/
private function compile_tag_include($tag_args)
{
return "\$this->_tpl_include('$tag_args');";
}
/**
* Compile INCLUDE_PHP tag
* @access private
*/
private function compile_tag_include_php($tag_args)
{
return "include('$tag_args');";
}
/**
* parse expression
* This is from Smarty
* @access private
*/
private function _parse_is_expr($is_arg, $tokens)
{
$expr_end = 0;
$negate_expr = false;
if (($first_token = array_shift($tokens)) == 'not')
{
$negate_expr = true;
$expr_type = array_shift($tokens);
}
else
{
$expr_type = $first_token;
}
switch ($expr_type)
{
case 'even':
if (isset($tokens[$expr_end]) && $tokens[$expr_end] == 'by')
{
$expr_end++;
$expr_arg = $tokens[$expr_end++];
$expr = "!(($is_arg / $expr_arg) & 1)";
}
else
{
$expr = "!($is_arg & 1)";
}
break;
case 'odd':
if (isset($tokens[$expr_end]) && $tokens[$expr_end] == 'by')
{
$expr_end++;
$expr_arg = $tokens[$expr_end++];
$expr = "(($is_arg / $expr_arg) & 1)";
}
else
{
$expr = "($is_arg & 1)";
}
break;
case 'div':
if (isset($tokens[$expr_end]) && $tokens[$expr_end] == 'by')
{
$expr_end++;
$expr_arg = $tokens[$expr_end++];
$expr = "!($is_arg % $expr_arg)";
}
break;
}
if ($negate_expr)
{
if ($expr[0] == '!')
{
// Negated expression, de-negate it.
$expr = substr($expr, 1);
}
else
{
$expr = "!($expr)";
}
}
array_splice($tokens, 0, $expr_end, $expr);
return $tokens;
}
/**
* Generates a reference to the given variable inside the given (possibly nested)
* block namespace. This is a string of the form:
* ' . $_tpldata['parent'][$_parent_i]['$child1'][$_child1_i]['$child2'][$_child2_i]...['varname'] . '
* It's ready to be inserted into an "echo" line in one of the templates.
*
* @access private
* @param string $namespace Namespace to access (expects a trailing "." on the namespace)
* @param string $varname Variable name to use
* @param bool $echo If true return an echo statement, otherwise a reference to the internal variable
* @param bool $defop If true this is a variable created with the DEFINE construct, otherwise template variable
* @return string Code to access variable or echo it if $echo is true
*/
private function generate_block_varref($namespace, $varname, $echo = true, $defop = false)
{
// Strip the trailing period.
$namespace = substr($namespace, 0, -1);
$expr = true;
$isset = false;
// S_ROW_COUNT is deceptive, it returns the current row number now the number of rows
// hence S_ROW_COUNT is deprecated in favour of S_ROW_NUM
switch ($varname)
{
case 'S_ROW_NUM':
case 'S_ROW_COUNT':
$varref = "\$_${namespace}_i";
break;
case 'S_NUM_ROWS':
$varref = "\$_${namespace}_count";
break;
case 'S_FIRST_ROW':
$varref = "(\$_${namespace}_i == 0)";
break;
case 'S_LAST_ROW':
$varref = "(\$_${namespace}_i == \$_${namespace}_count - 1)";
break;
case 'S_BLOCK_NAME':
$varref = "'$namespace'";
break;
default:
// Get a reference to the data block for this namespace.
$varref = $this->generate_block_data_ref($namespace, true, $defop);
// Prepend the necessary code to stick this in an echo line.
// Append the variable reference.
$varref .= "['$varname']";
$expr = false;
$isset = true;
break;
}
// @todo Test the !$expr more
$varref = ($echo) ? '<?php echo ' . ($isset ? "isset($varref) ? $varref : ''" : $varref) . '; /**/?>' : (($expr || isset($varref)) ? $varref : '');
return $varref;
}
/**
* Generates a reference to the array of data values for the given
* (possibly nested) block namespace. This is a string of the form:
* $_tpldata['parent'][$_parent_i]['$child1'][$_child1_i]['$child2'][$_child2_i]...['$childN']
*
* @access private
* @param string $blockname Block to access (does not expect a trailing "." on the blockname)
* @param bool $include_last_iterator If $include_last_iterator is true, then [$_childN_i] will be appended to the form shown above.
* @param bool $defop If true this is a variable created with the DEFINE construct, otherwise template variable
* @return string Code to access variable
*/
private function generate_block_data_ref($blockname, $include_last_iterator, $defop = false)
{
// Get an array of the blocks involved.
$blocks = explode('.', $blockname);
$blockcount = sizeof($blocks) - 1;
// DEFINE is not an element of any referenced variable, we must use _tpldata to access it
if ($defop)
{
$varref = '$_tpldata[\'DEFINE\']';
// Build up the string with everything but the last child.
for ($i = 0; $i < $blockcount; $i++)
{
$varref .= "['" . $blocks[$i] . "'][\$_" . $blocks[$i] . '_i]';
}
// Add the block reference for the last child.
$varref .= "['" . $blocks[$blockcount] . "']";
// Add the iterator for the last child if requried.
if ($include_last_iterator)
{
$varref .= '[$_' . $blocks[$blockcount] . '_i]';
}
return $varref;
}
else if ($include_last_iterator)
{
return '$_'. $blocks[$blockcount] . '_val';
}
else
{
return '$_'. $blocks[$blockcount - 1] . '_val[\''. $blocks[$blockcount]. '\']';
}
}
}
stream_filter_register('phpbb_template', 'phpbb_template_filter');
/**
* Extension of template class - Functions needed for compiling templates only.
*
* psoTFX, phpBB Development Team - Completion of file caching, decompilation
* routines and implementation of conditionals/keywords and associated changes
*
* The interface was inspired by PHPLib templates, and the template file (formats are
* quite similar)
*
* The keyword/conditional implementation is currently based on sections of code from
* the Smarty templating engine (c) 2001 ispi of Lincoln, Inc. which is released
* (on its own and in whole) under the LGPL. Section 3 of the LGPL states that any code
* derived from an LGPL application may be relicenced under the GPL, this applies
* to this source
*
* DEFINE directive inspired by a request by Cyberalien
*
* @package phpBB3
* @uses template_filter As a PHP stream filter to perform compilation of templates
*/
class phpbb_template_compile
{
/**
* @var phpbb_template Reference to the {@link phpbb_template template} object performing compilation
*/
private $template;
/**
* Constructor
* @param phpbb_template $template {@link phpbb_template Template} object performing compilation
*/
public function __construct(phpbb_template $template)
{
$this->template = $template;
}
/**
* Load template source from file
* @access public
* @param string $handle Template handle we wish to load
* @return bool Return true on success otherwise false
*/
public function _tpl_load_file($handle)
{
// Try and open template for read
if (!file_exists($this->template->files[$handle]))
{
trigger_error("template->_tpl_load_file(): File {$this->template->files[$handle]} does not exist or is empty", E_USER_ERROR);
}
// Actually compile the code now.
return $this->compile_write($handle, $this->template->files[$handle]);
}
/**
* Load template source from file
* @access public
* @param string $handle Template handle we wish to compile
* @return string|bool Return compiled code on successful compilation otherwise false
*/
public function _tpl_gen_src($handle)
{
// Try and open template for read
if (!file_exists($this->template->files[$handle]))
{
trigger_error("template->_tpl_load_file(): File {$this->template->files[$handle]} does not exist or is empty", E_USER_ERROR);
}
// Actually compile the code now.
return $this->compile_gen($this->template->files[$handle]);
}
/**
* Write compiled file to cache directory
* @access private
* @param string $handle Template handle to compile
* @param string $source_file Source template file
* @return bool Return true on success otherwise false
*/
private function compile_write($handle, $source_file)
{
global $system, $phpEx;
$filename = $this->template->cachepath . str_replace('/', '.', $this->template->filename[$handle]) . '.' . $phpEx;
$source_handle = @fopen($source_file, 'rb');
$destination_handle = @fopen($filename, 'wb');
if (!$source_handle || !$destination_handle)
{
return false;
}
@flock($destination_handle, LOCK_EX);
stream_filter_append($source_handle, 'phpbb_template');
stream_copy_to_stream($source_handle, $destination_handle);
@fclose($source_handle);
@flock($destination_handle, LOCK_UN);
@fclose($destination_handle);
phpbb_chmod($filename, CHMOD_READ | CHMOD_WRITE);
clearstatcache();
return true;
}
/**
* Generate source for eval()
* @access private
* @param string $source_file Source template file
* @return string|bool Return compiled code on successful compilation otherwise false
*/
private function compile_gen($source_file)
{
$source_handle = @fopen($source_file, 'rb');
$destination_handle = @fopen('php://temp' ,'r+b');
if (!$source_handle || !$destination_handle)
{
return false;
}
stream_filter_append($source_handle, 'phpbb_template');
stream_copy_to_stream($source_handle, $destination_handle);
@fclose($source_handle);
rewind($destination_handle);
return stream_get_contents($destination_handle);
}
}
?>