6 * @copyright (c) 2005 phpBB Group, sections (c) 2001 ispi of Lincoln Inc
7 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
14 if (!defined('IN_PHPBB'))
20 * The template filter that does the actual compilation
21 * @see template_compile
24 class phpbb_template_filter
extends php_user_filter
27 * @var string Replaceable tokens regex
29 private $regex = '~<!-- ([A-Z][A-Z_0-9]+)(?: (.*?) ?)?-->|{((?:[a-z][a-z_0-9]+\.)*\\$?[A-Z][A-Z_0-9]+)}~';
34 private $block_names = array();
39 private $block_else_level = array();
46 public function filter($in, $out, &$consumed, $closing)
50 while ($bucket = stream_bucket_make_writeable($in))
52 $consumed +
= $bucket->datalen
;
54 $data = $this->chunk
. $bucket->data
;
55 $last_nl = strrpos($data, "\n");
56 $this->chunk
= substr($data, $last_nl);
57 $data = substr($data, 0, $last_nl);
66 $bucket->data
= $this->compile($data);
67 $bucket->datalen
= strlen($bucket->data
);
68 stream_bucket_append($out, $bucket);
71 if ($closing && strlen($this->chunk
))
74 $bucket = stream_bucket_new($this->stream
, $this->compile($this->chunk
));
75 stream_bucket_append($out, $bucket);
78 return $written ? PSFS_PASS_ON
: PSFS_FEED_ME
;
81 public function onCreate()
87 private function compile($data)
89 $data = preg_replace('#<(?:[\\?%]|script)#s', '<?php echo\'\\0\';?>', $data);
90 return str_replace('?><?php', '', preg_replace_callback($this->regex
, array($this, 'replace'), $data));
93 private function replace($matches)
95 if (isset($matches[3]))
97 return $this->compile_var_tags($matches[0]);
103 $this->block_else_level
[] = false;
104 return '<?php ' . $this->compile_tag_block($matches[2]) . ' ?>';
108 $this->block_else_level
[sizeof($this->block_else_level
) - 1] = true;
109 return '<?php }} else { ?>';
113 array_pop($this->block_names
);
114 return '<?php ' . ((array_pop($this->block_else_level
)) ?
'}' : '}}') . ' ?>';
118 return '<?php ' . $this->compile_tag_if($matches[2], false) . ' ?>';
122 return '<?php } else { ?>';
126 return '<?php ' . $this->compile_tag_if($matches[2], true) . ' ?>';
134 return '<?php ' . $this->compile_tag_define($matches[2], true) . ' ?>';
138 return '<?php ' . $this->compile_tag_define($matches[2], false) . ' ?>';
142 return '<?php ' . $this->compile_tag_include($matches[2]) . ' ?>';
146 return (phpbb
::$config['tpl_allow_php']) ?
'<?php ' . $this->compile_tag_include_php($matches[2]) . ' ?>' : '';
150 return (phpbb
::$config['tpl_allow_php']) ?
'<?php ' : '<!-- ';
154 return (phpbb
::$config['tpl_allow_php']) ?
' ?>' : ' -->';
169 private function compile_var_tags(&$text_blocks)
171 // change template varrefs into PHP varrefs
174 // This one will handle varrefs WITH namespaces
175 preg_match_all('#\{((?:[a-z0-9\-_]+\.)+)(\$)?([A-Z0-9\-_]+)\}#', $text_blocks, $varrefs, PREG_SET_ORDER
);
177 foreach ($varrefs as $var_val)
179 $namespace = $var_val[1];
180 $varname = $var_val[3];
181 $new = $this->generate_block_varref($namespace, $varname, true, $var_val[2]);
183 $text_blocks = str_replace($var_val[0], $new, $text_blocks);
186 // Handle special language tags L_ and LA_
187 $this->compile_language_tags($text_blocks);
189 // This will handle the remaining root-level varrefs
190 $text_blocks = preg_replace('#\{([A-Z0-9\-_]+)\}#', "<?php echo (isset(\$_rootref['\\1'])) ? \$_rootref['\\1'] : ''; ?>", $text_blocks);
191 $text_blocks = preg_replace('#\{\$([A-Z0-9\-_]+)\}#', "<?php echo (isset(\$_tpldata['DEFINE']['.']['\\1'])) ? \$_tpldata['DEFINE']['.']['\\1'] : ''; ?>", $text_blocks);
197 * Handlse special language tags L_ and LA_
199 private function compile_language_tags(&$text_blocks)
201 // transform vars prefixed by L_ into their language variable pendant if nothing is set within the tpldata array
202 if (strpos($text_blocks, '{L_') !== false)
204 $text_blocks = preg_replace('#\{L_([A-Z0-9\-_]+)\}#', "<?php echo ((isset(\$_rootref['L_\\1'])) ? \$_rootref['L_\\1'] : ((isset(\$_lang['\\1'])) ? \$_lang['\\1'] : '{ \\1 }')); ?>", $text_blocks);
207 // Handle addslashed language variables prefixed with LA_
208 // If a template variable already exist, it will be used in favor of it...
209 if (strpos($text_blocks, '{LA_') !== false)
211 $text_blocks = preg_replace('#\{LA_([A-Z0-9\-_]+)\}#', "<?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);
219 private function compile_tag_block($tag_args)
223 // Is the designer wanting to call another loop in a loop?
224 // <!-- BEGIN loop -->
225 // <!-- BEGIN !loop2 -->
226 // <!-- END !loop2 -->
228 // 'loop2' is actually on the same nesting level as 'loop' you assign
229 // variables to it with template->assign_block_vars('loop2', array(...))
230 if (strpos($tag_args, '!') === 0)
232 // Count the number if ! occurrences (not allowed in vars)
233 $no_nesting = substr_count($tag_args, '!');
234 $tag_args = substr($tag_args, $no_nesting);
237 // Allow for control of looping (indexes start from zero):
238 // foo(2) : Will start the loop on the 3rd entry
239 // foo(-2) : Will start the loop two entries from the end
240 // foo(3,4) : Will start the loop on the fourth entry and end it on the fifth
241 // foo(3,-4) : Will start the loop on the fourth entry and end it four from last
243 if (preg_match('#^([^()]*)\(([\-\d]+)(?:,([\-\d]+))?\)$#', $tag_args, $match))
245 $tag_args = $match[1];
249 $loop_start = '($_' . $tag_args . '_count ' . $match[2] . ' < 0 ? 0 : $_' . $tag_args . '_count ' . $match[2] . ')';
253 $loop_start = '($_' . $tag_args . '_count < ' . $match[2] . ' ? $_' . $tag_args . '_count : ' . $match[2] . ')';
256 if (strlen($match[3]) < 1 ||
$match[3] == -1)
258 $loop_end = '$_' . $tag_args . '_count';
260 else if ($match[3] >= 0)
262 $loop_end = '(' . ($match[3] +
1) . ' > $_' . $tag_args . '_count ? $_' . $tag_args . '_count : ' . ($match[3] +
1) . ')';
264 else //if ($match[3] < -1)
266 $loop_end = '$_' . $tag_args . '_count' . ($match[3] +
1);
272 $loop_end = '$_' . $tag_args . '_count';
275 $tag_template_php = '';
276 array_push($this->block_names
, $tag_args);
278 if ($no_nesting !== false)
280 // We need to implode $no_nesting times from the end...
281 $block = array_slice($this->block_names
, -$no_nesting);
285 $block = $this->block_names
;
288 if (sizeof($block) < 2)
290 // Block is not nested.
291 $tag_template_php = '$_' . $tag_args . "_count = (isset(\$_tpldata['$tag_args'])) ? sizeof(\$_tpldata['$tag_args']) : 0;";
292 $varref = "\$_tpldata['$tag_args']";
296 // This block is nested.
297 // Generate a namespace string for this block.
298 $namespace = implode('.', $block);
300 // Get a reference to the data array for this block that depends on the
301 // current indices of all parent blocks.
302 $varref = $this->generate_block_data_ref($namespace, false);
304 // Create the for loop code to iterate over this block.
305 $tag_template_php = '$_' . $tag_args . '_count = (isset(' . $varref . ')) ? sizeof(' . $varref . ') : 0;';
308 $tag_template_php .= 'if ($_' . $tag_args . '_count) {';
311 * 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
315 * $tag_template_php .= 'foreach (' . $varref . ' as $_' . $tag_args . '_i => $_' . $tag_args . '_val){';
320 $tag_template_php .= 'for ($_' . $tag_args . '_i = ' . $loop_start . '; $_' . $tag_args . '_i < ' . $loop_end . '; ++$_' . $tag_args . '_i){';
321 $tag_template_php .= '$_'. $tag_args . '_val = &' . $varref . '[$_'. $tag_args. '_i];';
323 return $tag_template_php;
327 * Compile a general expression - much of this is from Smarty with
328 * some adaptions for our block level methods
331 private function compile_expression($tag_args)
335 "[^"\\\\]*(?:\\\\.[^"\\\\]*)*" |
336 \'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\' |
338 [^\s(),]+)/x', $tag_args, $match);
341 $is_arg_stack = array();
343 for ($i = 0, $size = sizeof($tokens); $i < $size; $i++
)
345 $token = &$tokens[$i];
421 array_push($is_arg_stack, $i);
425 $is_arg_start = ($tokens[$i-1] == ')') ?
array_pop($is_arg_stack) : $i-1;
426 $is_arg = implode(' ', array_slice($tokens, $is_arg_start, $i - $is_arg_start));
428 $new_tokens = $this->_parse_is_expr($is_arg, array_slice($tokens, $i+
1));
430 array_splice($tokens, $is_arg_start, sizeof($tokens), $new_tokens);
438 if (preg_match('#^((?:[a-z0-9\-_]+\.)+)?(\$)?(?=[A-Z])([A-Z0-9\-_]+)#s', $token, $varrefs))
440 if (!empty($varrefs[1]))
442 $namespace = substr($varrefs[1], 0, -1);
443 $namespace = (strpos($namespace, '.') === false) ?
$namespace : strrchr($namespace, '.');
445 // S_ROW_COUNT is deceptive, it returns the current row number now the number of rows
446 // hence S_ROW_COUNT is deprecated in favour of S_ROW_NUM
451 $token = "\$_${namespace}_i";
455 $token = "\$_${namespace}_count";
459 $token = "(\$_${namespace}_i == 0)";
463 $token = "(\$_${namespace}_i == \$_${namespace}_count - 1)";
467 $token = "'$namespace'";
471 $token = $this->generate_block_data_ref(substr($varrefs[1], 0, -1), true, $varrefs[2]) . '[\'' . $varrefs[3] . '\']';
477 $token = ($varrefs[2]) ?
'$_tpldata[\'DEFINE\'][\'.\'][\'' . $varrefs[3] . '\']' : '$_rootref[\'' . $varrefs[3] . '\']';
480 else if (preg_match('#^\.((?:[a-z0-9\-_]+\.?)+)$#s', $token, $varrefs))
482 // Allow checking if loops are set with .loopname
483 // It is also possible to check the loop count by doing <!-- IF .loopname > 1 --> for example
484 $blocks = explode('.', $varrefs[1]);
486 // If the block is nested, we have a reference that we can grab.
487 // If the block is not nested, we just go and grab the block from _tpldata
488 if (sizeof($blocks) > 1)
490 $block = array_pop($blocks);
491 $namespace = implode('.', $blocks);
492 $varref = $this->generate_block_data_ref($namespace, true);
494 // Add the block reference for the last child.
495 $varref .= "['" . $block . "']";
499 $varref = '$_tpldata';
501 // Add the block reference for the last child.
502 $varref .= "['" . $blocks[0] . "']";
504 $token = "isset($varref) && sizeof($varref)";
515 private function compile_tag_if($tag_args, $elseif)
517 $tokens = $this->compile_expression($tag_args);
519 // @todo We suppress notices within IF statements until we find a way to correctly check them
520 $tpl = ($elseif) ?
'} else if (@(' : 'if (@(';
521 $tpl .= implode(' ', $tokens);
528 * Compile DEFINE tags
531 private function compile_tag_define($tag_args, $op)
534 preg_match('#^((?:[a-z0-9\-_]+\.)+)?\$(?=[A-Z])([A-Z0-9_\-]*)(?: = (.*?))?$#', $tag_args, $match);
536 if (empty($match[2]) ||
(!isset($match[3]) && $op))
543 return 'unset(' . (($match[1]) ?
$this->generate_block_data_ref(substr($match[1], 0, -1), true, true) . '[\'' . $match[2] . '\']' : '$_tpldata[\'DEFINE\'][\'.\'][\'' . $match[2] . '\']') . ');';
546 $parsed_statement = implode(' ', $this->compile_expression($match[3]));
548 return (($match[1]) ?
$this->generate_block_data_ref(substr($match[1], 0, -1), true, true) . '[\'' . $match[2] . '\']' : '$_tpldata[\'DEFINE\'][\'.\'][\'' . $match[2] . '\']') . ' = ' . $parsed_statement . ';';
552 * Compile INCLUDE tag
555 private function compile_tag_include($tag_args)
557 return "\$this->_tpl_include('$tag_args');";
561 * Compile INCLUDE_PHP tag
564 private function compile_tag_include_php($tag_args)
566 return "include('" . $tag_args . "');";
571 * This is from Smarty
574 private function _parse_is_expr($is_arg, $tokens)
577 $negate_expr = false;
579 if (($first_token = array_shift($tokens)) == 'not')
582 $expr_type = array_shift($tokens);
586 $expr_type = $first_token;
592 if (@$tokens[$expr_end] == 'by')
595 $expr_arg = $tokens[$expr_end++
];
596 $expr = "!(($is_arg / $expr_arg) & 1)";
600 $expr = "!($is_arg & 1)";
605 if (@$tokens[$expr_end] == 'by')
608 $expr_arg = $tokens[$expr_end++
];
609 $expr = "(($is_arg / $expr_arg) & 1)";
613 $expr = "($is_arg & 1)";
618 if (@$tokens[$expr_end] == 'by')
621 $expr_arg = $tokens[$expr_end++
];
622 $expr = "!($is_arg % $expr_arg)";
632 array_splice($tokens, 0, $expr_end, $expr);
638 * Generates a reference to the given variable inside the given (possibly nested)
639 * block namespace. This is a string of the form:
640 * ' . $_tpldata['parent'][$_parent_i]['$child1'][$_child1_i]['$child2'][$_child2_i]...['varname'] . '
641 * It's ready to be inserted into an "echo" line in one of the templates.
644 * @param string $namespace Namespace to access (expects a trailing "." on the namespace)
645 * @param string $varname Variable name to use
646 * @param bool $echo If true return an echo statement, otherwise a reference to the internal variable
647 * @param bool $defop If true this is a variable created with the DEFINE construct, otherwise template variable
648 * @return string Code to access variable or echo it if $echo is true
650 private function generate_block_varref($namespace, $varname, $echo = true, $defop = false)
652 // Strip the trailing period.
653 $namespace = substr($namespace, 0, -1);
657 // S_ROW_COUNT is deceptive, it returns the current row number now the number of rows
658 // hence S_ROW_COUNT is deprecated in favour of S_ROW_NUM
663 $varref = "\$_${namespace}_i";
667 $varref = "\$_${namespace}_count";
671 $varref = "(\$_${namespace}_i == 0)";
675 $varref = "(\$_${namespace}_i == \$_${namespace}_count - 1)";
679 $varref = "'$namespace'";
683 // Get a reference to the data block for this namespace.
684 $varref = $this->generate_block_data_ref($namespace, true, $defop);
685 // Prepend the necessary code to stick this in an echo line.
687 // Append the variable reference.
688 $varref .= "['$varname']";
693 // @todo Test the !$expr more
694 $varref = ($echo) ?
"<?php echo $varref; ?>" : (($expr ||
isset($varref)) ?
$varref : '');
700 * Generates a reference to the array of data values for the given
701 * (possibly nested) block namespace. This is a string of the form:
702 * $_tpldata['parent'][$_parent_i]['$child1'][$_child1_i]['$child2'][$_child2_i]...['$childN']
705 * @param string $blockname Block to access (does not expect a trailing "." on the blockname)
706 * @param bool $include_last_iterator If $include_last_iterator is true, then [$_childN_i] will be appended to the form shown above.
707 * @param bool $defop If true this is a variable created with the DEFINE construct, otherwise template variable
708 * @return string Code to access variable
710 private function generate_block_data_ref($blockname, $include_last_iterator, $defop = false)
712 // Get an array of the blocks involved.
713 $blocks = explode('.', $blockname);
714 $blockcount = sizeof($blocks) - 1;
716 // DEFINE is not an element of any referenced variable, we must use _tpldata to access it
719 $varref = '$_tpldata[\'DEFINE\']';
720 // Build up the string with everything but the last child.
721 for ($i = 0; $i < $blockcount; $i++
)
723 $varref .= "['" . $blocks[$i] . "'][\$_" . $blocks[$i] . '_i]';
725 // Add the block reference for the last child.
726 $varref .= "['" . $blocks[$blockcount] . "']";
727 // Add the iterator for the last child if requried.
728 if ($include_last_iterator)
730 $varref .= '[$_' . $blocks[$blockcount] . '_i]';
734 else if ($include_last_iterator)
736 return '$_'. $blocks[$blockcount] . '_val';
740 return '$_'. $blocks[$blockcount - 1] . '_val[\''. $blocks[$blockcount]. '\']';
745 stream_filter_register('phpbb_template', 'phpbb_template_filter');
748 * Extension of template class - Functions needed for compiling templates only.
750 * psoTFX, phpBB Development Team - Completion of file caching, decompilation
751 * routines and implementation of conditionals/keywords and associated changes
753 * The interface was inspired by PHPLib templates, and the template file (formats are
756 * The keyword/conditional implementation is currently based on sections of code from
757 * the Smarty templating engine (c) 2001 ispi of Lincoln, Inc. which is released
758 * (on its own and in whole) under the LGPL. Section 3 of the LGPL states that any code
759 * derived from an LGPL application may be relicenced under the GPL, this applies
762 * DEFINE directive inspired by a request by Cyberalien
765 * @uses template_filter As a PHP stream filter to perform compilation of templates
767 class phpbb_template_compile
770 * @var template Reference to the {@link template template} object performing compilation
776 * @param template $template {@link template Template} object performing compilation
778 function __construct(phpbb_template
$template)
780 $this->template
= $template;
784 * Load template source from file
786 * @param string $handle Template handle we wish to load
787 * @return bool Return true on success otherwise false
789 public function _tpl_load_file($handle)
791 // Try and open template for read
792 if (!file_exists($this->template
->files
[$handle]))
794 trigger_error("template->_tpl_load_file(): File {$this->template->files[$handle]} does not exist or is empty", E_USER_ERROR
);
797 // Actually compile the code now.
798 return $this->compile_write($handle, $this->template
->files
[$handle]);
802 * Load template source from file
804 * @param string $handle Template handle we wish to compile
805 * @return string|bool Return compiled code on successful compilation otherwise false
807 public function _tpl_gen_src($handle)
809 // Try and open template for read
810 if (!file_exists($this->template
->files
[$handle]))
812 trigger_error("template->_tpl_load_file(): File {$this->template->files[$handle]} does not exist or is empty", E_USER_ERROR
);
815 // Actually compile the code now.
816 return $this->compile_gen($this->template
->files
[$handle]);
820 * Write compiled file to cache directory
822 * @param string $handle Template handle to compile
823 * @param string $source_file Source template file
824 * @return bool Return true on success otherwise false
826 private function compile_write($handle, $source_file)
828 $filename = $this->template
->cachepath
. str_replace('/', '.', $this->template
->filename
[$handle]) . '.' . PHP_EXT
;
830 $source_handle = @fopen
($source_file, 'rb');
831 $destination_handle = @fopen
($filename, 'wb');
833 if (!$source_handle ||
!$destination_handle)
838 @flock
($destination_handle, LOCK_EX
);
840 stream_filter_append($source_handle, 'phpbb_template');
841 stream_copy_to_stream($source_handle, $destination_handle);
843 @fclose
($source_handle);
844 @flock
($destination_handle, LOCK_UN
);
845 @fclose
($destination_handle);
847 phpbb
::$system->chmod($filename, phpbb
::CHMOD_READ | phpbb
::CHMOD_WRITE
);
855 * Generate source for eval()
857 * @param string $source_file Source template file
858 * @return string|bool Return compiled code on successful compilation otherwise false
860 private function compile_gen($source_file)
862 $source_handle = @fopen
($source_file, 'rb');
863 $destination_handle = @fopen
('php://temp' ,'r+b');
865 if (!$source_handle ||
!$destination_handle)
870 stream_filter_append($source_handle, 'phpbb_template');
871 stream_copy_to_stream($source_handle, $destination_handle);
873 @fclose
($source_handle);
875 rewind($destination_handle);
876 return stream_get_contents($destination_handle);