Файл: library/XenForo/Template/Compiler.php
Строк: 1900
<?php
/**
* General template compiling class. This takes a string (template) and converts it
* into PHP code. This code represents the full statements.
*
* Most methods are public so as to be usable by the tag/function handlers. Externally,
* {@link compile()} is the primary method to use for basic compilation.
*
* @package XenForo_Template
*/
class XenForo_Template_Compiler
{
/**
* Local cache parsed templates. Used for includes
*
* @var array
*/
protected static $_templateCache = array();
/**
* The type of compiler. This should be unique per class, based on the source
* for things like included templates, etc.
*
* @var string
*/
protected static $_compilerType = 'public';
/**
* The text to compile
*
* @var string
*/
protected $_text = '';
/**
* Array of objects that handle the named template tags. Key is tag name (lower case)
* and value is the object.
*
* @var array
*/
protected $_tagHandlers = array();
/**
* Array of objects that handle the named template functions. Key is the function
* name (lower case) and value is the object.
*
* @var array
*/
protected $_functionHandlers = array();
/**
* Default options for compilation. These will be used if individual handles do not
* override them. Handlers may override all of them or individual ones.
*
* @var array
*/
protected $_options = array(
'varEscape' => 'htmlspecialchars(%s, ENT_QUOTES, 'UTF-8')',
'allowRawStatements' => true,
'disableVarMap' => false
);
/**
* Name of the variable that the should be used to create full statements
*
* @var string
*/
protected $_outputVar = '__output';
/**
* Counter to create a unique variable name
*
* @var integer
*/
protected $_uniqueVarCount = 0;
/**
* Prefix for a variable that holds internal content for the compiler.
*
* @var string
*/
protected $_uniqueVarPrefix = '__compilerVar';
/**
* Controls whether external data (phrases, includes) should be followed
* and inserted when compiling. This can be set to false for test compiles.
*
* @var boolean
*/
protected $_followExternal = true;
protected $_styleId = 0;
protected $_languageId = 0;
protected $_title = '';
protected $_includedTemplates = array();
protected $_failedTemplateIncludes = array();
protected static $_phraseCache = array();
protected $_includedPhrases = array();
protected $_enableDynamicPhraseLoad = true;
/**
* Line number currently on in the original version of the template.
*
* @var integer
*/
protected $_lineNumber = 0;
/**
* Key value set of variables to map. This is primarily used for includes.
* Key is the from, value is the to.
*
* @var array
*/
protected $_variableMap = array();
/**
* Constructor. Sets up text.
*
* @param string Text to compile
*/
public function __construct($text = '')
{
if ($text !== '')
{
$this->setText($text);
}
$this->_setupDefaults();
}
/**
* Set up the defaults. Primarily sets up the handlers for various functions/tags.
*/
protected function _setupDefaults()
{
$this->addFunctionHandlers(array(
'raw' => new XenForo_Template_Compiler_Function_Raw(),
'escape' => new XenForo_Template_Compiler_Function_Escape(),
'urlencode' => new XenForo_Template_Compiler_Function_UrlEncode(),
'jsescape' => new XenForo_Template_Compiler_Function_JsEscape(),
'phrase' => new XenForo_Template_Compiler_Function_Phrase(),
'property' => new XenForo_Template_Compiler_Function_Property(),
'pagenav' => new XenForo_Template_Compiler_Function_PageNav(),
'if' => new XenForo_Template_Compiler_Function_If(),
'checked' => new XenForo_Template_Compiler_Function_CheckedSelected(),
'selected' => new XenForo_Template_Compiler_Function_CheckedSelected(),
'date' => new XenForo_Template_Compiler_Function_DateTime(),
'time' => new XenForo_Template_Compiler_Function_DateTime(),
'datetime' => new XenForo_Template_Compiler_Function_DateTime(),
'number' => new XenForo_Template_Compiler_Function_Number(),
'link' => new XenForo_Template_Compiler_Function_Link(),
'adminlink' => new XenForo_Template_Compiler_Function_Link(),
'calc' => new XenForo_Template_Compiler_Function_Calc(),
'array' => new XenForo_Template_Compiler_Function_Array(),
'count' => new XenForo_Template_Compiler_Function_Count(),
'helper' => new XenForo_Template_Compiler_Function_Helper(),
'string' => new XenForo_Template_Compiler_Function_String(),
));
$this->addTagHandlers(array(
'foreach' => new XenForo_Template_Compiler_Tag_Foreach(),
'if' => new XenForo_Template_Compiler_Tag_If(),
'elseif' => new XenForo_Template_Compiler_Tag_If(),
'else' => new XenForo_Template_Compiler_Tag_If(),
'contentcheck' => new XenForo_Template_Compiler_Tag_If(),
'navigation' => new XenForo_Template_Compiler_Tag_Navigation(),
'breadcrumb' => new XenForo_Template_Compiler_Tag_Navigation(),
'title' => new XenForo_Template_Compiler_Tag_Title(),
'description' => new XenForo_Template_Compiler_Tag_Description(),
'h1' => new XenForo_Template_Compiler_Tag_H1(),
'sidebar' => new XenForo_Template_Compiler_Tag_Sidebar(),
'topctrl' => new XenForo_Template_Compiler_Tag_TopCtrl(),
'container' => new XenForo_Template_Compiler_Tag_Container(),
'require' => new XenForo_Template_Compiler_Tag_Require(),
'include' => new XenForo_Template_Compiler_Tag_Include(),
'edithint' => new XenForo_Template_Compiler_Tag_EditHint(),
'set' => new XenForo_Template_Compiler_Tag_Set(),
'hook' => new XenForo_Template_Compiler_Tag_Hook(),
'callback' => new XenForo_Template_Compiler_Tag_Callback(),
'formaction' => new XenForo_Template_Compiler_Tag_FormAction(),
'datetime' => new XenForo_Template_Compiler_Tag_DateTime(),
'avatar' => new XenForo_Template_Compiler_Tag_Avatar(),
'username' => new XenForo_Template_Compiler_Tag_Username(),
'likes' => new XenForo_Template_Compiler_Tag_Likes(),
'follow' => new XenForo_Template_Compiler_Tag_Follow(),
'pagenav' => new XenForo_Template_Compiler_Tag_PageNav(),
// note: comment and untreated are handled by the lexer/parser
));
}
/**
* Modifies the default options. Note that this merges into the options, maintaining
* any that are not specified in the parameter.
*
* @param array
*
* @return XenForo_Template_Compiler Fluent interface ($this)
*/
public function setDefaultOptions(array $options)
{
$this->_options = array_merge($this->_options, $options);
return $this;
}
/**
* Gets the current set of default options.
*
* @return array
*/
public function getDefaultOptions()
{
return $this->_options;
}
/**
* Sets the text to be compiled.
*
* @param string
*/
public function setText($text)
{
$this->_text = strval($text);
}
/**
* Adds or replaces a template tag handler.
*
* @param string Name of tag to handle
* @param XenForo_Template_Compiler_Tag_Interface Handler object
*
* @return XenForo_Template_Compiler Fluent interface ($this)
*/
public function addTagHandler($tag, XenForo_Template_Compiler_Tag_Interface $handler)
{
$this->_tagHandlers[strtolower($tag)] = $handler;
return $this;
}
/**
* Adds or replaces an array of template tag handlers.
*
* @param array Tag handlers; key: tag name, value: object
*
* @return XenForo_Template_Compiler Fluent interface ($this)
*/
public function addTagHandlers(array $tags)
{
foreach ($tags AS $tag => $handler)
{
$this->addTagHandler($tag, $handler);
}
return $this;
}
/**
* Adds or replaces a template function handler.
*
* @param string Name of function to handle
* @param XenForo_Template_Compiler_Function_Interface Handler object
*
* @return XenForo_Template_Compiler Fluent interface ($this)
*/
public function addFunctionHandler($function, XenForo_Template_Compiler_Function_Interface $handler)
{
$this->_functionHandlers[strtolower($function)] = $handler;
return $this;
}
/**
* Adds or replaces an array of template function handlers.
*
* @param array Function handlers; key: function name, value: object
*
* @return XenForo_Template_Compiler Fluent interface ($this)
*/
public function addFunctionHandlers(array $functions)
{
foreach ($functions AS $function => $handler)
{
$this->addFunctionHandler($function, $handler);
}
return $this;
}
/**
* Gets the variable name that full statements will write their contents into.
*
* @return string
*/
public function getOutputVar()
{
return $this->_outputVar;
}
/**
* Sets the variable name that full statements will write their contents into.
*
* @param string
*/
public function setOutputVar($_outputVar)
{
$this->_outputVar = strval($_outputVar);
}
/**
* Gets a unique variable name for an internal variable.
*
* @return string
*/
public function getUniqueVar()
{
return $this->_uniqueVarPrefix . ++$this->_uniqueVarCount;
}
/**
* Compiles this template from a string. Returns any number of statements.
*
* @param string $title Title of this template (required to prevent circular references)
* @param integer $styleId Style ID this template belongs to (for template includes)
* @param integer $languageId Language ID this compilation is for (used for phrases)
*
* @return string
*/
public function compile($title = '', $styleId = 0, $languageId = 0)
{
$segments = $this->lexAndParse();
return $this->compileParsed($segments, $title, $styleId, $languageId);
}
/**
* Compiles this template from its parsed output.
*
* @param string|array $segments
* @param string $title Title of this template (required to prevent circular references)
* @param integer $styleId Style ID this template belongs to (for template includes)
* @param integer $languageId Language ID this compilation is for (used for phrases)
*
* @return string
*/
public function compileParsed($segments, $title, $styleId, $languageId)
{
$this->_title = $title;
$this->_styleId = $styleId;
$this->_languageId = $languageId;
$this->_includedTemplates = array();
$this->_failedTemplateIncludes = array();
if (!is_string($segments) && !is_array($segments))
{
throw new XenForo_Exception('Got unexpected, non-string/non-array segments for compilation.');
}
$this->_findAndLoadPhrasesFromSegments($segments);
$statements = $this->compileSegments($segments);
return $this->getOutputVarInitializer() . $statements->getFullStatements($this->_outputVar);
}
/**
* Compiles this template from its parsed output. The template is considered to be plain
* text (the default variable escaping is disabled).
*
* @param string|array $segments
* @param string $title Title of this template (required to prevent circular references)
* @param integer $styleId Style ID this template belongs to (for template includes)
* @param integer $languageId Language ID this compilation is for (used for phrases)
*
* @return string
*/
public function compileParsedPlainText($segments, $title, $styleId, $languageId)
{
$existingOptions = $this->getDefaultOptions();
$this->setDefaultOptions(array('varEscape' => false));
$compiled = $this->compileParsed($segments, $title, $styleId, $languageId);
$this->setDefaultOptions($existingOptions);
return $compiled;
}
/**
* Helper funcion to compile the provided segments into the specified variable.
* This is commonly used to simplify compilation of data that needs to be passed
* into a function (eg, the children of a form tag).
*
* @param string|array $segments Segmenets
* @param string $var Name of the variable to compile into. If generateVar is true, this will be written to (by ref).
* @param array $options Compiler options
* @param boolean $generateVar Whether to generate the var in argument 2 or use the provided input
*
* @return string Full compiled statements
*/
public function compileIntoVariable($segments, &$var = '', array $options = null, $generateVar = true)
{
if ($generateVar)
{
$var = $this->getUniqueVar();
}
$oldOutputVar = $this->getOutputVar();
$this->setOutputVar($var);
$output =
$this->getOutputVarInitializer()
. $this->compileSegments($segments, $options)->getFullStatements($var);
$this->setOutputVar($oldOutputVar);
return $output;
}
/**
* Gets the PHP statement that initializers the output var.
*
* @return string
*/
public function getOutputVarInitializer()
{
return '$' . $this->_outputVar . " = '';n";
}
/**
* Combine uncompiled segments into a string of PHP code. This is simply a helper
* function that compiles and then combines them for you.
*
* @param string|array Segment(s)
* @param array|null Override options. If specified, this represents all options.
*
* @return string Valid PHP code
*/
public function compileAndCombineSegments($segments, array $options = null)
{
if (!is_array($options))
{
$options = $this->_options;
}
$options = array_merge($options, array('allowRawStatements' => false));
return $this->compileSegments($segments, $options)->getPartialStatement();
}
/**
* Lex and parse the template into segments for final compilation.
*
* @return array Parsed segments
*/
public function lexAndParse()
{
$lexer = new XenForo_Template_Compiler_Lexer($this->_text);
$parser = new XenForo_Template_Compiler_Parser();
try
{
while ($lexer->yylex() !== false)
{
$parser->doParse($lexer->match[0], $lexer->match[1]);
$parser->setLineNumber($lexer->line); // if this is before the doParse, it seems to give wrong numbers
}
$parser->doParse(0, 0);
}
catch (Exception $e)
{
// from lexer, can't use the base exception, re-throw
throw new XenForo_Template_Compiler_Exception(new XenForo_Phrase('line_x_template_syntax_error', array('number' => $lexer->line)), true);
}
// XenForo_Template_Compiler_Exception: ok -- no need to catch and rethrow
return $parser->getOutput();
}
/**
* Compile segments into an array of PHP code.
*
* @param string|array Segment(s)
* @param array|null Override options. If specified, this represents all options.
*
* @return XenForo_Template_Compiler_Statement_Collection Collection of parts of a statement or sub statements
*/
public function compileSegments($segments, array $options = null)
{
$segments = $this->prepareSegmentsForIteration($segments);
if (!is_array($options))
{
$options = $this->_options;
}
$statement = $this->getNewStatementCollection();
foreach ($segments AS $segment)
{
$compiled = $this->compileSegment($segment, $options);
if ($compiled !== '' && $compiled !== null)
{
$statement->addStatement($compiled);
}
}
return $statement;
}
/**
* Prepare a collection of segments for iteration. This sanitizes the segments
* so that each step will give you the next segment, which itself may be a string
* or an array.
*
* @param string|array
*
* @return array
*/
public function prepareSegmentsForIteration($segments)
{
if (!is_array($segments))
{
// likely a string (simple literal)
$segments = array($segments);
}
else if (isset($segments['type']))
{
// a simple curly var/function
$segments = array($segments);
}
return $segments;
}
/**
* Compile segment into PHP code
*
* @param string|array Segment
* @param array Override options, must be specified
*
* @return string
*/
public function compileSegment($segment, array $options)
{
if (is_string($segment))
{
$this->setLastVistedSegment($segment);
return $this->compilePlainText($segment, $options);
}
else if (is_array($segment) && isset($segment['type']))
{
$this->setLastVistedSegment($segment);
switch ($segment['type'])
{
case 'TAG':
return $this->compileTag(
$segment['name'], $segment['attributes'],
isset($segment['children']) ? $segment['children'] : array(),
$options
);
case 'CURLY_VAR':
return $this->compileVar($segment['name'], $segment['keys'], $options);
case 'CURLY_FUNCTION':
return $this->compileFunction($segment['name'], $segment['arguments'], $options);
}
}
else if ($segment === null)
{
return '';
}
throw $this->getNewCompilerException(new XenForo_Phrase('internal_compiler_error_unknown_segment_type'));
}
/**
* Sets the last segment that has been visited, updating the line number
* to reflect this.
*
* @param mixed $segment
*/
public function setLastVistedSegment($segment)
{
if (is_array($segment) && isset($segment['type']))
{
if (!empty($segment['line']))
{
$this->_lineNumber = $segment['line'];
}
}
}
/**
* Escape a string for use inside a single-quoted string.
*
* @param string
*
* @return string
*/
public function escapeSingleQuotedString($string)
{
return str_replace(array('\', "'"), array('\\', "'"), $string);
}
/**
* Compile a plain text segment.
*
* @param string Text to compile
* @param array Options
*/
public function compilePlainText($text, array $options)
{
return "'" . $this->escapeSingleQuotedString($text) . "'";
}
/**
* Compile a tag segment. Mostly handled by the specified tag handler.
*
* @param string Tag found
* @param array Attributes (key: name, value: value)
* @param array Any nodes (text, var, tag) that are within this tag
* @param array Options
*/
public function compileTag($tag, array $attributes, array $children, array $options)
{
$tag = strtolower($tag);
if (isset($this->_tagHandlers[$tag]))
{
return $this->_tagHandlers[$tag]->compile($this, $tag, $attributes, $children, $options);
}
else
{
throw $this->getNewCompilerException(new XenForo_Phrase('unknown_tag_x', array('tag' => $tag)));
}
}
/**
* Compile a var segment.
*
* @param string Name of variable found, not including keys
* @param array Keys, may be empty
* @param array Options
*/
public function compileVar($name, $keys, array $options)
{
$name = $this->resolveMappedVariable($name, $options);
$varName = '$' . $name;
if (!empty($keys) && is_array($keys))
{
foreach ($keys AS $key)
{
if (is_string($key))
{
$varName .= "['" . $this->escapeSingleQuotedString($key) . "']";
}
else if (isset($key['type']) && $key['type'] == 'CURLY_VAR')
{
$varName .= '[' . $this->compileVar($key['name'], $key['keys'], array_merge($options, array('varEscape' => false))) . ']';
}
}
}
if (!empty($options['varEscape']))
{
return sprintf($options['varEscape'], $varName);
}
else
{
return $varName;
}
}
/**
* Compile a function segment.
*
* @param string Name of function found
* @param array Arguments (really should have at least 1 value). Each argument may be any number of segments
* @param array Options
*/
public function compileFunction($function, array $arguments, array $options)
{
$function = strtolower($function);
if (isset($this->_functionHandlers[$function]))
{
return $this->_functionHandlers[$function]->compile($this, $function, $arguments, $options);
}
else
{
throw $this->getNewCompilerException(new XenForo_Phrase('unknown_function_x', array('function' => $function)));
}
}
/**
* Compiles a variable reference. A var ref is a string that looks somewhat like a variable.
* It is used in some arguments to simplify variable access and only allow variables.
*
* Data received is any number of segments containing strings or variable segments.
*
* Examples: $var, $var.key, $var.{$key}.2, {$key}, {$key}.blah, {$key.blah}.x
*
* @param string|array Variable reference segment(s)
* @param array Options
*
* @return string PHP code to access named variable
*/
public function compileVarRef($varRef, array $options)
{
$replacements = array();
if (is_array($varRef))
{
if (!isset($varRef[0]))
{
$varRef = array($varRef);
}
$newVarRef = '';
foreach ($varRef AS $segment)
{
if (is_string($segment))
{
$newVarRef .= $segment;
}
else
{
$newVarRef .= '?';
$replacements[] = $segment;
}
}
$varRef = $newVarRef;
}
$parts = explode('.', $varRef);
$variable = array_shift($parts);
if ($variable == '?')
{
$variable = $this->compileSegment(array_shift($replacements), array_merge($options, array('varEscape' => false)));
if (!preg_match('#^$[a-zA-Z_]#', $variable))
{
throw $this->getNewCompilerException(new XenForo_Phrase('invalid_variable_reference'));
}
}
else if (!preg_match('#^$([a-zA-Z_][a-zA-Z0-9_]*)$#', $variable))
{
throw $this->getNewCompilerException(new XenForo_Phrase('invalid_variable_reference'));
}
$keys = array();
foreach ($parts AS $part)
{
if ($part == '?')
{
$part = $this->compileSegment(array_shift($replacements), array_merge($options, array('varEscape' => false)));
}
else if ($part === '' || strpos($part, '?') !== false)
{
// empty key or simply contains a replacement
throw $this->getNewCompilerException(new XenForo_Phrase('invalid_variable_reference'));
}
else
{
$part = "'" . $this->escapeSingleQuotedString($part) . "'";
}
$keys[] = '[' . $part . ']';
}
$variable = '$' . $this->resolveMappedVariable(substr($variable, 1), $options);
return $variable . implode('', $keys);
}
/**
* Parses a set of named arguments. Each argument should be in the form of "key=value".
* The key must be literal, but the value can be anything.
*
* @param array Arguments to treat as named
*
* @return array Key is the argument name, value is segment(s) to be compiled
*/
function parseNamedArguments(array $arguments)
{
$params = array();
foreach ($arguments AS $argument)
{
if (!isset($argument[0]) || !is_string($argument[0]) || !preg_match('#^([a-z0-9_.]+)=#i', $argument[0], $match))
{
throw $this->getNewCompilerException(new XenForo_Phrase('named_parameter_not_specified_correctly'));
}
$name = $match[1];
$nameRemoved = substr($argument[0], strlen($match[0]));
if ($nameRemoved === false)
{
// we ate the whole string, remove the argument
unset($argument[0]);
}
else
{
$argument[0] = $nameRemoved;
}
$nameParts = explode('.', $name);
if (count($nameParts) > 1)
{
$pointer =& $params;
foreach ($nameParts AS $namePart)
{
if (!isset($pointer[$namePart]))
{
$pointer[$namePart] = array();
}
$pointer =& $pointer[$namePart];
}
$pointer = $argument;
}
else
{
$params[$name] = $argument;
}
}
return $params;
}
/**
* Compiled a set of named params into a set of named params that can be used as PHP code.
* The key is a single quoted string.
*
* @param array See {@link parseNamedArguments()}. Key is the name, value is segments for that param.
* @param array Compiler options
* @param array A list of named params should be compiled as conditions instead of plain output
*
* @return array
*/
public function compileNamedParams(array $params, array $options, array $compileAsCondition = array())
{
$compiled = array();
foreach ($params AS $name => $value)
{
if (in_array($name, $compileAsCondition))
{
$compiled[$name] = $this->parseConditionExpression($value, $options);
}
else
{
if (is_array($value))
{
// if an associative array, not a list of segments
reset($value);
list($key, ) = each($value);
if (is_string($key))
{
$compiled[$name] = $this->compileNamedParams($value, $options);
continue;
}
}
$compiled[$name] = $this->compileAndCombineSegments($value, $options);
}
}
return $compiled;
}
/**
* Build actual PHP code from a set of compiled named params
*
* @param array $compiled Already compiled named params. See {@link compileNamedParams}.
*
* @return string
*/
public function buildNamedParamCode(array $compiled)
{
if (!$compiled)
{
return 'array()';
}
$output = "array(n";
$i = 0;
foreach ($compiled AS $name => $value)
{
if (is_array($value))
{
$value = $this->buildNamedParamCode($value);
}
if ($i > 0)
{
$output .= ",n";
}
$output .= "'" . $this->escapeSingleQuotedString($name) . "' => $value";
$i++;
}
$output .= "n)";
return $output;
}
/**
* Takes a compiled set of named parameters and turns them into PHP code (an array).
*
* @param array See {@link parseNamedArguments()}. Key is the name, value is segments for that param.
* @param array Compiler options
* @param array A list of named params should be compiled as conditions instead of plain output
*
* @return string PHP code for an array
*/
public function getNamedParamsAsPhpCode(array $params, array $options, array $compileAsCondition = array())
{
$compiled = $this->compileNamedParams($params, $options, $compileAsCondition);
return $this->buildNamedParamCode($compiled);
}
/**
* Creates a new raw statement handler.
*
* @param string Quickly set a statement
*
* @return XenForo_Template_Compiler_Statement_Raw
*/
public function getNewRawStatement($statement = '')
{
return new XenForo_Template_Compiler_Statement_Raw($statement);
}
/**
* Creates a new statement collection handler.
*
* @return XenForo_Template_Compiler_Statement_Collection
*/
public function getNewStatementCollection()
{
return new XenForo_Template_Compiler_Statement_Collection();
}
/**
* Creates a new compiler exception.
*
* @param string $message Optional message
* @param mixed $segment The segment that caused this. If specified and has a line number, that line is reported.
*
* @return XenForo_Template_Compiler_Exception
*/
public function getNewCompilerException($message = '', $segment = false)
{
if (is_array($segment) && !empty($segment['line']))
{
$lineNumber = $segment['line'];
}
else if (is_int($segment) && !empty($segment))
{
$lineNumber = $segment;
}
else
{
$lineNumber = $this->_lineNumber;
}
if ($lineNumber)
{
$message = new XenForo_Phrase('line_x', array('line' => $lineNumber)) . ': ' . $message;
}
if ($this->_title)
{
$message = $this->_title . ' - ' . $message;
}
$e = new XenForo_Template_Compiler_Exception($message, true);
$e->setLineNumber($lineNumber);
return $e;
}
/**
* Creates a new compiler exception for an incorrect amount of arguments.
*
* @param mixed $segment The segment that caused this. If specified and has a line number, that line is reported.
*
* @return XenForo_Template_Compiler_Exception
*/
public function getNewCompilerArgumentException($segment = false)
{
return $this->getNewCompilerException(new XenForo_Phrase('incorrect_arguments'), $segment);
}
/**
* Determines if the segment is a tag with the specified name.
*
* @param string|array Segment
* @param string Tag name
*
* @return boolean
*/
public function isSegmentNamedTag($segment, $tagName)
{
return (is_array($segment) && isset($segment['type']) && $segment['type'] == 'TAG' && $segment['name'] == $tagName);
}
/**
* Parses a conditional expression into valid PHP code
*
* @param string|array The original unparsed condition. This will consist of plaintext or curly var/function segments.
* @param array Compiler options
*
* @return string Valid PHP code for the condition
*/
public function parseConditionExpression($origCondition, array $options)
{
$placeholders = array();
$placeholderChar = "x1A"; // substitute character in ascii
if ($origCondition === '')
{
throw $this->getNewCompilerException(new XenForo_Phrase('invalid_condition_expression'));
}
if (is_string($origCondition))
{
$condition = $origCondition;
}
else
{
$condition = '';
foreach ($this->prepareSegmentsForIteration($origCondition) AS $segment)
{
if (is_string($segment))
{
if (strpos($segment, $placeholderChar) !== false)
{
throw $this->getNewCompilerException(new XenForo_Phrase('invalid_condition_expression'));
}
$condition .= $segment;
}
else
{
$condition .= $placeholderChar;
$placeholders[] = $this->compileSegment($segment, array_merge($options, array('varEscape' => false)));
}
}
}
return $this->_parseConditionExpression($condition, $placeholders);
}
/**
* Internal function for parsing a condition expression. Note that the variables
* passed into this will be modified by reference.
*
* @param string Expression with placeholders replaced with "x1A"
* @param array Placeholders for variables/functions
* @param boolean Whether to return when we match a right parenthesis
*
* @return string Parsed condition
*/
protected function _parseConditionExpression(&$expression, array &$placeholders,
$internalExpression = false, $isFunction = false)
{
if ($internalExpression && $isFunction && strlen($expression) > 0 && $expression[0] == ')')
{
$expression = substr($expression, 1);
return '()';
}
$state = 'value';
$endState = 'operator';
$compiled = '';
$allowedFunctions = 'is_array|is_object|is_string|isset|empty'
. '|array|array_key_exists|count|in_array|array_search'
. '|preg_match|preg_match_all|strpos|stripos|strlen|trim'
. '|ceil|floor|round|max|min|mt_rand|rand';
do
{
$eatChars = 0;
if ($state == 'value')
{
if (preg_match('#^s+#', $expression, $match))
{
// ignore whitespace
$eatChars = strlen($match[0]);
}
else if ($expression[0] == "x1A")
{
$compiled .= array_shift($placeholders);
$state = 'operator';
$eatChars = 1;
}
else if ($expression[0] == '(')
{
$expression = substr($expression, 1);
$compiled .= $this->_parseConditionExpression($expression, $placeholders, true);
$state = 'operator';
continue; // not eating anything, so must continue
}
else if (preg_match('#^(-|!)#', $expression, $match))
{
$compiled .= $match[0];
$state = 'value'; // we still need a value after this, simply modifies the following value
$eatChars = strlen($match[0]);
}
else if (preg_match('#^(d+(.d+)?|true|false|null)#', $expression, $match))
{
$compiled .= $match[0];
$state = 'operator';
$eatChars = strlen($match[0]);
}
else if (preg_match('#^(' . $allowedFunctions . ')(#i', $expression, $match))
{
$expression = substr($expression, strlen($match[0]));
$compiled .= $match[1] . $this->_parseConditionExpression($expression, $placeholders, true, true);
$state = 'operator';
continue; // not eating anything, so must continue
}
else if (preg_match('#^('|")#', $expression, $match))
{
$quoteClosePos = strpos($expression, $match[0], 1); // skip initial
if ($quoteClosePos === false)
{
throw $this->getNewCompilerException(new XenForo_Phrase('invalid_condition_expression'));
}
$quoted = substr($expression, 1, $quoteClosePos - 1);
$string = array();
$i = 0;
foreach (explode("x1A", $quoted) AS $quotedPart)
{
if ($i % 2 == 1)
{
// odd parts have a ? before them
$string[] = array_shift($placeholders);
}
if ($quotedPart !== '')
{
$string[] = "'" . $this->escapeSingleQuotedString($quotedPart) . "'";
}
$i++;
}
if (!$string)
{
$string[] = "''";
}
$compiled .= '(' . implode(' . ', $string) . ')';
$eatChars = strlen($quoted) + 2; // 2 = quotes on either side
$state = 'operator';
}
}
else if ($state == 'operator')
{
if (preg_match('#^s+#', $expression, $match))
{
// ignore whitespace
$eatChars = strlen($match[0]);
}
else if (preg_match('#^(*|+|-|/|%|===|==|!==|!=|>=|<=|<|>||||&&|and|or|xor|&||)#i', $expression, $match))
{
$eatChars = strlen($match[0]);
$compiled .= " $match[0] ";
$state = 'value';
}
else if ($expression[0] == ')' && $internalExpression)
{
// eat and return successfully
$eatChars = 1;
$state = false;
}
else if ($expression[0] == ',' && $isFunction)
{
$eatChars = 1;
$compiled .= ", ";
$state = 'value';
}
}
if ($eatChars)
{
$expression = substr($expression, $eatChars);
}
else
{
// prevent infinite loops -- if you want to avoid this, use "continue"
throw $this->getNewCompilerException(new XenForo_Phrase('invalid_condition_expression'));
}
} while ($state !== false && $expression !== '' && $expression !== false);
if ($state != $endState && $state !== false)
{
// operator is the end state -- means we're expecting an operator, so it can be anything
throw $this->getNewCompilerException(new XenForo_Phrase('invalid_condition_expression'));
}
return "($compiled)";
}
/**
* Gets the literal value of a curly function's argument. If a literal value
* cannot be obtained, false is returned.
*
* @param string|array $argument
*
* @return string|false Literal value or false
*/
public function getArgumentLiteralValue($argument)
{
if (is_string($argument))
{
return $argument;
}
else if (is_array($argument) && sizeof($argument) == 1 && is_string($argument[0]))
{
return $argument[0];
}
else
{
return false;
}
}
/**
* Quickly gets multiple named attributes and returns any of them that exist.
*
* @param array Attributes for the tag
* @param array Attributes to fetch
*
* @return array Any attributes that existed
*/
public function getNamedAttributes(array $attributes, array $wantedAttributes)
{
$output = array();
foreach ($wantedAttributes AS $wanted)
{
if (isset($attributes[$wanted]))
{
$output[$wanted] = $attributes[$wanted];
}
}
return $output;
}
/**
* Sets whether external data (phrases, includes) should be "followed" and fetched.
* This can be set to false when doing a test compile.
*
* @param boolean $value
*/
public function setFollowExternal($value)
{
$this->_followExternal = (bool)$value;
}
/**
* Sets the line number manually. This may be needed to report a more
* accurate line number when tags manually handle child tags (eg, if).
*
* If this function is not used, the line number from the last tag
* handled by {@link compileSegment()} will be used.
*
* @param integer $lineNumber
*/
public function setLineNumber($lineNumber)
{
$this->_lineNumber = intval($lineNumber);
}
/**
* Gets the current line number.
*
* @return integer
*/
public function getLineNumber()
{
return $this->_lineNumber;
}
/**
* Merges phrases into the existing phrase cache. Phrases are expected
* for all languages.
*
* @param array $phraseData Format: [language id][title] => value
*/
public function mergePhraseCache(array $phraseData)
{
foreach ($phraseData AS $languageId => $phrases)
{
if (!is_array($phrases))
{
continue;
}
if (isset(self::$_phraseCache[$languageId]))
{
self::$_phraseCache[$languageId] = array_merge(self::$_phraseCache[$languageId], $phrases);
}
else
{
self::$_phraseCache[$languageId] = $phrases;
}
}
}
/**
* Resets the phrase cache. This should be done when a phrase value
* changes, before compiling templates.
*/
public static function resetPhraseCache()
{
self::$_phraseCache = array();
}
/**
* Gets the value for a phrase in the language the compiler is compiling for.
*
* @param string $title
*
* @return string|false
*/
public function getPhraseValue($title)
{
if (!$this->_followExternal)
{
return false;
}
$this->_includedPhrases[$title] = true;
if (isset(self::$_phraseCache[$this->_languageId][$title]))
{
return self::$_phraseCache[$this->_languageId][$title];
}
else
{
return false;
}
}
/**
* Disables dynamic phrase loading. Generally only desired for tests.
*/
public function disableDynamicPhraseLoad()
{
$this->_enableDynamicPhraseLoad = false;
}
/**
* Gets a list of all phrases included in this template.
*
* @return array List of phrase titles
*/
public function getIncludedPhrases()
{
return array_keys($this->_includedPhrases);
}
/**
* Gets an already parsed template for inclusion.
*
* @param string $title Name of the template to include
*
* @return string|array Segments
*/
public function includeParsedTemplate($title)
{
if ($title == $this->_title)
{
throw $this->getNewCompilerException(new XenForo_Phrase('circular_reference_found_in_template_includes'));
}
if (!$this->_followExternal)
{
return '';
}
if (!isset(self::$_templateCache[$this->getCompilerType()][$this->_styleId][$title]))
{
self::$_templateCache[$this->getCompilerType()][$this->_styleId][$title] = $this->_getParsedTemplateFromModel($title, $this->_styleId);
}
$info = self::$_templateCache[$this->getCompilerType()][$this->_styleId][$title];
if (is_array($info))
{
if (empty($this->_includedTemplates[$info['id']]))
{
// cache phrases for this template as we haven't included it
$this->_findAndLoadPhrasesFromSegments($info['data']);
}
$this->_includedTemplates[$info['id']] = true;
return $info['data'];
}
else
{
$this->_failedTemplateIncludes[$title] = true;
return '';
}
}
/**
* Finds the phrases used by the specified segments, loads them, and then
* merges them into the local phrase cache.
*
* @param array|string $segments
*/
protected function _findAndLoadPhrasesFromSegments($segments)
{
if (!$this->_enableDynamicPhraseLoad)
{
return;
}
$phrasesUsed = $this->identifyPhrasesInParsedTemplate($segments);
foreach ($phrasesUsed AS $key => $title)
{
if (isset(self::$_phraseCache[$this->_languageId][$title]))
{
unset($phrasesUsed[$key]);
}
}
if ($phrasesUsed)
{
$phraseData = XenForo_Model::create('XenForo_Model_Phrase')->getEffectivePhraseValuesInAllLanguages($phrasesUsed);
$this->mergePhraseCache($phraseData);
}
}
/**
* Helper to go to the model to get the parsed version of the specified template.
*
* @param string $title Title of template
* @param integer $styleId ID of the style the template should apply to
*
* @return false|array Array should have keys of id and data (data should be parsed version of template)
*/
protected function _getParsedTemplateFromModel($title, $styleId)
{
$template = XenForo_Model::create('XenForo_Model_Template')->getEffectiveTemplateByTitle($title, $styleId);
if (isset($template['template_parsed']))
{
return array(
'id' => $template['template_map_id'],
'data' => unserialize($template['template_parsed'])
);
}
else
{
return false;
}
}
/**
* Adds parsed templates to the template cache for the specified style.
*
* @param array $templates Keys are template names, values are parsed vesions of templates
* @param integer $styleId ID of the style that the templates are from
*/
public static function setTemplateCache(array $templates, $styleId = 0)
{
self::_setTemplateCache($templates, $styleId, self::$_compilerType);
}
/**
* Internal handler for setting the template cache.
*
* @param array $templates
* @param integer $styleId
* @param string $compilerType
*/
protected static function _setTemplateCache(array $templates, $styleId, $compilerType)
{
if (empty(self::$_templateCache[$compilerType][$styleId]))
{
self::$_templateCache[$compilerType][$styleId] = $templates;
}
else
{
self::$_templateCache[$compilerType][$styleId] = array_merge(self::$_templateCache[$compilerType][$styleId], $templates);
}
}
/**
* Helper to reset the template cache to reclaim memory or for tests.
*
* @param integer|true $styleId Style ID to reset the cache for; true for all styles
*/
public static function resetTemplateCache($styleId = true)
{
self::_resetTemplateCache($styleId, self::$_compilerType);
}
/**
* Internal handler for resetting the template cache.
*
* @param integer|boolean $styleId
* @param string $compilerType
*/
protected static function _resetTemplateCache($styleId, $compilerType)
{
if ($styleId === true)
{
self::$_templateCache[$compilerType] = array();
}
else
{
self::$_templateCache[$compilerType][$styleId] = array();
}
}
/**
* Removes the named template from the compiler cache.
*
* @param string $title
*/
public static function removeTemplateFromCache($title)
{
self::_removeTemplateFromCache($title, self::$_compilerType);
}
/**
* Internal handler to remove the named template from the specified compiler
* cache.
*
* @param string $title
* @param string $compilerType
*/
protected static function _removeTemplateFromCache($title, $compilerType)
{
if (!$title || !isset(self::$_templateCache[$compilerType]))
{
return;
}
foreach (self::$_templateCache[$compilerType] AS $styleId => $style)
{
if (isset($style[$title]))
{
unset(self::$_templateCache[$compilerType][$styleId][$title]);
}
}
}
/**
* Gets the list of included template IDs (map or actual template IDs).
*
* @return array
*/
public function getIncludedTemplates()
{
return array_keys($this->_includedTemplates);
}
/**
* Gets list of template include template names that didn't exist
*
* @return array
*/
public function getFailedTemplateIncludes()
{
return array_keys($this->_failedTemplateIncludes);
}
/**
* Gets the compiler type. This method generally needs to be overridden
* in child classes because of the lack of LSB.
*
* @return string
*/
public function getCompilerType()
{
return self::$_compilerType;
}
/**
* Identifies the list of phrases that exist in a parsed template. This list
* can be populated even if the template is invalid.
*
* @param string|array $segments List of parsed segments
*
* @return array Unique list of phrases used in this template
*/
public function identifyPhrasesInParsedTemplate($segments)
{
$phrases = $this->_identifyPhrasesInSegments($segments);
return array_unique($phrases);
}
/**
* Internal handler to get the phrases that are used in a collection of
* template segments.
*
* @param string|array $segments
*
* @return array List of phrases used in these segments; phrases may be repeated
*/
protected function _identifyPhrasesInSegments($segments)
{
$phrases = array();
foreach ($this->prepareSegmentsForIteration($segments) AS $segment)
{
if (!is_array($segment) || !isset($segment['type']))
{
continue;
}
switch ($segment['type'])
{
case 'TAG':
$phrases = array_merge($phrases,
$this->_identifyPhrasesInSegments($segment['children'])
);
foreach ($segment['attributes'] AS $attribute)
{
$phrases = array_merge($phrases, $this->_identifyPhrasesInSegments($attribute));
}
break;
case 'CURLY_FUNCTION':
if ($segment['name'] == 'phrase' && isset($segment['arguments'][0]))
{
$literalValue = $this->getArgumentLiteralValue($segment['arguments'][0]);
if ($literalValue !== false)
{
$phrases[] = $literalValue;
}
}
foreach ($segment['arguments'] AS $argument)
{
$phrases = array_merge($phrases, $this->_identifyPhrasesInSegments($argument));
}
}
}
return $phrases;
}
/**
* Resolves the mapping for the specified variable.
*
* @param string $name
* @param array $options Compiler options
*
* @return string
*/
public function resolveMappedVariable($name, array $options)
{
if (!empty($options['disableVarMap']))
{
return $name;
}
$visited = array(); // loop protection
while (isset($this->_variableMap[$name]) && !isset($visited[$name]))
{
$visited[$name] = true;
$name = $this->_variableMap[$name];
}
return $name;
}
/**
* Gets the variable map list.
*
* @return array
*/
public function getVariableMap()
{
return $this->_variableMap;
}
/**
* Sets/merges the variable map list.
*
* @param array $map
* @param boolean $merge If true, merges; otherwise, overwrites
*/
public function setVariableMap(array $map, $merge = false)
{
if ($merge)
{
if ($map)
{
$this->_variableMap = array_merge($this->_variableMap, $map);
}
}
else
{
$this->_variableMap = $map;
}
}
}