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
25 class template_filter
extends php_user_filter
28 * @var string Replaceable tokens regex
30 private $regex = '~<!-- ([A-Z][A-Z_0-9]+)(?: (.*?) ?)?-->|{((?:[a-z][a-z_0-9]+\.)*\\$?[A-Z][A-Z_0-9]+)}~';
35 private $block_names = array();
40 private $block_else_level = array();
47 public function filter($in, $out, &$consumed, $closing)
51 while ($bucket = stream_bucket_make_writeable($in))
53 $consumed +
= $bucket->datalen
;
55 $data = $this->chunk
. $bucket->data
;
56 $last_nl = strrpos($data, "\n");
57 $this->chunk
= substr($data, $last_nl);
58 $data = substr($data, 0, $last_nl);
67 $bucket->data
= $this->compile($data);
68 $bucket->datalen
= strlen($bucket->data
);
69 stream_bucket_append($out, $bucket);
72 if ($closing && strlen($this->chunk
))
75 $bucket = stream_bucket_new($this->stream
, $this->compile($this->chunk
));
76 stream_bucket_append($out, $bucket);
79 return $written ? PSFS_PASS_ON
: PSFS_FEED_ME
;
82 public function onCreate()
88 private function compile($data)
90 $data = preg_replace('#<(?:[\\?%]|script)#s', '<?php echo\'\\0\';?>', $data);
91 return str_replace('?><?php', '', preg_replace_callback($this->regex
, array($this, 'replace'), $data));
94 private function replace($matches)
98 if (isset($matches[3]))
100 return $this->compile_var_tags($matches[0]);
106 $this->block_else_level
[] = false;
107 return '<?php ' . $this->compile_tag_block($matches[2]) . ' ?>';
111 $this->block_else_level
[sizeof($this->block_else_level
) - 1] = true;
112 return '<?php }} else { ?>';
116 array_pop($this->block_names
);
117 return '<?php ' . ((array_pop($this->block_else_level
)) ?
'}' : '}}') . ' ?>';
121 return '<?php ' . $this->compile_tag_if($matches[2], false) . ' ?>';
125 return '<?php } else { ?>';
129 return '<?php ' . $this->compile_tag_if($matches[2], true) . ' ?>';
137 return '<?php ' . $this->compile_tag_define($matches[2], true) . ' ?>';
141 return '<?php ' . $this->compile_tag_define($matches[2], false) . ' ?>';
145 return '<?php ' . $this->compile_tag_include($matches[2]) . ' ?>';
149 return ($config['tpl_allow_php']) ?
'<?php ' . $this->compile_tag_include_php($matches[2]) . ' ?>' : '';
153 return ($config['tpl_allow_php']) ?
'<?php ' : '<!-- ';
157 return ($config['tpl_allow_php']) ?
' ?>' : ' -->';
172 private function compile_var_tags(&$text_blocks)
174 // change template varrefs into PHP varrefs
177 // This one will handle varrefs WITH namespaces
178 preg_match_all('#\{((?:[a-z0-9\-_]+\.)+)(\$)?([A-Z0-9\-_]+)\}#', $text_blocks, $varrefs, PREG_SET_ORDER
);
180 foreach ($varrefs as $var_val)
182 $namespace = $var_val[1];
183 $varname = $var_val[3];
184 $new = $this->generate_block_varref($namespace, $varname, true, $var_val[2]);
186 $text_blocks = str_replace($var_val[0], $new, $text_blocks);
189 // This will handle the remaining root-level varrefs
190 // transform vars prefixed by L_ into their language variable pendant if nothing is set within the tpldata array
191 if (strpos($text_blocks, '{L_') !== false)
193 $text_blocks = preg_replace('#\{L_([a-z0-9\-_]*)\}#is', "<?php echo ((isset(\$_rootref['L_\\1'])) ? \$_rootref['L_\\1'] : ((isset(\$_lang['\\1'])) ? \$_lang['\\1'] : '{ \\1 }')); ?>", $text_blocks);
196 // Handle addslashed language variables prefixed with LA_
197 // If a template variable already exist, it will be used in favor of it...
198 if (strpos($text_blocks, '{LA_') !== false)
200 $text_blocks = preg_replace('#\{LA_([a-z0-9\-_]*)\}#is', "<?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);
203 // Handle remaining varrefs
204 $text_blocks = preg_replace('#\{([a-z0-9\-_]*)\}#is', "<?php echo (isset(\$_rootref['\\1'])) ? \$_rootref['\\1'] : ''; ?>", $text_blocks);
205 $text_blocks = preg_replace('#\{\$([a-z0-9\-_]*)\}#is', "<?php echo (isset(\$_tpldata['DEFINE']['.']['\\1'])) ? \$_tpldata['DEFINE']['.']['\\1'] : ''; ?>", $text_blocks);
214 private function compile_tag_block($tag_args)
218 // Is the designer wanting to call another loop in a loop?
219 // <!-- BEGIN loop -->
220 // <!-- BEGIN !loop2 -->
221 // <!-- END !loop2 -->
223 // 'loop2' is actually on the same nesting level as 'loop' you assign
224 // variables to it with template->assign_block_vars('loop2', array(...))
225 if (strpos($tag_args, '!') === 0)
227 // Count the number if ! occurrences (not allowed in vars)
228 $no_nesting = substr_count($tag_args, '!');
229 $tag_args = substr($tag_args, $no_nesting);
232 // Allow for control of looping (indexes start from zero):
233 // foo(2) : Will start the loop on the 3rd entry
234 // foo(-2) : Will start the loop two entries from the end
235 // foo(3,4) : Will start the loop on the fourth entry and end it on the fifth
236 // foo(3,-4) : Will start the loop on the fourth entry and end it four from last
238 if (preg_match('#^([^()]*)\(([\-\d]+)(?:,([\-\d]+))?\)$#', $tag_args, $match))
240 $tag_args = $match[1];
244 $loop_start = '($_' . $tag_args . '_count ' . $match[2] . ' < 0 ? 0 : $_' . $tag_args . '_count ' . $match[2] . ')';
248 $loop_start = '($_' . $tag_args . '_count < ' . $match[2] . ' ? $_' . $tag_args . '_count : ' . $match[2] . ')';
251 if (strlen($match[3]) < 1 ||
$match[3] == -1)
253 $loop_end = '$_' . $tag_args . '_count';
255 else if ($match[3] >= 0)
257 $loop_end = '(' . ($match[3] +
1) . ' > $_' . $tag_args . '_count ? $_' . $tag_args . '_count : ' . ($match[3] +
1) . ')';
259 else //if ($match[3] < -1)
261 $loop_end = '$_' . $tag_args . '_count' . ($match[3] +
1);
267 $loop_end = '$_' . $tag_args . '_count';
270 $tag_template_php = '';
271 array_push($this->block_names
, $tag_args);
273 if ($no_nesting !== false)
275 // We need to implode $no_nesting times from the end...
276 $block = array_slice($this->block_names
, -$no_nesting);
280 $block = $this->block_names
;
283 if (sizeof($block) < 2)
285 // Block is not nested.
286 $tag_template_php = '$_' . $tag_args . "_count = (isset(\$_tpldata['$tag_args'])) ? sizeof(\$_tpldata['$tag_args']) : 0;";
287 $varref = "\$_tpldata['$tag_args']";
291 // This block is nested.
292 // Generate a namespace string for this block.
293 $namespace = implode('.', $block);
295 // Get a reference to the data array for this block that depends on the
296 // current indices of all parent blocks.
297 $varref = $this->generate_block_data_ref($namespace, false);
299 // Create the for loop code to iterate over this block.
300 $tag_template_php = '$_' . $tag_args . '_count = (isset(' . $varref . ')) ? sizeof(' . $varref . ') : 0;';
303 $tag_template_php .= 'if ($_' . $tag_args . '_count) {';
306 * 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
310 * $tag_template_php .= 'foreach (' . $varref . ' as $_' . $tag_args . '_i => $_' . $tag_args . '_val){';
315 $tag_template_php .= 'for ($_' . $tag_args . '_i = ' . $loop_start . '; $_' . $tag_args . '_i < ' . $loop_end . '; ++$_' . $tag_args . '_i){';
316 $tag_template_php .= '$_'. $tag_args . '_val = &' . $varref . '[$_'. $tag_args. '_i];';
318 return $tag_template_php;
322 * Compile a general expression - much of this is from Smarty with
323 * some adaptions for our block level methods
326 private function compile_expression($tag_args)
330 "[^"\\\\]*(?:\\\\.[^"\\\\]*)*" |
331 \'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\' |
333 [^\s(),]+)/x', $tag_args, $match);
336 $is_arg_stack = array();
338 for ($i = 0, $size = sizeof($tokens); $i < $size; $i++
)
340 $token = &$tokens[$i];
416 array_push($is_arg_stack, $i);
420 $is_arg_start = ($tokens[$i-1] == ')') ?
array_pop($is_arg_stack) : $i-1;
421 $is_arg = implode(' ', array_slice($tokens, $is_arg_start, $i - $is_arg_start));
423 $new_tokens = $this->_parse_is_expr($is_arg, array_slice($tokens, $i+
1));
425 array_splice($tokens, $is_arg_start, sizeof($tokens), $new_tokens);
433 if (preg_match('#^((?:[a-z0-9\-_]+\.)+)?(\$)?(?=[A-Z])([A-Z0-9\-_]+)#s', $token, $varrefs))
435 if (!empty($varrefs[1]))
437 $namespace = substr($varrefs[1], 0, -1);
438 $namespace = (strpos($namespace, '.') === false) ?
$namespace : strrchr($namespace, '.');
440 // S_ROW_COUNT is deceptive, it returns the current row number not the number of rows
441 // hence S_ROW_COUNT is deprecated in favour of S_ROW_NUM
446 $token = "\$_${namespace}_i";
450 $token = "\$_${namespace}_count";
454 $token = "(\$_${namespace}_i == 0)";
458 $token = "(\$_${namespace}_i == \$_${namespace}_count - 1)";
462 $token = "'$namespace'";
466 $token = $this->generate_block_data_ref(substr($varrefs[1], 0, -1), true, $varrefs[2]) . '[\'' . $varrefs[3] . '\']';
472 $token = ($varrefs[2]) ?
'$_tpldata[\'DEFINE\'][\'.\'][\'' . $varrefs[3] . '\']' : '$_rootref[\'' . $varrefs[3] . '\']';
475 else if (preg_match('#^\.((?:[a-z0-9\-_]+\.?)+)$#s', $token, $varrefs))
477 // Allow checking if loops are set with .loopname
478 // It is also possible to check the loop count by doing <!-- IF .loopname > 1 --> for example
479 $blocks = explode('.', $varrefs[1]);
481 // If the block is nested, we have a reference that we can grab.
482 // If the block is not nested, we just go and grab the block from _tpldata
483 if (sizeof($blocks) > 1)
485 $block = array_pop($blocks);
486 $namespace = implode('.', $blocks);
487 $varref = $this->generate_block_data_ref($namespace, true);
489 // Add the block reference for the last child.
490 $varref .= "['" . $block . "']";
494 $varref = '$_tpldata';
496 // Add the block reference for the last child.
497 $varref .= "['" . $blocks[0] . "']";
499 $token = "isset($varref) && sizeof($varref)";
510 private function compile_tag_if($tag_args, $elseif)
512 $tokens = $this->compile_expression($tag_args);
513 return (($elseif) ?
'} else if (' : 'if (') . (implode(' ', $tokens) . ') { ');
517 * Compile DEFINE tags
520 private function compile_tag_define($tag_args, $op)
523 preg_match('#^((?:[a-z0-9\-_]+\.)+)?\$(?=[A-Z])([A-Z0-9_\-]*)(?: = (.*?))?$#', $tag_args, $match);
525 if (empty($match[2]) ||
(!isset($match[3]) && $op))
532 return 'unset(' . (($match[1]) ?
$this->generate_block_data_ref(substr($match[1], 0, -1), true, true) . '[\'' . $match[2] . '\']' : '$_tpldata[\'DEFINE\'][\'.\'][\'' . $match[2] . '\']') . ');';
535 $parsed_statement = implode(' ', $this->compile_expression($match[3]));
537 return (($match[1]) ?
$this->generate_block_data_ref(substr($match[1], 0, -1), true, true) . '[\'' . $match[2] . '\']' : '$_tpldata[\'DEFINE\'][\'.\'][\'' . $match[2] . '\']') . ' = ' . $parsed_statement . ';';
541 * Compile INCLUDE tag
544 private function compile_tag_include($tag_args)
546 return "\$this->_tpl_include('$tag_args');";
550 * Compile INCLUDE_PHP tag
553 private function compile_tag_include_php($tag_args)
555 return "include('" . $tag_args . "');";
560 * This is from Smarty
563 private function _parse_is_expr($is_arg, $tokens)
566 $negate_expr = false;
568 if (($first_token = array_shift($tokens)) == 'not')
571 $expr_type = array_shift($tokens);
575 $expr_type = $first_token;
581 if (@$tokens[$expr_end] == 'by')
584 $expr_arg = $tokens[$expr_end++
];
585 $expr = "!(($is_arg / $expr_arg) & 1)";
589 $expr = "!($is_arg & 1)";
594 if (@$tokens[$expr_end] == 'by')
597 $expr_arg = $tokens[$expr_end++
];
598 $expr = "(($is_arg / $expr_arg) & 1)";
602 $expr = "($is_arg & 1)";
607 if (@$tokens[$expr_end] == 'by')
610 $expr_arg = $tokens[$expr_end++
];
611 $expr = "!($is_arg % $expr_arg)";
621 array_splice($tokens, 0, $expr_end, $expr);
627 * Generates a reference to the given variable inside the given (possibly nested)
628 * block namespace. This is a string of the form:
629 * ' . $_tpldata['parent'][$_parent_i]['$child1'][$_child1_i]['$child2'][$_child2_i]...['varname'] . '
630 * It's ready to be inserted into an "echo" line in one of the templates.
633 * @param string $namespace Namespace to access (expects a trailing "." on the namespace)
634 * @param string $varname Variable name to use
635 * @param bool $echo If true return an echo statement, otherwise a reference to the internal variable
636 * @param bool $defop If true this is a variable created with the DEFINE construct, otherwise template variable
637 * @return string Code to access variable or echo it if $echo is true
639 private function generate_block_varref($namespace, $varname, $echo = true, $defop = false)
641 // Strip the trailing period.
642 $namespace = substr($namespace, 0, -1);
646 // S_ROW_COUNT is deceptive, it returns the current row number not the number of rows
647 // hence S_ROW_COUNT is deprecated in favour of S_ROW_NUM
652 $varref = "\$_${namespace}_i";
656 $varref = "\$_${namespace}_count";
660 $varref = "(\$_${namespace}_i == 0)";
664 $varref = "(\$_${namespace}_i == \$_${namespace}_count - 1)";
668 $varref = "'$namespace'";
672 // Get a reference to the data block for this namespace.
673 $varref = $this->generate_block_data_ref($namespace, true, $defop);
674 // Prepend the necessary code to stick this in an echo line.
676 // Append the variable reference.
677 $varref .= "['$varname']";
682 // @todo Test the !$expr more
683 $varref = ($echo) ?
"<?php echo $varref; ?>" : (($expr ||
isset($varref)) ?
$varref : '');
689 * Generates a reference to the array of data values for the given
690 * (possibly nested) block namespace. This is a string of the form:
691 * $_tpldata['parent'][$_parent_i]['$child1'][$_child1_i]['$child2'][$_child2_i]...['$childN']
694 * @param string $blockname Block to access (does not expect a trailing "." on the blockname)
695 * @param bool $include_last_iterator If $include_last_iterator is true, then [$_childN_i] will be appended to the form shown above.
696 * @param bool $defop If true this is a variable created with the DEFINE construct, otherwise template variable
697 * @return string Code to access variable
699 private function generate_block_data_ref($blockname, $include_last_iterator, $defop = false)
701 // Get an array of the blocks involved.
702 $blocks = explode('.', $blockname);
703 $blockcount = sizeof($blocks) - 1;
705 // DEFINE is not an element of any referenced variable, we must use _tpldata to access it
708 $varref = '$_tpldata[\'DEFINE\']';
709 // Build up the string with everything but the last child.
710 for ($i = 0; $i < $blockcount; $i++
)
712 $varref .= "['" . $blocks[$i] . "'][\$_" . $blocks[$i] . '_i]';
714 // Add the block reference for the last child.
715 $varref .= "['" . $blocks[$blockcount] . "']";
716 // Add the iterator for the last child if requried.
717 if ($include_last_iterator)
719 $varref .= '[$_' . $blocks[$blockcount] . '_i]';
723 else if ($include_last_iterator)
725 return '$_'. $blocks[$blockcount] . '_val';
729 return '$_'. $blocks[$blockcount - 1] . '_val[\''. $blocks[$blockcount]. '\']';
734 stream_filter_register('template', 'template_filter');
737 * Extension of template class - Functions needed for compiling templates only.
739 * psoTFX, phpBB Development Team - Completion of file caching, decompilation
740 * routines and implementation of conditionals/keywords and associated changes
742 * The interface was inspired by PHPLib templates, and the template file (formats are
745 * The keyword/conditional implementation is currently based on sections of code from
746 * the Smarty templating engine (c) 2001 ispi of Lincoln, Inc. which is released
747 * (on its own and in whole) under the LGPL. Section 3 of the LGPL states that any code
748 * derived from an LGPL application may be relicenced under the GPL, this applies
751 * DEFINE directive inspired by a request by Cyberalien
754 * @uses template_filter As a PHP stream filter to perform compilation of templates
756 class template_compile
759 * @var template Reference to the {@link template template} object performing compilation
765 * @param template $template {@link template Template} object performing compilation
767 function __construct(template
$template)
769 $this->template
= $template;
773 * Load template source from file
775 * @param string $handle Template handle we wish to load
776 * @return bool Return true on success otherwise false
778 public function _tpl_load_file($handle)
780 // Try and open template for read
781 if (!file_exists($this->template
->files
[$handle]))
783 trigger_error("template->_tpl_load_file(): File {$this->template->files[$handle]} does not exist or is empty", E_USER_ERROR
);
786 // Actually compile the code now.
787 return $this->compile_write($handle, $this->template
->files
[$handle]);
791 * Load template source from file
793 * @param string $handle Template handle we wish to compile
794 * @return string|bool Return compiled code on successful compilation otherwise false
796 public function _tpl_gen_src($handle)
798 // Try and open template for read
799 if (!file_exists($this->template
->files
[$handle]))
801 trigger_error("template->_tpl_load_file(): File {$this->template->files[$handle]} does not exist or is empty", E_USER_ERROR
);
804 // Actually compile the code now.
805 return $this->compile_gen($this->template
->files
[$handle]);
809 * Write compiled file to cache directory
811 * @param string $handle Template handle to compile
812 * @param string $source_file Source template file
813 * @return bool Return true on success otherwise false
815 private function compile_write($handle, $source_file)
817 $filename = $this->template
->cachepath
. str_replace('/', '.', $this->template
->filename
[$handle]) . '.' . PHP_EXT
;
819 $source_handle = @fopen
($source_file, 'rb');
820 $destination_handle = @fopen
($filename, 'wb');
822 if (!$source_handle ||
!$destination_handle)
827 @flock
($destination_handle, LOCK_EX
);
829 stream_filter_append($source_handle, 'template');
830 stream_copy_to_stream($source_handle, $destination_handle);
832 @fclose
($source_handle);
833 @flock
($destination_handle, LOCK_UN
);
834 @fclose
($destination_handle);
836 phpbb_chmod($filename, CHMOD_WRITE
);
844 * Generate source for eval()
846 * @param string $source_file Source template file
847 * @return string|bool Return compiled code on successful compilation otherwise false
849 private function compile_gen($source_file)
851 $source_handle = @fopen
($source_file, 'rb');
852 $destination_handle = @fopen
('php://temp' ,'r+b');
854 if (!$source_handle ||
!$destination_handle)
859 stream_filter_append($source_handle, 'template');
860 stream_copy_to_stream($source_handle, $destination_handle);
862 @fclose
($source_handle);
864 rewind($destination_handle);
865 return stream_get_contents($destination_handle);