<?php
/* -*- tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
# ***** BEGIN LICENSE BLOCK *****
# This file is part of Plume Framework, a simple PHP Application Framework.
# Copyright (C) 2001-2007 Loic d'Anterroches and contributors.
#
# Plume Framework is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# Plume Framework is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
#
# ***** END LICENSE BLOCK ***** */
/**
* Class to compile a template file into the corresponding PHP script
* to be run by the Template class.
*
* Compiler dataflow
*
* The important elements of the compiler are the include extends
* block and superblock directives. They cannot be handled in a linear
* way like the rest of the elements, they are more like nodes.
*
*
* @credit Copyright (C) 2006 Laurent Jouanneau.
*/
class Pluf_Template_Compiler
{
/**
* Store the literal blocks.
**/
protected $_literals;
/**
* Variables.
*/
protected $_vartype = array(T_CHARACTER, T_CONSTANT_ENCAPSED_STRING,
T_DNUMBER, T_ENCAPSED_AND_WHITESPACE,
T_LNUMBER, T_OBJECT_OPERATOR, T_STRING,
T_WHITESPACE, T_ARRAY, T_CLASS);
/**
* Assignation operators.
*/
protected $_assignOp = array(T_AND_EQUAL, T_DIV_EQUAL, T_MINUS_EQUAL,
T_MOD_EQUAL, T_MUL_EQUAL, T_OR_EQUAL,
T_PLUS_EQUAL, T_PLUS_EQUAL, T_SL_EQUAL,
T_SR_EQUAL, T_XOR_EQUAL);
/**
* Operators.
*/
protected $_op = array(T_BOOLEAN_AND, T_BOOLEAN_OR, T_EMPTY, T_INC,
T_ISSET, T_IS_EQUAL, T_IS_GREATER_OR_EQUAL,
T_IS_IDENTICAL, T_IS_NOT_EQUAL, T_IS_NOT_IDENTICAL,
T_IS_SMALLER_OR_EQUAL, T_LOGICAL_AND, T_LOGICAL_OR,
T_LOGICAL_XOR, T_SR, T_SL, T_DOUBLE_ARROW);
/**
* Authorized elements in variables.
*/
protected $_allowedInVar;
/**
* Authorized elements in expression.
*/
protected $_allowedInExpr;
/**
* Authorized elements in assignation.
*/
protected $_allowedAssign;
/**
* Output filters.
*/
protected $_modifier = array('upper' => 'strtoupper',
'lower' => 'strtolower',
'count' => 'count',
'md5' => 'md5',
'escxml' => 'htmlspecialchars',
'escape' => 'Pluf_Template_htmlspecialchars',
'strip_tags' => 'strip_tags',
'escurl' => 'rawurlencode',
'capitalize' => 'ucwords',
// Not var_export because of recursive issues.
'debug' => 'print_r',
'dump' => 'Pluf_Template_varExport',
'fulldebug' => 'var_export',
'count' => 'count',
'nl2br' => 'Pluf_Template_nl2br',
'trim' => 'trim',
'unsafe' => 'Pluf_Template_unsafe',
'safe' => 'Pluf_Template_unsafe',
'date' => 'Pluf_Template_dateFormat',
'time' => 'Pluf_Template_timeFormat',
'dateago' => 'Pluf_Template_dateAgo',
'timeago' => 'Pluf_Template_timeAgo',
);
/**
* After the compilation is completed, this contains the list of
* modifiers used in the template. The GetCompiledTemplate method
* will add a series of Pluf::loadFunction at the top to preload
* these modifiers.
*/
public $_usedModifiers = array();
/**
* Default allowed extra tags/functions.
*
* These default tags are merged with the 'template_tags' defined
* in the configuration of the application.
*/
protected $_allowedTags = array(
'url' => 'Pluf_Template_Tag_Url',
'aurl' => 'Pluf_Template_Tag_Rurl',
'media' => 'Pluf_Template_Tag_MediaUrl',
'amedia' => 'Pluf_Template_Tag_RmediaUrl',
'getmsgs' => 'Pluf_Template_Tag_Messages',
);
/**
* During compilation, all the tags are created once so to query
* their interface easily.
*/
protected $_extraTags = array();
/**
* The block stack to see if the blocks are correctly closed.
*/
protected $_blockStack = array();
/**
* Special stack for the translation handling in blocktrans.
*/
protected $_transStack = array();
protected $_transPlural = false;
/**
* Current template source file.
*/
protected $_sourceFile;
/**
* Current tag.
*/
protected $_currentTag;
/**
* Template folders.
*/
public $templateFolders = array();
/**
* Template content. It can be set directly from a string.
*/
public $templateContent = '';
/**
* The extend blocks.
*/
public $_extendBlocks = array();
/**
* The extended template.
*/
public $_extendedTemplate = '';
/**
* Construct the compiler.
*
* @param string Basename of the template file.
* @param array Base folders in which the templates files
* should be found. (array())
* @param bool Load directly the template content. (true)
*/
function __construct($template_file, $folders=array(), $load=true)
{
$allowedtags = Pluf::f('template_tags', array());
$this->_allowedTags = array_merge($allowedtags, $this->_allowedTags);
$modifiers = Pluf::f('template_modifiers', array());
$this->_modifier = array_merge($modifiers, $this->_modifier);
foreach ($this->_allowedTags as $name=>$model) {
$this->_extraTags[$name] = new $model();
}
$this->_sourceFile = $template_file;
$this->_allowedInVar = array_merge($this->_vartype, $this->_op);
$this->_allowedInExpr = array_merge($this->_vartype, $this->_op);
$this->_allowedAssign = array_merge($this->_vartype, $this->_assignOp,
$this->_op);
$this->templateFolders = $folders;
if ($load) {
$this->loadTemplateFile($template_file);
}
}
/**
* Compile the template into a PHP code.
*
* @return string PHP code of the compiled template.
*/
function compile()
{
$this->compileBlocks();
$tplcontent = $this->templateContent;
// Remove the template comments
$tplcontent = preg_replace('!{\*(.*?)\*}!s', '', $tplcontent);
// Remove PHP code
$tplcontent = preg_replace('!<\?php(.*?)\?>!s', '', $tplcontent);
// Catch the litteral blocks and put them in the
// $this->_literals stack
preg_match_all('!{literal}(.*?){/literal}!s', $tplcontent, $_match);
$this->_literals = $_match[1];
$tplcontent = preg_replace("!{literal}(.*?){/literal}!s", '{literal}', $tplcontent);
// Core regex to parse the template
$result = preg_replace_callback('/{((.).*?)}/s',
array($this, '_callback'),
$tplcontent);
if (count($this->_blockStack)) {
trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR);
}
return $result;
}
/**
* Get a cleaned compile template.
*
*/
function getCompiledTemplate()
{
$result = $this->compile();
if (count($this->_usedModifiers)) {
$code = array();
foreach ($this->_usedModifiers as $modifier) {
$code[] = 'Pluf::loadFunction(\''.$modifier.'\'); ';
}
$result = '<?php '.implode("\n", $code).'?>'.$result;
}
// Clean the output
$result = str_replace(array('?><?php', '<?php ?>', '<?php ?>'), '', $result);
// To avoid the triming of the \n after a php closing tag.
$result = str_replace("?>\n", "?>\n\n", $result);
return $result;
}
/**
* Parse the extend blocks.
*
* If the current template extends another, it finds the extended
* template and grabs the defined blocks and compile them.
*/
function compileBlocks()
{
$tplcontent = $this->templateContent;
$this->_extendedTemplate = '';
// Match extends on the first line of the template
if (preg_match("!{extends\s['\"](.*?)['\"]}!", $tplcontent, $_match)) {
$this->_extendedTemplate = $_match[1];
}
// Get the blocks in the current template
$cnt = preg_match_all("!{block\s(\S+?)}(.*?){/block}!s", $tplcontent, $_match);
// Compile the blocks
for ($i=0; $i<$cnt; $i++) {
if (!isset($this->_extendBlocks[$_match[1][$i]])
or false !== strpos($this->_extendBlocks[$_match[1][$i]], '~~{~~superblock~~}~~')) {
$compiler = clone($this);
$compiler->templateContent = $_match[2][$i];
$_tmp = $compiler->compile();
$this->updateModifierStack($compiler);
if (!isset($this->_extendBlocks[$_match[1][$i]])) {
$this->_extendBlocks[$_match[1][$i]] = $_tmp;
} else {
$this->_extendBlocks[$_match[1][$i]] = str_replace('~~{~~superblock~~}~~', $_tmp, $this->_extendBlocks[$_match[1][$i]]);
}
}
}
if (strlen($this->_extendedTemplate) > 0) {
// The template of interest is now the extended template
// as we are not in a base template
$this->loadTemplateFile($this->_extendedTemplate);
$this->_sourceFile = $this->_extendedTemplate;
$this->compileBlocks(); //It will recurse to the base template.
} else {
// Replace the current blocks by a place holder
if ($cnt) {
$this->templateContent = preg_replace("!{block\s(\S+?)}(.*?){/block}!s", "{block $1}", $tplcontent, -1);
}
}
}
/**
* Load a template file.
*
* The path to the file to load is relative and the file is found
* in one of the $templateFolders array of folders.
*
* @param string Relative path of the file to load.
*/
function loadTemplateFile($file)
{
// FIXME: Very small security check, could be better.
if (strpos($file, '..') !== false) {
throw new Exception(sprintf(__('Template file contains invalid characters: %s'), $file));
}
foreach ($this->templateFolders as $folder) {
if (file_exists($folder.'/'.$file)) {
$this->templateContent = file_get_contents($folder.'/'.$file);
return;
}
}
// File not found in all the folders.
throw new Exception(sprintf(__('Template file not found: %s'), $file));
}
function _callback($matches)
{
list(,$tag, $firstcar) = $matches;
if (!preg_match('/^\$|[\'"]|[a-zA-Z\/]$/', $firstcar)) {
trigger_error(sprintf(__('Invalid tag syntax: %s'), $tag), E_USER_ERROR);
return '';
}
$this->_currentTag = $tag;
if (in_array($firstcar, array('$', '\'', '"'))) {
if ('blocktrans' !== end($this->_blockStack)) {
return '<?php Pluf_Template_safeEcho('.$this->_parseVariable($tag).'); ?>';
} else {
$tok = explode('|', $tag);
$this->_transStack[substr($tok[0], 1)] = $this->_parseVariable($tag);
return '%%'.substr($tok[0], 1).'%%';
}
} else {
if (!preg_match('/^(\/?[a-zA-Z0-9_]+)(?:(?:\s+(.*))|(?:\((.*)\)))?$/', $tag, $m)) {
trigger_error(sprintf(__('Invalid function syntax: %s'), $tag), E_USER_ERROR);
return '';
}
if (count($m) == 4){
$m[2] = $m[3];
}
if (!isset($m[2])) $m[2] = '';
if($m[1] == 'ldelim') return '{';
if($m[1] == 'rdelim') return '}';
if ($m[1] != 'include') {
return '<?php '.$this->_parseFunction($m[1], $m[2]).'?>';
} else {
return $this->_parseFunction($m[1], $m[2]);
}
}
}
function _parseVariable($expr)
{
$tok = explode('|', $expr);
$res = $this->_parseFinal(array_shift($tok), $this->_allowedInVar);
foreach ($tok as $modifier) {
if (!preg_match('/^(\w+)(?:\:(.*))?$/', $modifier, $m)) {
trigger_error(sprintf(__('Invalid modifier syntax: (%s) %s'), $this->_currentTag, $modifier), E_USER_ERROR);
return '';
}
$targs = array($res);
if(isset($m[2])){
$res = $this->_modifier[$m[1]].'('.$res.','.$m[2].')';
}
else if (isset($this->_modifier[$m[1]])) {
$res = $this->_modifier[$m[1]].'('.$res.')';
} else {
trigger_error(sprintf(__('Unknown modifier: (%s) %s'), $this->_currentTag, $m[1]), E_USER_ERROR);
return '';
}
if (!in_array($this->_modifier[$m[1]], $this->_usedModifiers)) {
$this->_usedModifiers[] = $this->_modifier[$m[1]];
}
}
return $res;
}
function _parseFunction($name, $args)
{
switch ($name) {
case 'if':
$res = 'if ('.$this->_parseFinal($args, $this->_allowedInExpr).'): ';
array_push($this->_blockStack, 'if');
break;
case 'else':
if (end($this->_blockStack) != 'if') {
trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR);
}
$res = 'else: ';
break;
case 'elseif':
if (end($this->_blockStack) != 'if') {
trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR);
}
$res = 'elseif('.$this->_parseFinal($args, $this->_allowedInExpr).'):';
break;
case 'foreach':
$res = 'foreach ('.$this->_parseFinal($args, array_merge(array(T_AS, T_DOUBLE_ARROW, T_STRING, T_OBJECT_OPERATOR, $this->_allowedAssign, '[', ']')), array(';','!')).'): ';
array_push($this->_blockStack, 'foreach');
break;
case 'while':
$res = 'while('.$this->_parseFinal($args,$this->_allowedInExpr).'):';
array_push($this->_blockStack, 'while');
break;
case '/foreach':
case '/if':
case '/while':
$short = substr($name,1);
if(end($this->_blockStack) != $short){
trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR);
}
array_pop($this->_blockStack);
$res = 'end'.$short.'; ';
break;
case 'assign':
$res = $this->_parseFinal($args, $this->_allowedAssign).'; ';
break;
case 'literal':
if(count($this->_literals)){
$res = '?>'.array_shift($this->_literals).'<?php ';
}else{
trigger_error(__('End tag of a block missing: literal'), E_USER_ERROR);
}
break;
case '/literal':
trigger_error(__('Start tag of a block missing: literal'), E_USER_ERROR);
break;
case 'block':
$res = '?>'.$this->_extendBlocks[$args].'<?php ';
break;
case 'superblock':
$res = '?>~~{~~superblock~~}~~<?php ';
break;
case 'trans':
$argfct = $this->_parseFinal($args, $this->_allowedAssign);
$res = 'echo(__('.$argfct.'));';
break;
case 'blocktrans':
array_push($this->_blockStack, 'blocktrans');
$res = '';
$this->_transStack = array();
if ($args) {
$this->_transPlural = true;
$_args = $this->_parseFinal($args, $this->_allowedAssign,
array(';', '[', ']'), true);
$res .= '$_b_t_c='.trim(array_shift($_args)).'; ';
}
$res .= 'ob_start(); ';
break;
case '/blocktrans':
$short = substr($name,1);
if(end($this->_blockStack) != $short){
trigger_error(sprintf(__('End tag of a block missing: %s'), end($this->_blockStack)), E_USER_ERROR);
}
$res = '';
if ($this->_transPlural) {
$res .= '$_b_t_p=ob_get_contents(); ob_end_clean(); echo(';
$res .= 'Pluf_Translation::sprintf(_n($_b_t_s, $_b_t_p, $_b_t_c), array(';
$_tmp = array();
foreach ($this->_transStack as $key=>$_trans) {
$_tmp[] = '\''.addslashes($key).'\' => Pluf_Template_safeEcho('.$_trans.', false)';
}
$res .= implode(', ', $_tmp);
unset($_trans, $_tmp);
$res .= ')));';
$this->_transStack = array();
} else {
$res .= '$_b_t_s=ob_get_contents(); ob_end_clean(); ';
if (count($this->_transStack) == 0) {
$res .= 'echo(__($_b_t_s)); ';
} else {
$res .= 'echo(Pluf_Translation::sprintf(__($_b_t_s), array(';
$_tmp = array();
foreach ($this->_transStack as $key=>$_trans) {
$_tmp[] = '\''.addslashes($key).'\' => Pluf_Template_safeEcho('.$_trans.', false)';
}
$res .= implode(', ', $_tmp);
unset($_trans, $_tmp);
$res .= '))); ';
$this->_transStack = array();
}
}
$this->_transPlural = false;
array_pop($this->_blockStack);
break;
case 'plural':
$res = '$_b_t_s=ob_get_contents(); ob_end_clean(); ob_start(); ';
break;
case 'include':
// XXX fixme: Will need some security check, when online editing.
$argfct = preg_replace('!^[\'"](.*)[\'"]$!', '$1', $args);
$_comp = new Pluf_Template_Compiler($argfct, $this->templateFolders);
$res = $_comp->compile();
$this->updateModifierStack($_comp);
break;
default:
$_end = false;
$oname = $name;
if (substr($name, 0, 1) == '/') {
$_end = true;
$name = substr($name, 1);
}
// Here we should allow custom blocks.
// Here we start the template tag calls at the template tag
// {tag ...} is not a block, so it must be a function.
if (!isset($this->_allowedTags[$name])) {
trigger_error(sprintf(__('The function tag "%s" is not allowed.'), $name), E_USER_ERROR);
}
$argfct = $this->_parseFinal($args, $this->_allowedAssign);
// $argfct is a string that can be copy/pasted in the PHP code
// but we need the array of args.
$res = '';
if (isset($this->_extraTags[$name])) {
if (false == $_end) {
if (method_exists($this->_extraTags[$name], 'start')) {
$res .= '$_extra_tag = Pluf::factory(\''.$this->_allowedTags[$name].'\', $t); $_extra_tag->start('.$argfct.'); ';
}
if (method_exists($this->_extraTags[$name], 'genStart')) {
$res .= $this->_extraTags[$name]->genStart();
}
} else {
if (method_exists($this->_extraTags[$name], 'end')) {
$res .= '$_extra_tag = Pluf::factory(\''.$this->_allowedTags[$name].'\', $t); $_extra_tag->end('.$argfct.'); ';
}
if (method_exists($this->_extraTags[$name], 'genEnd')) {
$res .= $this->_extraTags[$name]->genEnd();
}
}
}
if ($res == '') {
trigger_error(sprintf(__('The function tag "{%s ...}" is not supported.'), $oname), E_USER_ERROR);
}
}
return $res;
}
/*
-------
if: op, autre, var
foreach: T_AS, T_DOUBLE_ARROW, T_VARIABLE, @locale@
for: autre, fin_instruction
while: op, autre, var
assign: T_VARIABLE puis assign puis autre, ponctuation, T_STRING
echo: T_VARIABLE/@locale@ puis autre + ponctuation
modificateur: serie de autre séparé par une virgule
tous : T_VARIABLE, @locale@
*/
function _parseFinal($string, $allowed=array(),
$exceptchar=array(';'), $getAsArray=false)
{
$tokens = token_get_all('<?php '.$string.'?>');
$result = '';
$first = true;
$inDot = false;
$firstok = array_shift($tokens);
$afterAs = false;
$f_key = '';
$f_val = '';
$results = array();
// il y a un bug, parfois le premier token n'est pas T_OPEN_TAG...
if ($firstok == '<' && $tokens[0] == '?' && is_array($tokens[1])
&& $tokens[1][0] == T_STRING && $tokens[1][1] == 'php') {
array_shift($tokens);
array_shift($tokens);
}
foreach ($tokens as $tok) {
if (is_array($tok)) {
list($type, $str) = $tok;
$first = false;
if($type == T_CLOSE_TAG){
continue;
}
if ($type == T_AS) {
$afterAs = true;
}
if ($type == T_STRING && $inDot) {
$result .= $str;
} elseif ($type == T_VARIABLE) {
$result .= '$t->_vars[\''.substr($str, 1).'\']';
} elseif ($type == T_WHITESPACE || in_array($type, $allowed)) {
$result .= $str;
} else {
trigger_error(sprintf(__('Invalid syntax: (%s) %s.'), $this->_currentTag, $str.' tokens'.var_export($tokens, true)), E_USER_ERROR);
return '';
}
} else {
if (in_array($tok, $exceptchar)) {
trigger_error(sprintf(__('Invalid character: (%s) %s.'), $this->_currentTag, $tok), E_USER_ERROR);
} elseif ($tok == '.') {
$inDot = true;
$result .= '->';
} elseif ($tok == '~') {
$result .= '.';
} elseif ($tok =='[') {
$result.=$tok;
} elseif ($tok ==']') {
$result.=$tok;
} elseif ($getAsArray && $tok == ',') {
$results[]=$result;
$result='';
} else {
$result .= $tok;
}
$first = false;
}
}
if (!$getAsArray) {
return $result;
} else {
if ($result != '') {
$results[] = $result;
}
return $results;
}
}
/**
* Update the current stack of modifiers from another compiler.
*/
protected function updateModifierStack($compiler)
{
foreach ($compiler->_usedModifiers as $_um) {
if (!in_array($_um, $this->_usedModifiers)) {
$this->_usedModifiers[] = $_um;
}
}
}
}