* @copyright Copyright (c) 2008, Jordi Boggiano
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
* @link http://dwoo.org/
* @version 0.3.4
* @date 2008-04-09
* @package Dwoo
*/
class DwooCompiler implements DwooICompiler
{
/**
* constant that represents a php opening tag
*
* use it in case it needs to be adjusted
*
* @var string
*/
const PHP_OPEN = "";
/**
* boolean flag to enable or disable debugging output
*
* @var bool
*/
public $debug = false;
/**
* left script delimiter
*
* @var string
*/
protected $ld = '{';
/**
* left script delimiter with escaped regex meta characters
*
* @var string
*/
protected $ldr = '\\{';
/**
* right script delimiter
*
* @var string
*/
protected $rd = '}';
/**
* right script delimiter with escaped regex meta characters
*
* @var string
*/
protected $rdr = '\\}';
/**
* defines whether opening and closing tags can contain spaces before valid data or not
*
* turn to true if you want to be sloppy with the syntax, but when set to false it allows
* to skip javascript and css tags as long as they are in the form "{ something", which is
* nice. default is false.
*/
protected $allowLooseOpenings = false;
/**
* security policy object
*
* @var DwooSecurityPolicy
*/
protected $securityPolicy;
/**
* storage for parse errors/warnings
*
* will be deprecated when proper exceptions are added
*
* @var array
*/
protected $errors = array();
/**
* stores the custom plugins registered with this compiler
*
* @var array
*/
protected $customPlugins = array();
/**
* stores the pre- and post-processors callbacks
*
* @var array
*/
protected $processors = array('pre'=>array(), 'post'=>array());
/**
* stores a list of plugins that are used in the currently compiled
* template, and that are not compilable. these plugins will be loaded
* during the template's runtime if required.
*
* it is a 1D array formatted as key:pluginName value:pluginType
*
* @var array
*/
protected $usedPlugins;
/**
* stores the template undergoing compilation
*
* @var string
*/
protected $template;
/**
* stores the current pointer position inside the template
*
* @var int
*/
protected $pointer;
/**
* stores the data within which the scope moves
*
* @var array
*/
protected $data;
/**
* variable scope of the compiler, set to null if
* it can not be resolved to a static string (i.e. if some
* plugin defines a new scope based on a variable array key)
*
* @var mixed
*/
protected $scope;
/**
* variable scope tree, that allows to rebuild the current
* scope if required, i.e. when going to a parent level
*
* @var array
*/
protected $scopeTree;
/**
* block plugins stack, accessible through some methods
*
* @see findBlock
* @see getCurrentBlock
* @see addBlock
* @see addCustomBlock
* @see injectBlock
* @see removeBlock
* @see removeTopBlock
*
* @var array
*/
protected $stack = array();
/**
* current block at the top of the block plugins stack,
* accessible through getCurrentBlock
*
* @see getCurrentBlock
*
* @var DwooBlockPlugin
*/
protected $curBlock;
/**
* holds an instance of this class, used by getInstance when you don't
* provide a custom compiler in order to save resources
*
* @var DwooCompiler
*/
protected static $instance;
/**
* sets the delimiters to use in the templates
*
* delimiters can be multi-character strings but should not be one of those as they will
* make it very hard to work with templates or might even break the compiler entirely : "\", "$", "|", ":" and finally "#" only if you intend to use config-vars with the #var# syntax.
*
* @param string $left left delimiter
* @param string $right right delimiter
*/
public function setDelimiters($left, $right)
{
$this->ld = $left;
$this->rd = $right;
$this->ldr = preg_quote($left);
$this->rdr = preg_quote($right);
}
/**
* returns the left and right template delimiters
*
* @return array containing the left and the right delimiters
*/
public function getDelimiters()
{
return array($this->ld, $this->rd);
}
/**
* sets the tag openings handling strictness, if set to true, template tags can
* contain spaces before the first function/string/variable such as { $foo} is valid.
*
* if set to false (default setting), { $foo} is invalid but that is however a good thing
* as it allows css (i.e. #foo { color:red; }) to be parsed silently without triggering
* an error, same goes for javascript.
*
* @param bool $allow true to allow loose handling, false to restore default setting
*/
public function setLooseOpeningHandling($allow = false)
{
$this->allowLooseOpenings = (bool) $allow;
}
/**
* returns the tag openings handling strictness setting
*
* @see setLooseOpeningHandling
* @return bool true if loose tags are allowed
*/
public function getLooseOpeningHandling()
{
return $this->allowLooseOpenings;
}
/**
* adds a preprocessor to the compiler, it will be called
* before the template is compiled
*
* @param mixed $callback either a valid callback to the preprocessor or a simple name if the autoload is set to true
* @param bool $autoload if set to true, the preprocessor is auto-loaded from one of the plugin directories, else you must provide a valid callback
*/
public function addPreProcessor($callback, $autoload = false)
{
if($autoload)
{
$name = str_replace('DwooProcessor_', '', $callback);
$class = 'DwooProcessor_'.$name;
if(!class_exists($class, false) && !function_exists($class))
DwooLoader::loadPlugin($name);
if(class_exists($class, false))
$callback = array(new $class($this), 'process');
elseif(function_exists($class))
$callback = $class;
else
$this->triggerError('Wrong pre-processor name, when using autoload the filter must be in one of your plugin dir as "name.php" containg a class or function named "DwooProcessor_name"', E_USER_ERROR);
$this->processors['pre'][] = $callback;
}
else
{
$this->processors['pre'][] = $callback;
}
}
/**
* removes a preprocessor from the compiler
*
* @param mixed $callback either a valid callback to the preprocessor or a simple name if it was autoloaded
*/
public function removePreProcessor($callback)
{
if(($index = array_search($callback, $this->processors['pre'], true)) !== false)
unset($this->processors['pre'][$index]);
elseif(($index = array_search('DwooProcessor_'.str_replace('DwooProcessor_', '', $callback), $this->processors['pre'], true)) !== false)
unset($this->processors['pre'][$index]);
else
{
$class = 'DwooProcessor_' . str_replace('DwooProcessor_', '', $callback);
foreach($this->processors['pre'] as $index=>$proc)
{
if(is_array($proc) && $proc[0] instanceof $class)
{
unset($this->processors['pre'][$index]);
break;
}
}
}
}
/**
* adds a postprocessor to the compiler, it will be called
* before the template is compiled
*
* @param mixed $callback either a valid callback to the postprocessor or a simple name if the autoload is set to true
* @param bool $autoload if set to true, the postprocessor is auto-loaded from one of the plugin directories, else you must provide a valid callback
*/
public function addPostProcessor($callback, $autoload = false)
{
if($autoload)
{
$name = str_replace('DwooProcessor_', '', $callback);
$class = 'DwooProcessor_'.$name;
if(!class_exists($class, false) && !function_exists($class))
DwooLoader::loadPlugin($name);
if(class_exists($class, false))
$callback = array(new $class($this), 'process');
elseif(function_exists($class))
$callback = $class;
else
$this->triggerError('Wrong post-processor name, when using autoload the processor must be in one of your plugin dir as "name.php" containg a class or function named "DwooProcessor_name"', E_USER_ERROR);
$this->processors['post'][] = $callback;
}
else
{
$this->processors['post'][] = $callback;
}
}
/**
* removes a postprocessor from the compiler
*
* @param mixed $callback either a valid callback to the postprocessor or a simple name if it was autoloaded
*/
public function removePostProcessor($callback)
{
if(($index = array_search($callback, $this->processors['post'], true)) !== false)
unset($this->processors['post'][$index]);
elseif(($index = array_search('DwooProcessor_'.str_replace('DwooProcessor_', '', $callback), $this->processors['post'], true)) !== false)
unset($this->processors['post'][$index]);
else
{
$class = 'DwooProcessor_' . str_replace('DwooProcessor_', '', $callback);
foreach($this->processors['post'] as $index=>$proc)
{
if(is_array($proc) && $proc[0] instanceof $class)
{
unset($this->processors['post'][$index]);
break;
}
}
}
}
/**
* adds the custom plugins loaded into Dwoo to the compiler so it can load them
*
* @see Dwoo::addPlugin
* @param array $customPlugins an array of custom plugins
*/
public function setCustomPlugins(array $customPlugins)
{
$this->customPlugins = $customPlugins;
}
/**
* sets the security policy object to enforce some php security settings
*
* use this if untrusted persons can modify templates
*
* @param DwooSecurityPolicy $policy the security policy object
*/
public function setSecurityPolicy(DwooSecurityPolicy $policy = null)
{
$this->securityPolicy = $policy;
}
/**
* returns the current security policy object or null by default
*
* @return DwooSecurityPolicy|null the security policy object if any
*/
public function getSecurityPolicy()
{
return $this->securityPolicy;
}
/**
* compiles the provided string down to php code
*
* @param string $tpl the template to compile
* @return string a compiled php string
*/
public function compile($tpl)
{
// resets variables
$this->usedPlugins = array('topLevelBlock' => Dwoo::BLOCK_PLUGIN);
$this->data = array();
$this->scope =& $this->data;
$this->scopeTree = array();
$this->stack = array();
if($this->debug) echo 'PROCESSING PREPROCESSORS
';
// runs preprocessors
foreach($this->processors['pre'] as $preProc)
{
if(is_array($preProc) && $preProc[0] instanceof DwooProcessor)
$tpl = call_user_func($preProc, $tpl);
else
$tpl = call_user_func($preProc, $this, $tpl);
}
unset($preProc);
$ptr = 0;
$this->template =& $tpl;
$this->pointer =& $ptr;
if($this->debug) echo '
'.print_r(htmlentities($tpl), true).'
';
$compiled = $this->addBlock('topLevelBlock', array(), 0);
// strips comments
if(strstr($tpl, $this->ld.'*') !== false)
$tpl = preg_replace('{'.$this->ldr.'\*.*?\*'.$this->rdr.'}s', '', $tpl);
// strips php tags if required by the security policy
if($this->securityPolicy !== null)
{
$search = array('{<\?php.*?\?>}');
if(ini_get('short_open_tags'))
$search = array('{<\?.*?\?>}', '{<%.*?%>}');
switch($this->securityPolicy->getPhpHandling())
{
case DwooSecurityPolicy::PHP_ALLOW:
break;
case DwooSecurityPolicy::PHP_ENCODE:
$tpl = preg_replace_callback($search, array($this, 'phpTagEncodingHelper'), $tpl);
break;
case DwooSecurityPolicy::PHP_REMOVE:
$tpl = preg_replace($search, '', $tpl);
}
}
// handles the built-in strip function
if(($pos = strpos($tpl, $this->ld.'strip'.$this->rd)) !== false && substr($tpl, $pos-1, 1) !== '\\')
$tpl = preg_replace_callback('{'.$this->ldr.'strip'.$this->rdr.'(.+?)'.$this->ldr.'/strip'.$this->rdr.'}s', array($this, 'stripPreprocessorHelper'), $tpl);
while(true)
{
$pos = strpos($tpl, $this->ld, $ptr);
if($pos === false)
{
$compiled .= substr($tpl, $ptr);
break;
}
elseif(substr($tpl, $pos-1, 1) === '\\' && substr($tpl, $pos-2, 1) !== '\\')
{
$compiled .= substr($tpl, $ptr, $pos-$ptr-1).$this->ld;
$ptr = $pos+strlen($this->ld);
}
elseif(strpos($tpl, $this->ld.'literal'.$this->rd, $pos) === $pos)
{
$endpos = strpos($tpl, $this->ld.'/literal'.$this->rd, $pos);
$compiled .= substr($tpl, $ptr, $pos-$ptr);
$compiled .= substr($tpl, $pos + strlen($this->ld.'literal'.$this->rd), $endpos-$pos-strlen($this->ld.'literal'.$this->rd));
$ptr = $endpos+strlen($this->ld.'/literal'.$this->rd);
}
else
{
if(substr($tpl, $pos-2, 1) === '\\' && substr($tpl, $pos-1, 1) === '\\')
{
$compiled .= substr($tpl, $ptr, $pos-$ptr-1);
$ptr = $pos;
}
$compiled .= substr($tpl, $ptr, $pos-$ptr);
$endpos = strpos($tpl, $this->rd, $pos);
if($endpos===false)
$this->triggerError('A template tag was not closed, started with '.substr($tpl, $pos, 100).'', E_USER_ERROR);
while(substr($tpl, $endpos-1, 1) === '\\')
{
$tpl = substr_replace($tpl, $this->rd, $endpos-1, 1+strlen($this->rd));
$endpos = strpos($tpl, $this->rd, $endpos);
}
$pos += strlen($this->ld);
if($this->allowLooseOpenings)
{
while(substr($tpl, $pos, 1) === ' ')
$pos+=1;
}
else
{
if(substr($tpl, $pos, 1) === ' ' || substr($tpl, $pos, 1) === "\r" || substr($tpl, $pos, 1) === "\n")
{
$ptr = $pos;
$compiled .= $this->ld;
continue;
}
}
$ptr = $endpos+strlen($this->rd);
if(substr($tpl, $pos, 1)==='/')
$compiled .= $this->removeBlock(substr($tpl, $pos+1, $endpos-$pos-1));
else
$compiled .= $this->parse($tpl, $pos, $endpos, false, 'root');
// adds additional line breaks between php closing and opening tags because the php parser removes those if there is just a single line break
if(substr($compiled, -2) === '?>' && preg_match('{^(([\r\n])([\r\n]?))}', substr($tpl, $ptr, 3), $m))
{
if($m[3] === '')
{
$ptr+=1;
$compiled .= $m[1].$m[1];
}
else
{
$ptr+=2;
$compiled .= $m[1]."\n";
}
}
}
}
$compiled .= $this->removeBlock('topLevelBlock');
if($this->debug) echo 'PROCESSING POSTPROCESSORS
';
foreach($this->processors['post'] as $postProc)
{
if(is_array($postProc) && $postProc[0] instanceof DwooProcessor)
$compiled = call_user_func($postProc, $compiled);
else
$compiled = call_user_func($postProc, $this, $compiled);
}
unset($postProc);
if($this->debug) echo 'COMPILATION COMPLETE : MEM USAGE : '.memory_get_usage().'
';
$output = "usedPlugins);
// build plugin preloader
foreach($this->usedPlugins as $plugin=>$type)
{
if($type & Dwoo::CUSTOM_PLUGIN) continue;
switch($type)
{
case Dwoo::BLOCK_PLUGIN:
case Dwoo::CLASS_PLUGIN:
$output .= "if(class_exists('DwooPlugin_$plugin', false)===false)\n\tDwooLoader::loadPlugin('$plugin');\n";
break;
case Dwoo::FUNC_PLUGIN:
$output .= "if(function_exists('DwooPlugin_$plugin')===false)\n\tDwooLoader::loadPlugin('$plugin');\n";
break;
case Dwoo::SMARTY_MODIFIER:
$output .= "if(function_exists('smarty_modifier_$plugin')===false)\n\tDwooLoader::loadPlugin('$plugin');\n";
break;
case Dwoo::SMARTY_FUNCTION:
$output .= "if(function_exists('smarty_function_$plugin')===false)\n\tDwooLoader::loadPlugin('$plugin');\n";
break;
case Dwoo::SMARTY_BLOCK:
$output .= "if(function_exists('smarty_block_$plugin')===false)\n\tDwooLoader::loadPlugin('$plugin');\n";
break;
default:
$this->triggerError('Type error for '.$plugin.' with type'.$type, E_USER_ERROR);
}
}
$output .= $compiled."\n?>";
$output = str_replace(self::PHP_CLOSE . self::PHP_OPEN, "\n", $output);
if($this->debug) {
echo '
';
$lines = preg_split('{\r\n|\n}', htmlentities($output));
foreach($lines as $i=>$line)
echo ($i+1).'. '.$line."\r\n";
}
if($this->debug) echo '
';
if(!empty($this->errors))
print_r($this->errors);
$this->template = null;
return $output;
}
/**
* returns the template that is being compiled
*
* @param bool $fromPointer if set to true, only the source from the current pointer position is returned
* @return string the template or partial template
*/
public function getTemplateSource($fromPointer = false)
{
if($fromPointer)
return substr($this->template, $this->pointer);
else
return $this->template;
}
/**
* sets the scope
*
* set to null if the scope becomes "unstable" (i.e. too variable or unknown) so that
* variables are compiled in a more evaluative way than just $this->scope['key']
*
* @param string $scope scope string that must be set (i.e. foo.bar.baz)
* @return string old scope
*/
public function setScope($scope)
{
$old = implode('.', $this->scopeTree);
if($scope===null)
{
unset($this->scope);
$this->scope = null;
}
if(empty($scope))
return $old;
$bits = explode('.', $scope);
while($bit = array_shift($bits))
{
if($bit === '_parent')
{
array_pop($this->scopeTree);
reset($this->scopeTree);
$this->scope =& $this->data;
$cnt = count($this->scopeTree);
for($i=0;$i<$cnt;$i++)
$this->scope =& $this->scope[$this->scopeTree[$i]];
}
elseif($bit === '_root')
{
$this->scope =& $this->data;
$this->scopeTree = array();
}
elseif(isset($this->scope[$bit]))
{
$this->scope =& $this->scope[$bit];
$this->scopeTree[] = $bit;
}
else
{
$this->scope[$bit] = array();
$this->scope =& $this->scope[$bit];
$this->scopeTree[] = $bit;
}
}
return $old;
}
/**
* forces an absolute scope
*
* @param string $scope the absolute scope string to use
* @return string the previous scope
*/
public function forceScope($scope)
{
$prev = $this->setScope('_root');
$this->setScope($scope);
return $prev;
}
/**
* adds a block to the top of the block stack
*
* @param string $type block type (name)
* @param array $params the parameters array
* @param int $paramtype the parameters type (see mapParams), 0, 1 or 2
* @return string the preProcessing() method's output
*/
public function addBlock($type, array $params, $paramtype)
{
$class = 'DwooPlugin_'.$type;
if(class_exists($class, false) === false)
DwooLoader::loadPlugin($type);
$params = $this->mapParams($params, array($class, 'init'), $paramtype);
$this->stack[] = array('type' => $type, 'params' => $params, 'custom' => false);
$this->curBlock =& $this->stack[count($this->stack)-1];
return call_user_func(array($class,'preProcessing'), $this, $params, '', '', $type);
}
/**
* adds a custom block to the top of the block stack
*
* @param string $type block type (name)
* @param array $params the parameters array
* @param int $paramtype the parameters type (see mapParams), 0, 1 or 2
* @return string the preProcessing() method's output
*/
public function addCustomBlock($type, array $params, $paramtype)
{
$callback = $this->customPlugins[$type]['callback'];
if(is_array($callback))
$class = is_object($callback[0]) ? get_class($callback[0]) : $callback[0];
else
$class = $callback;
$params = $this->mapParams($params, array($class, 'init'), $paramtype);
$this->stack[] = array('type' => $type, 'params' => $params, 'custom' => true, 'class'=>$class);
$this->curBlock =& $this->stack[count($this->stack)-1];
return call_user_func(array($class,'preProcessing'), $this, $params, '', '', $type);
}
/**
* injects a block at the top of the plugin stack without calling its preProcessing method
*
* used by {else} blocks to re-add themselves after having closed everything up to their parent
*
* @param string $type block type (name)
* @param array $params parameters array
*/
public function injectBlock($type, array $params)
{
$class = 'DwooPlugin_'.$type;
if(class_exists($class, false) === false)
DwooLoader::loadPlugin($type);
$this->stack[] = array('type' => $type, 'params' => $params, 'custom' => false);
$this->curBlock =& $this->stack[count($this->stack)-1];
}
/**
* removes the closest-to-top block of the given type and all other
* blocks encountered while going down the block stack
*
* @param string $type block type (name)
* @return string the output of all postProcessing() method's return values of the closed blocks
*/
public function removeBlock($type)
{
$output = '';
$pluginType = $this->getPluginType($type);
if($pluginType & Dwoo::SMARTY_BLOCK)
$type = 'smartyinterface';
while(true)
{
while($top = array_pop($this->stack))
{
if($top['custom'])
$class = $top['class'];
else
$class = 'DwooPlugin_'.$top['type'];
$output .= call_user_func(array($class, 'postProcessing'), $this, $top['params']);
if($top['type'] === $type)
break 2;
}
$this->triggerError('Syntax malformation, a block of type "'.$type.'" was closed but was not opened', E_USER_ERROR);
break;
}
$this->curBlock =& $this->stack[count($this->stack)-1];
return $output;
}
/**
* returns a reference to the first block of the given type encountered and
* optionally closes all blocks until it finds it
*
* this is mainly used by {else} plugins to close everything that was opened
* between their parent and themselves
*
* @param string $type the block type (name)
* @param bool $closeAlong whether to close all blocks encountered while going down the block stack or not
* @return &array the array is as such: array('type'=>pluginName, 'params'=>parameter array,
* 'custom'=>bool defining whether it's a custom plugin or not, for internal use)
*/
public function &findBlock($type, $closeAlong = false)
{
if($closeAlong===true)
{
while($b = end($this->stack))
{
if($b['type']===$type)
return $this->stack[key($this->stack)];
$this->removeTopBlock();
}
}
else
{
end($this->stack);
while($b = current($this->stack))
{
if($b['type']===$type)
return $this->stack[key($this->stack)];
prev($this->stack);
}
}
$this->triggerError('A parent block of type "'.$type.'" is required and can not be found', E_USER_ERROR);
}
/**
* returns a reference to the current block array
*
* @return &array the array is as such: array('type'=>pluginName, 'params'=>parameter array,
* 'custom'=>bool defining whether it's a custom plugin or not, for internal use)
*/
public function &getCurrentBlock()
{
return $this->curBlock;
}
/**
* removes the block at the top of the stack and calls its postProcessing() method
*
* @return string the postProcessing() method's output
*/
public function removeTopBlock()
{
$o = array_pop($this->stack);
if($o === null)
$this->triggerError('Syntax malformation, a block of unknown type was closed but was not opened.', E_USER_ERROR);
if($o['custom'])
$class = $o['class'];
else
$class = 'DwooPlugin_'.$o['type'];
$this->curBlock =& $this->stack[count($this->stack)-1];
return call_user_func(array($class, 'postProcessing'), $this, $o['params']);
}
/**
* returns the compiled parameters (for example a variable's compiled parameter will be "$this->scope['key']") out of the given parameter array
*
* @param array $params parameter array
* @return array filtered parameters
*/
public function getCompiledParams(array $params)
{
foreach($params as &$p)
$p = $p[0];
return $params;
}
/**
* returns the real parameters (for example a variable's real parameter will be its key, etc) out of the given parameter array
*
* @param array $params parameter array
* @return array filtered parameters
*/
public function getRealParams(array $params)
{
foreach($params as &$p)
$p = $p[1];
return $params;
}
/**
* entry point of the parser, it redirects calls to other parse* functions
*
* @param string $in the string within which we must parse something
* @param int $from the starting offset of the parsed area
* @param int $to the ending offset of the parsed area
* @param mixed $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by default
* @param string $curBlock the current parser-block being processed
* @param mixed $pointer a reference to a pointer that will be increased by the amount of characters parsed, or null by default
* @return string parsed values
*/
protected function parse($in, $from, $to, $parsingParams = false, $curBlock='', &$pointer = null)
{
if($to === null)
$to = strlen($in);
$first = $in[$from];
$substr = substr($in, $from, $to-$from);
if($this->debug) echo '
PARSE CALL : PARSING '.htmlentities(substr($in, 0, $from)).''.htmlentities(substr($in, $from, $to-$from)).''.htmlentities(substr($in, $to)).' @ '.$from.':'.$to.' in '.$curBlock.' : pointer='.$pointer.'
';
if($first==='$') // var
{
return $this->parseVar($in, $from, $to, $parsingParams, $curBlock, $pointer);
}
elseif($first==='"' || $first==="'") // string
{
return $this->parseString($in, $from, $to, $parsingParams, $curBlock, $pointer);
}
elseif(preg_match('#^[a-z][a-z0-9_]*('.(is_array($parsingParams)||$curBlock!='root'?'':' |').'\(|$)#i', $substr)) // func
{
return $this->parseFunction($in, $from, $to, $parsingParams, $curBlock, $pointer);
}
elseif(is_array($parsingParams) && preg_match('#^([a-z0-9_]+\s*=)(?:\s+|[^=]).*#i', $substr, $match)) // named parameter
{
if($this->debug) echo 'NAMED PARAM FOUND
';
$len = strlen($match[1]);
while(substr($in, $from+$len, 1)===' ')
$len++;
if($pointer !== null)
$pointer += $len;
$output = array(trim(substr(trim($match[1]),0,-1)), $this->parse($in, $from+$len, $to, false, 'namedparam', $pointer));
$parsingParams[] = $output;
return $parsingParams;
}
elseif($substr!=='' && (is_array($parsingParams) || $curBlock === 'namedparam')) // unquoted string, bool or number
{
return $this->parseOthers($in, $from, $to, $parsingParams, $curBlock, $pointer);
}
else // parse error
{
$this->triggerError('Parse error in '.substr($in, 0, $from).''.substr($in, $from, $to-$from).''.substr($in, $to).' @ '.$from, E_USER_ERROR);
}
}
/**
* parses a function call
*
* @param string $in the string within which we must parse something
* @param int $from the starting offset of the parsed area
* @param int $to the ending offset of the parsed area
* @param mixed $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by default
* @param string $curBlock the current parser-block being processed
* @param mixed $pointer a reference to a pointer that will be increased by the amount of characters parsed, or null by default
* @return string parsed values
*/
protected function parseFunction($in, $from, $to, $parsingParams = false, $curBlock='', &$pointer = null)
{
$cmdstr = substr($in, $from, $to-$from);
if($this->debug) echo 'FUNC FOUND
';
$paramsep = '';
if(is_array($parsingParams) || $curBlock != 'root')
$ppos1 = false;
else
$ppos1 = strpos($cmdstr, ' ');
$ppos2 = strpos($cmdstr, '(');
if($ppos1 !== false && $ppos2 !== false)
{
$paramspos = min($ppos1, $ppos2);
if($paramspos === $ppos2)
$paramsep = ')';
}
elseif($ppos1 !== false)
$paramspos = $ppos1;
else
{
$paramspos = $ppos2;
$paramsep = ')';
}
$state = 0;
if($paramspos === false)
{
if(strpos($cmdstr,' '))
$func = substr($cmdstr, 0, strpos($cmdstr,' '));
else
$func = $cmdstr;
$params = array();
if($curBlock === 'namedparam' || $curBlock === 'modifier' || $curBlock === 'function' || $curBlock === 'condition') /* smarty supports unquoted strings so they're supported for named param (smarty syntax),
any [a-z0-9_]+ without parenthesis/arguments after it is considered to be a string */
{
return $this->parseOthers($in, $from, $to, $parsingParams, $curBlock, $pointer);
}
}
else
{
$func = substr($cmdstr, 0, $paramspos);
$paramstr = substr($cmdstr, $paramspos+1);
if(substr($paramstr, -1, 1) === $paramsep)
$paramstr = substr($paramstr, 0, -1);
if(strlen($paramstr)===0)
{
$params = array();
$paramstr = '';
}
else
{
$ptr = 0;
$params = array();
while($ptr < strlen($paramstr))
{
while(true)
{
if($ptr >= strlen($paramstr))
break 2;
if($func !== 'if' && $func !== 'elseif' && $paramstr[$ptr] === ')')
{
if($this->debug) echo 'PARAM PARSING ENDED, ")" FOUND, POINTER AT '.$ptr.'
';
break 2;
}
if(($paramstr[$ptr] === ' ' || $paramstr[$ptr] === ','))
$ptr++;
else
break;
}
if($this->debug) echo 'FUNC START PARAM PARSING WITH POINTER AT '.$ptr.'
';
if($func === 'if' || $func === 'elseif')
$params = $this->parse($paramstr, $ptr, strlen($paramstr), $params, 'condition', $ptr);
else
$params = $this->parse($paramstr, $ptr, strlen($paramstr), $params, 'function', $ptr);
if($this->debug) echo 'PARAM PARSED, POINTER AT '.$ptr.'
';
}
$paramstr = substr($paramstr, 0, $ptr);
$state = 0;
foreach($params as $p)
{
if(is_array($p) && is_array($p[1]))
$state |= 2;
else
$state |= 1;
}
if($state === 3)
$this->triggerError('Function calls can not have both named and un-named parameters', E_USER_ERROR);
}
}
if($pointer !== null)
{
$pointer += (isset($paramstr) ? strlen($paramstr) : 0) + (')' === $paramsep ? 2 : 0) + strlen($func);
if($this->debug) echo 'FUNC ADDS '.((isset($paramstr) ? strlen($paramstr) : 0) + (')' === $paramsep ? 2 : 0) + strlen($func)).' TO POINTER
';
}
if($curBlock === 'method')
{
$pluginType = Dwoo::NATIVE_PLUGIN;
}
else
{
$pluginType = $this->getPluginType($func);
// add block
if($pluginType & Dwoo::BLOCK_PLUGIN)
{
if($curBlock !== 'root' || is_array($parsingParams))
$this->triggerError('Block plugins can not be used as other plugin\'s arguments', E_USER_ERROR);
if($pluginType & Dwoo::CUSTOM_PLUGIN)
return $this->addCustomBlock($func, $params, $state);
else
return $this->addBlock($func, $params, $state);
}
elseif($pluginType & Dwoo::SMARTY_BLOCK)
{
if($curBlock !== 'root' || is_array($parsingParams))
$this->triggerError('Block plugins can not be used as other plugin\'s arguments', E_USER_ERROR);
if($state===2)
{
array_unshift($params, array('__functype', array($pluginType, $pluginType)));
array_unshift($params, array('__funcname', array($func, $func)));
}
else
{
array_unshift($params, array($pluginType, $pluginType));
array_unshift($params, array($func, $func));
}
return $this->addBlock('smartyinterface', $params, $state);
}
}
if($pluginType & Dwoo::NATIVE_PLUGIN || $pluginType & Dwoo::SMARTY_FUNCTION || $pluginType & Dwoo::SMARTY_BLOCK)
{
$params = $this->mapParams($params, null, $state);
}
elseif($pluginType & Dwoo::CLASS_PLUGIN)
{
if($pluginType & Dwoo::CUSTOM_PLUGIN)
$params = $this->mapParams($params, array($this->customPlugins[$func]['class'], $this->customPlugins[$func]['function']), $state);
else
$params = $this->mapParams($params, array('DwooPlugin_'.$func, ($pluginType & Dwoo::COMPILABLE_PLUGIN) ? 'compile' : 'process'), $state);
}
elseif($pluginType & Dwoo::FUNC_PLUGIN)
{
if($pluginType & Dwoo::CUSTOM_PLUGIN)
$params = $this->mapParams($params, $this->customPlugins[$func]['callback'], $state);
else
$params = $this->mapParams($params, 'DwooPlugin_'.$func.(($pluginType & Dwoo::COMPILABLE_PLUGIN) ? '_compile' : ''), $state);
}
elseif($pluginType & Dwoo::SMARTY_MODIFIER)
{
$output = 'smarty_modifier_'.$func.'('.implode(', ', $params).')';
}
// keep php-syntax-safe values for non-block plugins
foreach($params as &$p)
$p = $p[0];
if($pluginType & Dwoo::NATIVE_PLUGIN)
{
if(isset($params['*']))
$output = $func.'('.implode(', ', $params['*']).')';
else
$output = $func.'()';
}
elseif($pluginType & Dwoo::FUNC_PLUGIN)
{
if($pluginType & Dwoo::COMPILABLE_PLUGIN)
{
$funcCompiler = 'DwooPlugin_'.$func.'_compile';
array_unshift($params, $this);
$output = call_user_func_array($funcCompiler, $params);
}
else
{
array_unshift($params, '$this');
$params = $this->implode_r($params);
if($pluginType & Dwoo::CUSTOM_PLUGIN)
{
$callback = $this->customPlugins[$func]['callback'];
$output = 'call_user_func(\''.$callback.'\', '.$params.')';
}
else
$output = 'DwooPlugin_'.$func.'('.$params.')';
}
}
elseif($pluginType & Dwoo::CLASS_PLUGIN)
{
if($pluginType & Dwoo::COMPILABLE_PLUGIN)
{
$funcCompiler = array('DwooPlugin_'.$func, 'compile');
array_unshift($params, $this);
$output = call_user_func_array($funcCompiler, $params);
}
else
{
$params = $this->implode_r($params);
if($pluginType & Dwoo::CUSTOM_PLUGIN)
{
$callback = $this->customPlugins[$func]['callback'];
if(!is_array($callback))
$output = 'call_user_func(array(\''.$callback.'\', \'process\'), '.$params.')';
elseif(is_object($callback[0]))
$output = 'call_user_func(array($this->customPlugins[\''.$func.'\'][0], \''.$callback[1].'\'), '.$params.')';
elseif(($ref = new ReflectionMethod($callback[0], $callback[1])) && $ref->isStatic())
$output = 'call_user_func(array(\''.$callback[0].'\', \''.$callback[1].'\'), '.$params.')';
else
$output = 'call_user_func(array($this->getObjectPlugin(\''.$callback[0].'\'), \''.$callback[1].'\'), '.$params.')';
}
else
$output = '$this->classCall(\''.$func.'\', array('.$params.'))';
}
}
elseif($pluginType & Dwoo::SMARTY_FUNCTION)
{
$params = $this->implode_r($params['*'], true);
if($pluginType & Dwoo::CUSTOM_PLUGIN)
{
$callback = $this->customPlugins[$func]['callback'];
if(is_array($callback))
{
if(is_object($callback[0]))
$output = 'call_user_func_array(array($this->customPlugins[\''.$func.'\'][0], \''.$callback[1].'\'), array(array('.$params.'), $this))';
else
$output = 'call_user_func_array(array(\''.$callback[0].'\', \''.$callback[1].'\'), array(array('.$params.'), $this))';
}
else
$output = $callback.'(array('.$params.'), $this)';
}
else
$output = 'smarty_function_'.$func.'(array('.$params.'), $this)';
}
if(is_array($parsingParams))
{
$parsingParams[] = array($output, $output);
return $parsingParams;
}
elseif($curBlock === 'namedparam')
return array($output, $output);
elseif($curBlock === 'method')
return $output;
else
return self::PHP_OPEN.'echo '.$output.';'.self::PHP_CLOSE;
}
/**
* parses a string
*
* @param string $in the string within which we must parse something
* @param int $from the starting offset of the parsed area
* @param int $to the ending offset of the parsed area
* @param mixed $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by default
* @param string $curBlock the current parser-block being processed
* @param mixed $pointer a reference to a pointer that will be increased by the amount of characters parsed, or null by default
* @return string parsed values
*/
protected function parseString($in, $from, $to, $parsingParams = false, $curBlock='', &$pointer = null)
{
$substr = substr($in, $from, $to-$from);
$first = $substr[0];
if($this->debug) echo 'STRING FOUND
';
$strend = false;
$o = $from+1;
while($strend===false)
{
$strend = strpos($in, $first, $o);
if($strend === false)
{
$this->triggerError('Unfinished string in : '.substr($in, 0, $from).''.substr($in, $from, $to-$from).''.substr($in, $to).'', E_USER_ERROR);
}
if(substr($in, $strend-1, 1) === '\\')
{
$o = $strend+1;
$strend = false;
}
}
if($curBlock !== 'modifier' && substr($in, $strend+1, 1)==='|')
{
$strend = strpos($in, ' ', $strend+1);
if($strend===false)
$strend = strlen($in)-1;
}
$srcOutput = substr($in, $from, $strend+1-$from);
if($pointer !== null)
$pointer += strlen($srcOutput);
$output = $this->replaceStringVars($srcOutput, $first);
// handle modifiers
if($curBlock !== 'modifier' && preg_match('#(.+?)((?:\|(?:@?[a-z0-9_]+(?::[^\s]*)*))+)#i', $output, $match))
{
$modstr = $match[2];
$output = $match[1];
$strend += strlen($match[1]);
if($curBlock === 'root' && substr($modstr, -1) === '}')
$modstr = substr($modstr, 0, -1);
$output = $this->replaceModifiers(array(null, null, $output, $modstr), 'string');
}
if($curBlock !== 'namedparam' && $curBlock !== 'modifier' && $curBlock !== 'function' && $curBlock !== 'condition' && strlen(substr($in, 0, $to)) > $strend+1)
$output .= $this->parse($in, $strend+1, $to, $parsingParams);
if(is_array($parsingParams))
{
$parsingParams[] = array($output, substr($srcOutput,1,-1));
return $parsingParams;
}
elseif($curBlock === 'namedparam')
return array($output, substr($srcOutput,1,-1));
elseif($curBlock === 'root')
return self::PHP_OPEN.'echo '.$output.';'.self::PHP_CLOSE;
else
return $output;
}
/**
* parses a variable
*
* @param string $in the string within which we must parse something
* @param int $from the starting offset of the parsed area
* @param int $to the ending offset of the parsed area
* @param mixed $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by default
* @param string $curBlock the current parser-block being processed
* @param mixed $pointer a reference to a pointer that will be increased by the amount of characters parsed, or null by default
* @return string parsed values
*/
protected function parseVar($in, $from, $to, $parsingParams = false, $curBlock='', &$pointer = null)
{
$substr = substr($in, $from, $to-$from);
if(preg_match('#(\$?[a-z0-9_:]+(?:(?:(?:\.|->)(?:[a-z0-9_:]+|(?R))|\[(?:[a-z0-9_:]+|(?R))\]))*)' . // var key
($curBlock==='root' || $curBlock==='function' || $curBlock==='condition' || $curBlock==='variable' || $curBlock==='expression' ? '(\([^)]*?\)(?:->[a-z0-9_]+(?:\([^)]*?\))?)*)?' : '()') . // method call
($curBlock==='root' || $curBlock==='function' || $curBlock==='condition' || $curBlock==='variable' || $curBlock==='string' ? '((?:(?:[+/*%-])(?:\$[a-z0-9.[\]>_:-]+(?:\([^)]*\))?|[0-9.,]*))*)':'()') . // simple math expressions
($curBlock!=='modifier'? '((?:\|(?:@?[a-z0-9_]+(?:(?::("|\').+?\5|:[^\s`"\']*))*))+)?':'(())') . // modifiers
'#i', $substr, $match))
{
$key = substr($match[1],1);
$matchedLength = strlen($match[0]);
$hasModifiers = isset($match[4]) && !empty($match[4]);
$hasExpression = isset($match[3]) && !empty($match[3]);
$hasMethodCall = isset($match[2]) && !empty($match[2]);
if($hasMethodCall)
{
$key = substr($match[1], 1, strrpos($match[1], '->')-1);
$methodCall = substr($match[1], strrpos($match[1], '->')) . $match[2];
}
if($pointer !== null)
$pointer += $matchedLength;
// replace useless brackets by dot accessed vars
$key = preg_replace('#\[([^\[.->]+)\]#', '.$1', $key);
// prevent $foo->$bar calls because it doesn't seem worth the trouble
if(strpos($key, '->$') !== false)
$this->triggerError('You can not access an object\'s property using a variable name.', E_USER_ERROR);
if($this->debug)
{
if($hasMethodCall)
echo 'METHOD CALL FOUND : $'.$key.$methodCall.'
';
else
echo 'VAR FOUND : $'.$key.'
';
}
$key = str_replace('"','\\"',$key);
$cnt=substr_count($key, '$');
if($cnt > 0)
{
$uid = 0;
$parsed = array($uid => '');
$current =& $parsed;
$curTxt =& $parsed[$uid++];
$tree = array();
$chars = str_split($key, 1);
while(($char = array_shift($chars)) !== null)
{
if($char === '[')
{
$tree[] =& $current;
$current[$uid] = array($uid+1 => '');
$current =& $current[$uid++];
$curTxt =& $current[$uid++];
}
elseif($char === ']')
{
$current =& $tree[count($tree)-1];
array_pop($tree);
if(current($chars) !== '[' && current($chars) !== false && current($chars) !== ']')
{
$current[$uid] = '';
$curTxt =& $current[$uid++];
}
}
else
{
$curTxt .= $char;
}
}
unset($uid, $current, $curTxt, $tree, $chars);
if($this->debug) echo 'RECURSIVE VAR REPLACEMENT : '.$key.'
';
$key = $this->flattenVarTree($parsed);
if($this->debug) echo 'RECURSIVE VAR REPLACEMENT DONE : '.$key.'
';
$output = preg_replace('#(^""\.|""\.|\.""$|(\()""\.|\.""(\)))#', '$2$3', '$this->readVar("'.$key.'")');
}
else
{
$output = $this->parseVarKey($key);
}
if($hasMethodCall)
{
preg_match_all('{->([a-z0-9_]+)(\([^)]*\))?}i', $methodCall, $calls);
foreach($calls[1] as $i=>$method)
{
$args = $calls[2][$i];
// property
if($args === '')
$output = '(property_exists($tmp = '.$output.', \''.$method.'\') ? $tmp->'.$method.' : null)';
// method
else
{
if($args === '()')
$parsedCall = '->'.$method.$args;
else
$parsedCall = '->'.$this->parseFunction($method.$args, 0, strlen($method.$args), false, 'method');
$output = '(is_object($tmp = '.$output.') ? (method_exists($tmp, \''.$method.'\') ? $tmp'.$parsedCall.' : $this->triggerError(\'Call to an undefined method : \'.get_class($tmp).\'::'.$method.'()\')) : $this->triggerError(\'Method '.$method.'() was called on a non-object (\'.var_export($tmp, true).\')\'))';
}
}
}
if($hasExpression)
{
preg_match_all('#(?:([+/*%-])(\$[a-z0-9.[\]>_:-]+(?:\([^)]*\))?|[0-9.,]*))#i', $match[3], $expMatch);
foreach($expMatch[1] as $k=>$operator)
{
if(substr($expMatch[2][$k], 0, 1) === '$')
{
$output = '('.$output.' '.$operator.' '.$this->parseVar($expMatch[2][$k], 0, strlen($expMatch[2][$k]), false, 'expression').')';
}
elseif(!empty($expMatch[2][$k]))
$output = '('.$output.' '.$operator.' '.str_replace(',', '.', $expMatch[2][$k]).')';
else
$this->triggerError('Unfinished expression, '.$substr.', missing var or number after math operator', E_USER_ERROR);
}
}
// handle modifiers
if($curBlock !== 'modifier' && $hasModifiers)
{
$output = $this->replaceModifiers(array(null, null, $output, $match[4]), 'var');
}
if(is_array($parsingParams))
{
$parsingParams[] = array($output, $key);
return $parsingParams;
}
elseif($curBlock === 'namedparam')
return array($output, $key);
elseif($curBlock === 'string')
return array($matchedLength, $output);
elseif($curBlock === 'expression' || $curBlock === 'variable')
return $output;
elseif(substr($output, 0, strlen(self::PHP_OPEN)) === self::PHP_OPEN)
return $output;
else
return self::PHP_OPEN.'echo '.$output.';'.self::PHP_CLOSE;
}
else
{
if($curBlock === 'string')
return array(0, '');
else
{
$this->triggerError('Invalid variable name '.$substr.'', E_USER_ERROR);
}
}
}
/**
* parses a constant variable (a variable that doesn't contain another variable) and preprocesses it to save runtime processing time
*
* @param string $key the variable to parse
* @return string parsed variable
*/
protected function parseVarKey($key)
{
if(preg_match('#dwoo\.(get|post|server|cookies|session|env|request)((?:\.[a-z0-9_-]+)+)#i', $key, $m))
{
$global = strtoupper($m[1]);
if($global === 'COOKIES')
$global = 'COOKIE';
$key = '$_'.$global;
foreach(explode('.', ltrim($m[2], '.')) as $part)
$key .= '['.var_export($part, true).']';
$output = '(isset('.$key.')?'.$key.':null)';
}
elseif(preg_match('#dwoo\.const\.([a-z0-9_:-]+)#i', $key, $m))
{
if($this->securityPolicy !== null && $this->securityPolicy->getConstantHandling() === DwooSecurityPolicy::CONST_DISALLOW)
return 'null';
if(strpos($m[1], ':') !== false)
$output = '(defined("'.$m[1].'") ? constant("'.$m[1].'") : null)';
else
$output = '(defined("'.$m[1].'") ? '.$m[1].' : null)';
}
elseif($this->scope !== null)
{
if(strstr($key, '.') === false && strstr($key, '[') === false && strstr($key, '->') === false)
{
if($key === 'dwoo')
{
$output = '$this->globals';
}
elseif($key === '_root')
{
$output = '$this->data';
}
elseif($key === '_parent')
{
$output = '$this->readParentVar(1)';
}
else
{
$output = '(isset($this->scope["'.$key.'"]) ? $this->scope["'.$key.'"] : null)';
}
}
else
{
preg_match_all('#(\[|->|\.)?([a-z0-9_]+)\]?#i', $key, $m);
$i = $m[2][0];
if($i === '_parent')
{
$parentCnt = 0;
while(true)
{
$parentCnt++;
array_shift($m[2]);
array_shift($m[1]);
if(current($m[2]) === '_parent')
continue;
break;
}
$output = '$this->readParentVar('.$parentCnt.')';
}
else
{
if($i === 'dwoo')
{
$output = '$this->globals';
array_shift($m[2]);
array_shift($m[1]);
}
elseif($i === '_root')
{
$output = '$this->data';
array_shift($m[2]);
array_shift($m[1]);
}
else
{
$output = '$this->scope';
}
while(count($m[1]) && $m[1][0] !== '->')
{
$output .= '["'.$m[2][0].'"]';
array_shift($m[2]);
array_shift($m[1]);
}
$output = '(isset('.$output.') ? '.$output.':null)';
}
if(count($m[2]))
{
unset($m[0]);
$output = '$this->readVarInto('.str_replace("\n", '', var_export($m, true)).', '.$output.')';
}
}
}
else
{
preg_match_all('#(\[|->|\.)?([a-z0-9_]+)\]?#i', $key, $m);
unset($m[0]);
$output = '$this->readVar('.str_replace("\n", '', var_export($m, true)).')';
}
return $output;
}
/**
* flattens a variable tree, this helps in parsing very complex variables such as $var.foo[$foo.bar->baz].baz,
* it computes the contents of the brackets first and works out from there
*
* @param array $tree the variable tree parsed by he parseVar() method that must be flattened
* @param bool $recursed leave that to false by default, it is only for internal use
* @return string flattened tree
*/
protected function flattenVarTree(array $tree, $recursed=false)
{
$out = $recursed ? '".$this->readVarInto(' : '';
foreach($tree as $bit)
{
if(is_array($bit))
$out.='.'.$this->flattenVarTree($bit, false);
else
{
$key = str_replace('"','\\"',$bit);
$cnt = substr_count($key, '$');
if($this->debug) echo 'PARSING SUBVARS IN : '.$key.'
';
if($cnt > 0)
{
while(--$cnt >= 0)
{
if(isset($last))
{
$last = strrpos($key, '$', - (strlen($key) - $last + 1));
}
else
{
$last = strrpos($key, '$');
}
preg_match('#\$[a-z0-9_]+((?:(?:\.|->)(?:[a-z0-9_]+|(?R))|\[(?:[a-z0-9_]+|(?R))\]))*'.
'((?:(?:[+/*%-])(?:\$[a-z0-9.[\]>_:-]+(?:\([^)]*\))?|[0-9.,]*))*)#i', substr($key, $last), $submatch);
$len = strlen($submatch[0]);
$key = substr_replace(
$key,
preg_replace_callback(
'#(\$[a-z0-9_]+((?:(?:\.|->)(?:[a-z0-9_]+|(?R))|\[(?:[a-z0-9_]+|(?R))\]))*)'.
'((?:(?:[+/*%-])(?:\$[a-z0-9.[\]>_:-]+(?:\([^)]*\))?|[0-9.,]*))*)#i',
array($this, 'replaceVarKeyHelper'), substr($key, $last, $len)
),
$last,
$len
);
if($this->debug) echo 'RECURSIVE VAR REPLACEMENT DONE : '.$key.'
';
}
$out .= $key;
}
else
{
$out .= $key;
}
}
}
$out .= $recursed ? ')."' : '';
return $out;
}
/**
* helper function that parses a variable
*
* @param array $match the matched variable, array(1=>"string match")
* @return string parsed variable
*/
protected function replaceVarKeyHelper($match)
{
return '".'.$this->parseVar($match[0], 0, strlen($match[0]), false, 'variable').'."';
}
/**
* parses various constants, operators or non-quoted strings
*
* @param string $in the string within which we must parse something
* @param int $from the starting offset of the parsed area
* @param int $to the ending offset of the parsed area
* @param mixed $parsingParams must be an array if we are parsing a function or modifier's parameters, or false by default
* @param string $curBlock the current parser-block being processed
* @param mixed $pointer a reference to a pointer that will be increased by the amount of characters parsed, or null by default
* @return string parsed values
*/
protected function parseOthers($in, $from, $to, $parsingParams = false, $curBlock='', &$pointer = null)
{
$first = $in[$from];
$substr = substr($in, $from, $to-$from);
$end = strlen($substr);
if($curBlock === 'condition')
{
$breakChars = array('(', ')', ' ', '||', '&&', '|', '&', '>=', '<=', '===', '==', '=', '!==', '!=', '<<', '<', '>>', '>', '^', '~', ',', '+', '-', '*', '/', '%', '!');
}
elseif($curBlock === 'modifier')
$breakChars = array(' ', ',', ')', ':', '|');
else
$breakChars = array(' ', ',', ')');
$breaker = false;
while(list($k,$char) = each($breakChars))
{
$test = strpos($substr,$char);
if($test !== false && $test < $end)
{
$end = $test;
$breaker = $k;
}
}
if($curBlock === 'condition')
{
if($end === 0 && $breaker !== false)
{
$end = strlen($breakChars[$breaker]);
}
}
if($end !== false)
$substr = substr($substr, 0, $end);
if($pointer !== null)
$pointer += strlen($substr);
$src = $substr;
if(strtolower($substr) === 'false' || strtolower($substr) === 'no' || strtolower($substr) === 'off')
{
if($this->debug) echo 'BOOLEAN(FALSE) PARSED
';
$substr = 'false';
}
elseif(strtolower($substr) === 'true' || strtolower($substr) === 'yes' || strtolower($substr) === 'on')
{
if($this->debug) echo 'BOOLEAN(TRUE) PARSED
';
$substr = 'true';
}
elseif($substr === 'null' || $substr === 'NULL')
{
if($this->debug) echo 'NULL PARSED
';
$substr = 'null';
}
elseif(is_numeric($substr))
{
if($this->debug) echo 'NUMBER PARSED
';
$substr = (float) $substr;
}
elseif(preg_match('{^-?(\d+|\d*(\.\d+))\s*([/*%+-]\s*-?(\d+|\d*(\.\d+)))+$}', $substr))
{
if($this->debug) echo 'SIMPLE MATH PARSED
';
$substr = '('.$substr.')';
}
elseif($curBlock === 'condition' && array_search($substr, $breakChars, true) !== false)
{
if($this->debug) echo 'BREAKCHAR PARSED
';
$substr = '"'.$substr.'"';
}
else
{
if($this->debug) echo 'BLABBER CASTED AS STRING
';
$substr = $this->replaceStringVars('"'.str_replace('"','\\"',$substr).'"', '"', $curBlock);
}
if(is_array($parsingParams))
{
$parsingParams[] = array($substr, $src);
return $parsingParams;
}
elseif($curBlock === 'namedparam')
return array($substr, $src);
}
/**
* replaces variables within a parsed string
*
* @param string $string the parsed string
* @param string $first the first character parsed in the string, which is the string delimiter (' or ")
* @param string $curBlock the current parser-block being processed
* @return string the original string with variables replaced
*/
protected function replaceStringVars($string, $first, $curBlock='')
{
// replace vars
$cnt=substr_count($string, '$');
if($this->debug) echo 'STRING VAR REPLACEMENT : '.$string.'
';
while(--$cnt >= 0)
{
if(isset($last))
{
$last = strrpos($string, '$', - (strlen($string) - $last + 1));
}
else
{
$last = strrpos($string, '$');
}
if(array_search($string[$last-1], array('\\', '/', '*', '+', '-', '%')) !== false)
continue;
$var = $this->parse($string, $last, null, false, $curBlock === 'modifier' ? 'modifier' : 'string');
$len = $var[0];
$string = substr_replace($string, $first.'.'.$var[1].'.'.$first, $last, $len);
if($this->debug) echo 'STRING VAR REPLACEMENT DONE : '.$string.'
';
}
// handle modifiers
$string = preg_replace_callback('#("|\')\.(.+?)\.\1((?:\|(?:@?[a-z0-9_]+(?:(?::("|\').+?\4|:[^\s`"\']*))*))+)#i', array($this, 'replaceModifiers'), $string);
// replace escaped dollar operators by unescaped ones if required
if($first==="'")
$string = str_replace('\\$', '$', $string);
// remove backticks around strings if needed
$string = preg_replace('#`(("|\').+?\2)`#','$1',$string);
return $string;
}
/**
* replaces the modifiers applied to a string or a variable
*
* @param array $m the regex matches that must be array(1=>"double or single quotes enclosing a string, when applicable", 2=>"the string or var", 3=>"the modifiers matched")
* @param string $curBlock the current parser-block being processed
* @return string the input enclosed with various function calls according to the modifiers found
*/
protected function replaceModifiers(array $m, $curBlock)
{
if($this->debug) echo 'PARSING MODIFIERS : '.$m[3].'
';
// remove first pipe
$cmdstrsrc = substr($m[3],1);
// remove last quote if present
if(substr($cmdstrsrc,-1,1) === $m[1])
{
$cmdstrsrc = substr($cmdstrsrc, 0, -1);
$add = $m[1];
}
$output = $m[2];
while(strlen($cmdstrsrc) > 0)
{
$cmdstr = $cmdstrsrc;
$paramsep = ':';
$paramspos = strpos($cmdstr, $paramsep);
$funcsep = strpos($cmdstr, '|');
if($funcsep !== false && ($paramspos === false || $paramspos > $funcsep))
{
$paramspos = false;
$cmdstr = substr($cmdstr, 0, $funcsep);
}
$state = 0;
if($paramspos === false)
{
$func = $cmdstr;
$cmdstrsrc = substr($cmdstrsrc, strlen($func)+1);
$params = array();
}
else
{
$func = substr($cmdstr, 0, $paramspos);
$paramstr = substr($cmdstr, $paramspos+1);
if(substr($paramstr, -1, 1) === $paramsep)
$paramstr = substr($paramstr, 0, -1);
$ptr = 0;
$params = array();
while($ptr < strlen($paramstr))
{
if($this->debug) echo 'MODIFIER START PARAM PARSING WITH POINTER AT '.$ptr.'
';
if($this->debug) echo $paramstr.'--'.$ptr.'--'.strlen($paramstr).'--modifier
';
$params = $this->parse($paramstr, $ptr, strlen($paramstr), $params, 'modifier', $ptr);
if($this->debug) echo 'PARAM PARSED, POINTER AT '.$ptr.'
';
if($ptr >= strlen($paramstr))
break;
while(true)
{
if($paramstr[$ptr] === ' ' || $paramstr[$ptr] === '|')
{
if($this->debug) echo 'PARAM PARSING ENDED, " " or "|" FOUND, POINTER AT '.$ptr.'
';
$ptr++;
break 2;
}
if($ptr < strlen($paramstr) && $paramstr[$ptr] === ':')
$ptr++;
else
break;
}
}
$cmdstrsrc = substr($cmdstrsrc, strlen($func)+1+$ptr);
$paramstr = substr($paramstr, 0, $ptr);
foreach($params as $p)
{
if(is_array($p) && is_array($p[1]))
$state |= 2;
else
$state |= 1;
}
if($state === 3)
$this->errors[] = 'A function can not have named AND un-named parameters in : '.$cmdstr;
}
// check if we must use array_map with this plugin or not
$mapped = false;
if(substr($func, 0, 1) === '@')
{
$func = substr($func, 1);
$mapped = true;
}
$pluginType = $this->getPluginType($func);
if($state===2)
array_unshift($params, array('value', array($output, $output)));
else
array_unshift($params, array($output, $output));
if($pluginType & Dwoo::NATIVE_PLUGIN)
{
$params = $this->mapParams($params, null, $state);
$params = $params['*'][0];
$params = $this->implode_r($params);
if($mapped)
$output = '$this->arrayMap(\''.$func.'\', array('.$params.'))';
else
$output = $func.'('.$params.')';
}
elseif($pluginType & Dwoo::SMARTY_MODIFIER)
{
$params = $this->mapParams($params, null, $state);
$params = $params['*'][0];
$params = $this->implode_r($params);
if($pluginType & Dwoo::CUSTOM_PLUGIN)
{
$callback = $this->customPlugins[$func]['callback'];
if(is_array($callback))
{
if(is_object($callback[0]))
$output = ($mapped ? '$this->arrayMap' : 'call_user_func_array').'(array($this->customPlugins[\''.$func.'\'][0], \''.$callback[1].'\'), array('.$params.'))';
else
$output = ($mapped ? '$this->arrayMap' : 'call_user_func_array').'(array(\''.$callback[0].'\', \''.$callback[1].'\'), array('.$params.'))';
}
elseif($mapped)
$output = '$this->arrayMap(\''.$callback.'\', array('.$params.'))';
else
$output = $callback.'('.$params.')';
}
elseif($mapped)
$output = '$this->arrayMap(\'smarty_modifier_'.$func.'\', array('.$params.'))';
else
$output = 'smarty_modifier_'.$func.'('.$params.')';
}
else
{
if($pluginType & Dwoo::CUSTOM_PLUGIN)
{
$callback = $this->customPlugins[$func]['callback'];
$pluginName = $callback;
}
else
{
$pluginName = 'DwooPlugin_'.$func;
if($pluginType & Dwoo::CLASS_PLUGIN)
{
$callback = array($pluginName, ($pluginType & Dwoo::COMPILABLE_PLUGIN) ? 'compile' : 'process');
}
else
{
$callback = $pluginName . (($pluginType & Dwoo::COMPILABLE_PLUGIN) ? '_compile' : '');
}
}
$params = $this->mapParams($params, $callback, $state);
foreach($params as &$p)
$p = $p[0];
if($pluginType & Dwoo::FUNC_PLUGIN)
{
if($pluginType & Dwoo::COMPILABLE_PLUGIN)
{
if($mapped)
$this->triggerError('The @ operator can not be used on compiled plugins.', E_USER_ERROR);
$funcCompiler = 'DwooPlugin_'.$func.'_compile';
array_unshift($params, $this);
$output = call_user_func_array($funcCompiler, $params);
}
else
{
array_unshift($params, '$this');
$params = $this->implode_r($params);
if($mapped)
$output = '$this->arrayMap(\''.$pluginName.'\', array('.$params.'))';
else
$output = $pluginName.'('.$params.')';
}
}
else
{
if($pluginType & Dwoo::COMPILABLE_PLUGIN)
{
if($mapped)
$this->triggerError('The @ operator can not be used on compiled plugins.', E_USER_ERROR);
$funcCompiler = array('DwooPlugin_'.$func, 'compile');
array_unshift($params, $this);
$output = call_user_func_array($funcCompiler, $params);
}
else
{
$params = $this->implode_r($params);
if($pluginType & Dwoo::CUSTOM_PLUGIN)
{
if(is_object($callback[0]))
$output = ($mapped ? '$this->arrayMap' : 'call_user_func_array').'(array($this->customPlugins[\''.$func.'\'][0], \''.$callback[1].'\'), array('.$params.'))';
else
$output = ($mapped ? '$this->arrayMap' : 'call_user_func_array').'(array(\''.$callback[0].'\', \''.$callback[1].'\'), array('.$params.'))';
}
elseif($mapped)
$output = '$this->arrayMap(array($this->getObjectPlugin(\'DwooPlugin_'.$func.'\'), \'process\'), array('.$params.'))';
else
$output = '$this->classCall(\''.$func.'\', array('.$params.'))';
}
}
}
}
if($curBlock === 'var' || $m[1] === null)
{
return $output;
}
elseif($curBlock === 'string' || $curBlock === 'root')
return $m[1].'.'.$output.'.'.$m[1].(isset($add)?$add:null);
}
/**
* recursively implodes an array in a similar manner as var_export() does but with some tweaks
* to handle pre-compiled values and the fact that we do not need to enclose everything with
* "array" and do not require top-level keys to be displayed
*
* @param array $params the array to implode
* @param bool $recursiveCall if set to true, the function outputs key names for the top level
* @return string the imploded array
*/
protected function implode_r(array $params, $recursiveCall = false)
{
$out = '';
foreach($params as $k=>$p)
{
if(is_array($p))
{
$out2 = 'array(';
foreach($p as $k2=>$v)
$out2 .= var_export($k2, true).' => '.(is_array($v) ? 'array('.$this->implode_r($v, true).')' : $v).', ';
$p = rtrim($out2, ', ').')';
}
if($recursiveCall)
$out .= var_export($k, true).' => '.$p.', ';
else
$out .= $p.', ';
}
return rtrim($out, ', ');
}
/**
* returns the plugin type of a plugin and adds it to the used plugins array if required
*
* @param string $name plugin name, as found in the template
* @return int type as a multi bit flag composed of the Dwoo plugin types constants
*/
protected function getPluginType($name)
{
$pluginType = -1;
if(($this->securityPolicy === null && function_exists($name)) ||
($this->securityPolicy !== null && in_array(strtolower($name), $this->securityPolicy->getAllowedPhpFunctions()) !== false))
{
$phpFunc = true;
}
while($pluginType <= 0)
{
if(isset($this->customPlugins[$name]))
$pluginType = $this->customPlugins[$name]['type'] | Dwoo::CUSTOM_PLUGIN;
elseif(class_exists('DwooPlugin_'.$name, false) !== false)
{
if(is_subclass_of('DwooPlugin_'.$name, 'DwooBlockPlugin'))
$pluginType = Dwoo::BLOCK_PLUGIN;
else
$pluginType = Dwoo::CLASS_PLUGIN;
$interfaces = class_implements('DwooPlugin_'.$name, false);
if(in_array('DwooICompilable', $interfaces) !== false || in_array('DwooICompilableBlock', $interfaces) !== false)
$pluginType |= Dwoo::COMPILABLE_PLUGIN;
}
elseif(function_exists('DwooPlugin_'.$name) !== false)
$pluginType = Dwoo::FUNC_PLUGIN;
elseif(function_exists('DwooPlugin_'.$name.'_compile'))
$pluginType = Dwoo::FUNC_PLUGIN | Dwoo::COMPILABLE_PLUGIN;
elseif(function_exists('smarty_modifier_'.$name) !== false)
$pluginType = Dwoo::SMARTY_MODIFIER;
elseif(function_exists('smarty_function_'.$name) !== false)
$pluginType = Dwoo::SMARTY_FUNCTION;
elseif(function_exists('smarty_block_'.$name) !== false)
$pluginType = Dwoo::SMARTY_BLOCK;
else
{
if($pluginType===-1)
{
try {
DwooLoader::loadPlugin($name);
} catch (Exception $e) {
if(isset($phpFunc))
$pluginType = Dwoo::NATIVE_PLUGIN;
else
throw $e;
}
}
else
$this->triggerError('Plugin "'.$name.'" could not be found', E_USER_ERROR);
$pluginType++;
}
}
if(($pluginType & Dwoo::COMPILABLE_PLUGIN) === 0 && ($pluginType & Dwoo::NATIVE_PLUGIN) === 0)
$this->usedPlugins[$name] = $pluginType;
return $pluginType;
}
/**
* handles the {strip} blocks regex replacement, do not rely on it as it will eventually be moved into it's own plugin
*
* @param array $matches the regex matches with the "1" index being the contents of the {strip} block
* @return string processed string
*/
protected function stripPreprocessorHelper(array $matches)
{
// TODO make this into a separated plugin
return str_replace(array("\n","\r"), null, preg_replace('#^\s*(.+?)\s*$#m', '$1', $matches[1]));
}
/**
* runs htmlentities over the matched blocks when the security policy enforces that
*
* @param array $match matched php block
* @return string the htmlentities-converted string
*/
protected function phpTagEncodingHelper($match)
{
return htmlspecialchars($match[0]);
}
/**
* maps the parameters received from the template onto the parameters required by the given callback
*
* @param array $params the array of parameters
* @param callback $callback the function or method to reflect on to find out the required parameters
* @param int $callType the type of call in the template, 0 = no params, 1 = php-style call, 2 = named parameters call
* @return array parameters sorted in the correct order with missing optional parameters filled
*/
protected function mapParams(array $params, $callback, $callType=2)
{
$map = $this->getParamMap($callback);
$paramlist = array();
// named parameters call
if($callType===2)
{
// transforms the parameter array from (x=>array('paramname'=>array(values))) to (paramname=>array(values))
$ps = array();
foreach($params as $p)
$ps[$p[0]] = $p[1];
// loops over the param map and assigns values from the template or default value for unset optional params
while(list($k,$v) = each($map))
{
// "rest" array parameter, fill every remaining params in it and then break
if($v[0] === '*')
{
if(count($ps) === 0)
{
if($v[1]===false)
$this->triggerError('Rest argument missing for '.str_replace(array('DwooPlugin_', '_compile'), '', (is_array($callback) ? $callback[0] : $callback)), E_USER_ERROR);
else
break;
}
$tmp = array();
$tmp2 = array();
foreach($ps as $i=>$p)
{
$tmp[$i] = $p[0];
$tmp2[$i] = $p[1];
}
$paramlist[$v[0]] = array($tmp, $tmp2);
unset($tmp, $tmp2, $i, $p);
break;
}
// parameter is defined
elseif(isset($ps[$v[0]]))
{
$paramlist[$v[0]] = $ps[$v[0]];
unset($ps[$v[0]]);
}
// parameter is not defined and not optional, throw error
elseif($v[1]===false)
$this->triggerError('Argument '.$k.'/'.$v[0].' missing for '.str_replace(array('DwooPlugin_', '_compile'), '', (is_array($callback) ? $callback[0] : $callback)), E_USER_ERROR);
// enforce lowercased null if default value is null (php outputs NULL with var export)
elseif($v[2]===null)
$paramlist[$v[0]] = array('null', null);
// outputs default value with var_export
else
$paramlist[$v[0]] = array(var_export($v[2], true), $v[2]);
}
}
// php call or no parameter call
elseif($callType===1||$callType===0)
{
// loops over the param map and assigns values from the template or default value for unset optional params
while(list($k,$v) = each($map))
{
// "rest" array parameter, fill every remaining params in it and then break
if($v[0] === '*')
{
if(count($params) === 0)
{
if($v[1]===false)
$this->triggerError('Rest argument missing for '.str_replace(array('DwooPlugin_', '_compile'), '', (is_array($callback) ? $callback[0] : $callback)), E_USER_ERROR);
else
break;
}
$tmp = array();
$tmp2 = array();
$i = 0;
foreach($params as $p)
{
$tmp[$i] = $p[0];
$tmp2[$i++] = $p[1];
}
$paramlist[$v[0]] = array($tmp, $tmp2);
unset($tmp, $tmp2, $i, $p);
break;
}
// parameter is defined
elseif(empty($params)===false)
{
$paramlist[$v[0]] = array_shift($params);
}
// parameter is not defined and not optional, throw error
elseif($v[1]===false)
$this->triggerError('Argument '.$k.'/'.$v[0].' missing for '.str_replace(array('DwooPlugin_', '_compile'), '', (is_array($callback) ? $callback[0] : $callback)), E_USER_ERROR);
// enforce lowercased null if default value is null (php outputs NULL with var export)
elseif($v[2]===null)
$paramlist[$v[0]] = array('null', null);
// outputs default value with var_export
else
$paramlist[$v[0]] = array(var_export($v[2], true), $v[2]);
}
}
// parser failed miserably
else
$this->triggerError('This should not happen, please report it if you see this message', E_USER_ERROR);
return $paramlist;
}
/**
* returns the parameter map of the given callback, it filters out entries typed as Dwoo and DwooCompiler and turns the rest parameter into a "*"
*
* @param callback $callback the function/method to reflect on
* @return array processed parameter map
*/
protected function getParamMap($callback)
{
if(is_null($callback))
return array(array('*', true));
if(is_array($callback))
$ref = new ReflectionMethod($callback[0], $callback[1]);
else
$ref = new ReflectionFunction($callback);
$out = array();
foreach($ref->getParameters() as $param)
{
if(($class = $param->getClass()) !== null && $class->name === 'Dwoo')
continue;
if(($class = $param->getClass()) !== null && $class->name === 'DwooCompiler')
continue;
if($param->getName() === 'rest' && $param->isArray() === true)
$out[] = array('*', $param->isOptional(), null);
$out[] = array($param->getName(), $param->isOptional(), $param->isOptional() ? $param->getDefaultValue() : null);
}
return $out;
}
/**
* returns a default instance of this compiler, used by default by all Dwoo templates that do not have a
* specific compiler assigned and when you do not override the default compiler factory function
*
* @see Dwoo::setDefaultCompilerFactory()
* @return DwooCompiler
*/
public static function compilerFactory()
{
if(self::$instance === null)
self::$instance = new self;
return self::$instance;
}
/**
* triggers a compiler error
*
* @param string $message error message
* @param int $level error level, one of the PHP's E_* constants
*/
public function triggerError($message, $level=E_USER_NOTICE)
{
trigger_error('DwooCompiler error : '.$message."
\r\nNear : ".htmlentities(substr($this->template, max(0, $this->pointer-30), 130)), $level);
}
}
?>