core/model/modx/modparser.class.php
<?php
/*
* This file is part of MODX Revolution.
*
* Copyright (c) MODX, LLC. All Rights Reserved.
*
* For complete copyright and license information, see the COPYRIGHT and LICENSE
* files found in the top-level directory of this distribution.
*/
/**
* Represents the MODX parser responsible for processing MODX tags.
*
* This class encapsulates all of the functions for collecting and evaluating
* element tags embedded in text content.
*
* @package modx
*/
class modParser {
/**
* A reference to the modX instance
* @var modX $modx
*/
public $modx= null;
/**
* If the parser is currently processing a tag
* @var bool $_processingTag
*/
protected $_processingTag = false;
/**
* If the parser is currently processing an element
* @var bool $_processingElement
*/
protected $_processingElement = false;
/**
* If the parser is currently processing an uncacheable tag
* @var bool $_processingUncacheable
*/
protected $_processingUncacheable = false;
/**
* If the parser is currently removing all unprocessed tags
* @var bool $_removingUnprocessed
*/
protected $_removingUnprocessed = false;
/**
* If the parser has ever processed uncacheable
*
* @var bool $_startedProcessingUncacheable
*/
protected $_startedProcessingUncacheable = false;
/**
* @param xPDO $modx A reference to the modX|xPDO instance
*/
function __construct(xPDO &$modx) {
$this->modx =& $modx;
}
/**
* Returns true if the parser is currently processing an uncacheable tag
* @return bool
*/
public function isProcessingUncacheable() {
$result = false;
if ($this->isProcessingTag() || $this->isProcessingElement()) $result = (boolean) $this->_processingUncacheable;
return $result;
}
/**
* Returns true if the parser has ever processed an uncacheable tag
* @return bool
*/
public function startedProcessingUncacheable() {
return $this->_startedProcessingUncacheable;
}
/**
* Returns true if the parser is currently removing any unprocessed tags
* @return bool
*/
public function isRemovingUnprocessed() {
$result = false;
if ($this->isProcessingTag() || $this->isProcessingElement()) $result = (boolean) $this->_removingUnprocessed;
return $result;
}
/**
* Returns true if the parser is currently processing a tag
* @return bool
*/
public function isProcessingTag() {
return (boolean) $this->_processingTag;
}
/**
* Returns true if the parser is currently processing an element
* @return bool
*/
public function isProcessingElement() {
return (boolean) $this->_processingElement;
}
public function setProcessingElement($arg = null) {
if (is_bool($arg)) {
$this->_processingElement = $arg;
} elseif ($arg === null) {
$this->_processingElement = !$this->_processingElement ? true : false;
} else {
$this->_processingElement = (boolean)$arg;
}
}
/**
* Collects element tags in a string and injects them into an array.
*
* @param string $origContent The content to collect tags from.
* @param array &$matches An array in which the collected tags will be
* stored (by reference)
* @param string $prefix The characters that define the start of a tag
* (default= "[[").
* @param string $suffix The characters that define the end of a tag
* (default= "]]").
* @return integer The number of tags collected from the content.
*/
public function collectElementTags($origContent, array &$matches, $prefix= '[[', $suffix= ']]') {
$matchCount= 0;
if (!empty ($origContent) && is_string($origContent) && strpos($origContent, $prefix) !== false) {
$openCount= 0;
$offset= 0;
$openPos= 0;
$closePos= 0;
if (($startPos= strpos($origContent, $prefix)) === false) {
return $matchCount;
}
$offset= $startPos +strlen($prefix);
if (($stopPos= strrpos($origContent, $suffix)) === false) {
return $matchCount;
}
$stopPos= $stopPos + strlen($suffix);
$length= $stopPos - $startPos;
$content= $origContent;
while ($length > 0) {
$openCount= 0;
$content= substr($content, $startPos);
$openPos= 0;
$offset= strlen($prefix);
if (($closePos= strpos($content, $suffix, $offset)) === false) {
break;
}
$nextOpenPos= strpos($content, $prefix, $offset);
while ($nextOpenPos !== false && $nextOpenPos < $closePos) {
$openCount++;
$offset= $nextOpenPos + strlen($prefix);
$nextOpenPos= strpos($content, $prefix, $offset);
}
$nextClosePos= strpos($content, $suffix, $closePos + strlen($suffix));
while ($openCount > 0 && $nextClosePos !== false) {
$openCount--;
$closePos= $nextClosePos;
$nextOpenPos= strpos($content, $prefix, $offset);
while ($nextOpenPos !== false && $nextOpenPos < $closePos) {
$openCount++;
$offset= $nextOpenPos + strlen($prefix);
$nextOpenPos= strpos($content, $prefix, $offset);
}
$nextClosePos= strpos($content, $suffix, $closePos + strlen($suffix));
}
$closePos= $closePos +strlen($suffix);
$outerTagLength= $closePos - $openPos;
$innerTagLength= ($closePos -strlen($suffix)) - ($openPos +strlen($prefix));
$matches[$matchCount][0]= substr($content, $openPos, $outerTagLength);
$matches[$matchCount][1]= substr($content, ($openPos +strlen($prefix)), $innerTagLength);
$matchCount++;
if ($nextOpenPos === false) {
$nextOpenPos= strpos($content, $prefix, $closePos);
}
if ($nextOpenPos !== false) {
$startPos= $nextOpenPos;
$length= $length - $nextOpenPos;
} else {
$length= 0;
}
}
}
if ($this->modx->getDebug() === true && !empty($matches)) {
$this->modx->log(modX::LOG_LEVEL_DEBUG, "modParser::collectElementTags \$matches = " . print_r($matches, 1) . "\n");
/* $this->modx->cacheManager->writeFile(MODX_CORE_PATH . 'logs/parser.log', print_r($matches, 1) . "\n", 'a'); */
}
return $matchCount;
}
/**
* Collects and processes any set of tags as defined by a prefix and suffix.
*
* @param string $parentTag The tag representing the element processing this
* tag. Pass an empty string to allow parsing without this recursion check.
* @param string $content The content to process and act on (by reference).
* @param boolean $processUncacheable Determines if noncacheable tags are to
* be processed (default= false).
* @param boolean $removeUnprocessed Determines if unprocessed tags should
* be left in place in the content, or stripped out (default= false).
* @param string $prefix The characters that define the start of a tag
* (default= "[[").
* @param string $suffix The characters that define the end of a tag
* (default= "]]").
* @param array $tokens Indicates that the parser should only process tags
* with the tokens included in this array.
* @param integer $depth The maximum iterations to recursively process tags
* returned by prior passes, 0 by default.
* @return int The number of processed tags
*/
public function processElementTags($parentTag, & $content, $processUncacheable= false, $removeUnprocessed= false, $prefix= "[[", $suffix= "]]", $tokens= array (), $depth= 0) {
if ($processUncacheable) {
$this->_startedProcessingUncacheable = true;
}
$_processingTag = $this->_processingTag;
$_processingUncacheable = $this->_processingUncacheable;
$_removingUnprocessed = $this->_removingUnprocessed;
$this->_processingTag = true;
$this->_processingUncacheable = (boolean) $processUncacheable;
$this->_removingUnprocessed = (boolean) $removeUnprocessed;
$depth = $depth > 0 ? $depth - 1 : 0;
$processed= 0;
$tags= array ();
/* invoke OnParseDocument event */
$this->modx->documentOutput = $content;
$this->modx->invokeEvent('OnParseDocument', array('content' => &$content));
$content = $this->modx->documentOutput;
unset($this->modx->documentOutput);
if ($collected= $this->collectElementTags($content, $tags, $prefix, $suffix, $tokens)) {
$tagMap= array ();
foreach ($tags as $tag) {
$token= substr($tag[1], 0, 1);
if (!$processUncacheable && $token === '!') {
if ($removeUnprocessed) {
$tagMap[$tag[0]]= '';
}
}
elseif (!empty ($tokens) && !in_array($token, $tokens)) {
$collected--;
continue;
}
if ($tag[0] === $parentTag) {
$tagMap[$tag[0]]= '';
$processed++;
continue;
}
$tagOutput= $this->processTag($tag, $processUncacheable);
if (($tagOutput === null || $tagOutput === false) && $removeUnprocessed) {
$tagMap[$tag[0]]= '';
$processed++;
}
elseif ($tagOutput !== null && $tagOutput !== false) {
$tagMap[$tag[0]]= $tagOutput;
if ($tag[0] !== $tagOutput) $processed++;
}
}
$this->mergeTagOutput($tagMap, $content);
if ($processed > 0 && $depth > 0) {
$processed+= $this->processElementTags($parentTag, $content, $processUncacheable, $removeUnprocessed, $prefix, $suffix, $tokens, $depth);
}
}
$this->_removingUnprocessed = $_removingUnprocessed;
$this->_processingUncacheable = $_processingUncacheable;
$this->_processingTag = $_processingTag;
return $processed;
}
/**
* Merges processed tag output into provided content string.
*
* @param array $tagMap An array with full tags as keys and processed output
* as the values.
* @param string $content The content to merge the tag output with (passed by
* reference).
*/
public function mergeTagOutput(array $tagMap, & $content) {
if (!empty ($content) && is_array($tagMap) && !empty ($tagMap)) {
$content= str_replace(array_keys($tagMap), array_values($tagMap), $content);
}
}
/**
* Parses an element/tag property string or array definition.
*
* @param string $propSource A valid property string or array source to
* parse.
* @return array An associative array of property values parsed from
* the property string or array definition.
*/
public function parseProperties($propSource) {
$properties= array ();
if (!empty ($propSource)) {
if (is_string($propSource)) {
$properties = $this->parsePropertyString($propSource, true);
} elseif (is_array($propSource)) {
foreach ($propSource as $propName => &$property) {
if (is_array($property) && array_key_exists('value', $property)) {
$properties[$propName]= $property['value'];
} else {
$properties[$propName]= &$property;
}
}
}
}
return $properties;
}
/**
* Parses an element/tag property string and returns an array of properties.
*
* @param string $string The property string to parse.
* @param boolean $valuesOnly Indicates only the property value should be
* returned.
* @return array The processed properties in array format
*/
public function parsePropertyString($string, $valuesOnly = false) {
$properties = array();
$tagProps= xPDO :: escSplit("&", $string);
foreach ($tagProps as $prop) {
$property= xPDO :: escSplit('=', $prop);
if (count($property) == 2) {
$propName= $property[0];
if (substr($propName, 0, 4) == "amp;") {
$propName= substr($propName, 4);
}
$propValue= $property[1];
$propType= 'textfield';
$propDesc= '';
$propOptions= array();
$pvTmp= xPDO :: escSplit(';', $propValue);
if ($pvTmp && isset ($pvTmp[1])) {
$propDesc= $pvTmp[0];
if (($pvTmp[1]=='list' || $pvTmp[1]=='combo') && isset($pvTmp[3]) && $pvTmp[3]) {
if (!$valuesOnly) {
$propType = modParser::_XType($pvTmp[1]);
$options = explode(',', $pvTmp[2]);
if ($options) {
foreach ($options as $option) $propOptions[] = array('name' => ucfirst($option), 'value' => $option);
}
}
$propValue = $pvTmp[3];
}
elseif ($pvTmp[1]!='list' && $pvTmp[1]!='combo' && isset($pvTmp[2]) && $pvTmp[2]) {
if (!$valuesOnly) {
$propType = modParser::_XType($pvTmp[1]);
}
$propValue = $pvTmp[2];
} else {
$propValue = $pvTmp[0];
}
}
if ($propValue[0] == '`' && $propValue[strlen($propValue) - 1] == '`') {
$propValue= substr($propValue, 1, strlen($propValue) - 2);
}
$propValue= str_replace("``", "`", $propValue);
if ($valuesOnly) {
$properties[$propName]= $propValue;
} else {
$properties[$propName]= array(
'name' => $propName,
'desc' => $propDesc,
'type' => $propType,
'options' => $propOptions,
'value' => $propValue
);
}
}
}
return $properties;
}
/**
* Converts legacy property string types to xtypes.
*
* @access protected
* @param string $type A property type string.
* @return string A valid xtype.
*/
protected function _XType($type) {
$xtype = $type;
switch ($type) {
case 'string':
$xtype = 'textfield';
break;
case 'int':
case 'integer':
case 'float':
$xtype = 'numberfield';
break;
case 'bool':
case 'boolean':
$xtype = 'checkbox';
break;
case 'list':
break;
default:
if (!in_array($xtype, array('checkbox', 'combo', 'datefield', 'numberfield', 'radio', 'textarea', 'textfield', 'timefield'))) {
$xtype = 'textfield';
}
break;
}
return $xtype;
}
/**
* Processes a modElement tag and returns the result.
*
* @param string $tag A full tag string parsed from content.
* @param boolean $processUncacheable
* @return mixed The output of the processed element represented by the
* specified tag.
*/
public function processTag($tag, $processUncacheable = true) {
$this->_processingTag = true;
$element= null;
$elementOutput= null;
$outerTag= $tag[0];
$innerTag= $tag[1];
/* Avoid all processing for comment tags, e.g. [[- comments here]] */
if (substr($innerTag, 0, 1) === '-') {
return "";
}
/* collect any nested element tags in the innerTag and process them */
$this->processElementTags($outerTag, $innerTag, $processUncacheable);
$this->_processingTag = true;
$outerTag= '[[' . $innerTag . ']]';
$tagParts= xPDO :: escSplit('?', $innerTag, '`', 2);
$tagName= trim($tagParts[0]);
$tagPropString= null;
if (isset ($tagParts[1])) {
$tagPropString= trim($tagParts[1]);
}
$token= substr($tagName, 0, 1);
$tokenOffset= 0;
$cacheable= true;
if ($token === '!') {
if (!$processUncacheable) {
$this->_processingTag = false;
return $outerTag;
}
$cacheable= false;
$tokenOffset++;
$token= substr($tagName, $tokenOffset, 1);
} elseif (!$processUncacheable && strpos($tagPropString, '[[!') !== false) {
$this->modx->log(xPDO::LOG_LEVEL_WARN, "You should not call uncached elements inside cached!\nOuter tag: {$tag[0]}\nInner tag {$innerTag}");
$this->_processingTag = false;
return $outerTag;
}
if ($cacheable && $token !== '+') {
$elementOutput= $this->loadFromCache($outerTag);
}
$_restoreProcessingUncacheable = $this->_processingUncacheable;
/* stop processing uncacheable tags so they are not cached in the cacheable content */
if ($this->_processingUncacheable && $cacheable && $this->modx->getOption('parser_recurse_uncacheable', null, true)) {
$this->_processingUncacheable = false;
}
if ($elementOutput === null) {
switch ($token) {
case '-':
$elementOutput = '';
break;
case '+':
$tagName= substr($tagName, 1 + $tokenOffset);
$element= new modPlaceholderTag($this->modx);
$element->set('name', $tagName);
$element->setTag($outerTag);
$elementOutput= $element->process($tagPropString);
break;
case '%':
$tagName= substr($tagName, 1 + $tokenOffset);
$element= new modLexiconTag($this->modx);
$element->set('name', $tagName);
$element->setTag($outerTag);
$element->setCacheable($cacheable);
$elementOutput= $element->process($tagPropString);
break;
case '~':
$tagName= substr($tagName, 1 + $tokenOffset);
$element= new modLinkTag($this->modx);
$element->set('name', $tagName);
$element->setTag($outerTag);
$element->setCacheable($cacheable);
$elementOutput= $element->process($tagPropString);
break;
case '$':
$tagName= substr($tagName, 1 + $tokenOffset);
if ($element= $this->getElement('modChunk', $tagName)) {
$element->set('name', $tagName);
$element->setTag($outerTag);
$element->setCacheable($cacheable);
$elementOutput= $element->process($tagPropString);
}
break;
case '*':
$tagName= substr($tagName, 1 + $tokenOffset);
$nextToken= substr($tagName, 0, 1);
if ($nextToken === '#') {
$tagName= substr($tagName, 1);
}
if (is_array($this->modx->resource->_fieldMeta) && in_array($this->realname($tagName), array_keys($this->modx->resource->_fieldMeta))) {
$element= new modFieldTag($this->modx);
$element->set('name', $tagName);
$element->setTag($outerTag);
$element->setCacheable($cacheable);
$elementOutput= $element->process($tagPropString);
}
else {
$element = $this->getElement('modTemplateVar', $tagName);
// If our element tag was not found (e.i. not an existing TV), create a new instance of
// modFieldTag. We do this to make it possible to use output modifiers such as default. This
// mirrors the behavior of placeholders.
if ($element === false) {
$element = new modFieldTag($this->modx);
}
$element->set('name', $tagName);
$element->setTag($outerTag);
$element->setCacheable($cacheable);
$elementOutput= $element->process($tagPropString);
}
break;
default:
$tagName= substr($tagName, $tokenOffset);
if ($element= $this->getElement('modSnippet', $tagName)) {
$element->set('name', $tagName);
$element->setTag($outerTag);
$element->setCacheable($cacheable);
$elementOutput= $element->process($tagPropString);
}
elseif(!empty($tagName)) {
if ($this->modx->getOption('log_snippet_not_found', null, false)) {
$this->modx->log(xPDO::LOG_LEVEL_ERROR, "Could not find snippet with name {$tagName}.");
}
}
}
}
if (($elementOutput === null || $elementOutput === false) && $outerTag !== $tag[0]) {
$elementOutput = $outerTag;
}
if ($this->modx->getDebug() === true) {
$this->modx->log(xPDO::LOG_LEVEL_DEBUG, "Processing {$outerTag} as {$innerTag} using tagname {$tagName}:\n" . print_r($elementOutput, 1) . "\n\n");
/* $this->modx->cacheManager->writeFile(MODX_BASE_PATH . 'parser.log', "Processing {$outerTag} as {$innerTag}:\n" . print_r($elementOutput, 1) . "\n\n", 'a'); */
}
$this->_processingTag = false;
$this->_processingUncacheable = $_restoreProcessingUncacheable;
return $elementOutput;
}
/**
* Get a modElement instance taking advantage of the modX::$sourceCache.
*
* @param string $class The modElement derivative class to load.
* @param string $name An element name or raw tagName to identify the modElement instance.
* @return modElement|null An instance of the specified modElement derivative class.
*/
public function getElement($class, $name) {
$realname = $this->realname($name);
if (array_key_exists($class, $this->modx->sourceCache) && array_key_exists($realname, $this->modx->sourceCache[$class])) {
/** @var modElement $element */
$element = $this->modx->newObject($class);
$element->fromArray($this->modx->sourceCache[$class][$realname]['fields'], '', true, true);
$element->setPolicies($this->modx->sourceCache[$class][$realname]['policies']);
if (!empty($this->modx->sourceCache[$class][$realname]['source'])) {
if (!empty($this->modx->sourceCache[$class][$realname]['source']['class_key'])) {
$sourceClassKey = $this->modx->sourceCache[$class][$realname]['source']['class_key'];
$this->modx->loadClass('sources.modMediaSource');
/* @var modMediaSource $source */
$source = $this->modx->newObject($sourceClassKey);
$source->fromArray($this->modx->sourceCache[$class][$realname]['source'],'',true,true);
$element->addOne($source,'Source');
}
}
} else {
/** @var modElement $element */
$element = $this->modx->getObjectGraph($class,array('Source' => array()),array('name' => $realname), true);
if ($element && array_key_exists($class, $this->modx->sourceCache)) {
$this->modx->sourceCache[$class][$realname] = array(
'fields' => $element->toArray(),
'policies' => $element->getPolicies(),
'source' => $element->Source ? $element->Source->toArray() : array(),
);
}
elseif(!$element) {
$evtOutput = $this->modx->invokeEvent('OnElementNotFound', array('class' => $class, 'name' => $realname));
$element = false;
if ($evtOutput != false) {
foreach ((array) $evtOutput as $elm) {
if (!empty($elm) && is_string($elm)) {
$element = $this->modx->newObject($class, array(
'name' => $realname,
'snippet' => $elm
));
}
elseif ($elm instanceof modElement ) {
$element = $elm;
}
if ($element) {
break;
}
}
}
}
}
if ($element instanceof modElement) {
$element->set('name', $name);
}
return $element;
}
/**
* Gets the real name of an element containing filter modifiers.
*
* @param string $unfiltered The unfiltered name of a {@link modElement}.
* @return string The name minus any filter modifiers.
*/
public function realname($unfiltered) {
$filtered= $unfiltered;
$split= xPDO :: escSplit(':', $filtered);
if ($split && isset($split[0])) {
$filtered= $split[0];
$propsetSplit = xPDO :: escSplit('@', $filtered);
if ($propsetSplit && isset($propsetSplit[0])) {
$filtered= $propsetSplit[0];
}
}
return $filtered;
}
/**
* Loads output cached by complete tag signature from the elementCache.
*
* @uses modX::$_elementCache Stores all cacheable content from processed
* elements.
* @param string $tag The tag signature representing the element instance.
* @return string The cached output from the element instance.
*/
public function loadFromCache($tag) {
$elementOutput= null;
if (isset ($this->modx->elementCache[$tag])) {
$elementOutput= (string) $this->modx->elementCache[$tag];
}
return $elementOutput;
}
}
/**
* Abstract class representing a pseudo-element that can be parsed.
*
* @abstract You must implement the process() method on derivatives to implement
* a parseable element tag. All element tags are identified by a unique single
* character token at the beginning of the tag string.
* @package modx
*/
abstract class modTag {
/**
* A reference to the modX instance
* @var modX $modx
*/
public $modx= null;
/**
* The name of the tag
* @var string $name
*/
public $name;
/**
* The properties on the tag
* @var array $properties
*/
public $properties;
/**
* The content of the tag
* @var string $_content
*/
public $_content= null;
/**
* The processed output of the tag
* @var string $_output
*/
public $_output= '';
/**
* The result of processing the tag
* @var bool $_result
*/
public $_result= true;
/**
* Just the isolated properties part of the tag string
* @var string $_propertyString
*/
public $_propertyString= '';
/**
* The arranged properties array for this tag
* @var array $_properties
*/
public $_properties= array();
/**
* Whether or not the tag has been processed
* @var boolean $_processed
*/
public $_processed= false;
/**
* The tag string
* @var string $_tag
*/
public $_tag= '';
/**
* The tag initial token ($,%,*,etc)
* @var string $_token
*/
public $_token= '';
/**
* Fields on the tag
* @var array $_fields
*/
public $_fields= array(
'name' => '',
'properties' => ''
);
/**
* Whether or not this tag is marked as cacheable
* @var boolean $_cacheable
*/
public $_cacheable= true;
/**
* Any output/input filters on this tag
* @var array $_filters
*/
public $_filters= array('input' => null, 'output' => null);
/**
* Set a reference to the modX object, load the name and properties, and instantiate the tag class instance.
* @param modX $modx A reference to the modX object
*/
function __construct(modX &$modx) {
$this->modx =& $modx;
$this->name =& $this->_fields['name'];
$this->properties =& $this->_fields['properties'];
}
/**
* Generic getter method for modTag attributes.
*
* @see xPDOObject::get()
* @param string $k The field key.
* @return mixed The value of the field or null if it is not set.
*/
public function get($k) {
$value = null;
if (array_key_exists($k, $this->_fields)) {
if ($k == 'properties') {
$value = is_string($this->_fields[$k]) && !empty($this->_fields[$k])
? unserialize($this->_fields[$k])
: array();
} else {
$value = $this->_fields[$k];
}
}
return $value;
}
/**
* Generic setter method for modTag attributes.
*
* @see xPDOObject::set()
* @param string $k The field key.
* @param mixed $v The value to assign to the field.
*/
public function set($k, $v) {
if ($k == 'properties') {
$v = is_array($v) ? serialize($v) : $v;
}
$this->_fields[$k]= $v;
}
/**
* Cache the element into the elementCache by tag signature.
* @see modElement::cache()
*/
public function cache() {
if ($this->isCacheable()) {
$this->modx->elementCache[$this->_tag]= $this->_output;
}
}
/**
* Returns the current token for the tag
*
* @return string The token for the tag
*/
public function getToken() {
return $this->_token;
}
/**
* Setter method for the token class var.
*
* @param string $token The token to use for this element tag.
*/
public function setToken($token) {
$this->_token = $token;
}
/**
* Setter method for the tag class var.
*
* @param string $tag The tag to use for this element.
*/
public function setTag($tag) {
$this->_tag = $tag;
}
/**
* Gets a tag representation of the modTag instance.
*
* @return string
*/
public function getTag() {
if (empty($this->_tag) && ($name = $this->get('name'))) {
$propTemp = array();
if (empty($this->_propertyString) && !empty($this->_properties)) {
foreach ($this->_properties as $key => $value) {
$propTemp[] = trim($key) . '=`' . $value . '`';
}
if (!empty($propTemp)) {
$this->_propertyString = '?' . implode('&', $propTemp);
}
}
$tag = '[[';
$tag.= $this->getToken();
$tag.= $name;
if (!empty($this->_propertyString)) {
$tag.= $this->_propertyString;
}
$tag.= ']]';
$this->_tag = $tag;
}
if (empty($this->_tag)) {
$this->modx->log(xPDO::LOG_LEVEL_ERROR, 'Instance of ' . get_class($this) . ' produced an empty tag!');
}
return $this->_tag;
}
/**
* Process the tag and return the result.
*
* @see modElement::process()
* @param array|string $properties An array of properties or a formatted
* property string.
* @param string $content Optional content to use for the element
* processing.
* @return mixed The result of processing the tag.
*/
public function process($properties= null, $content= null) {
$this->modx->getParser();
$this->modx->parser->setProcessingElement(true);
$this->getProperties($properties);
$this->getTag();
$this->filterInput();
if ($this->modx->getDebug() === true) $this->modx->log(xPDO::LOG_LEVEL_DEBUG, "Processing Element: " . $this->get('name') . ($this->_tag ? "\nTag: {$this->_tag}" : "\n") . "\nProperties: " . print_r($this->_properties, true));
if ($this->isCacheable() && isset ($this->modx->elementCache[$this->_tag])) {
$this->_output = $this->modx->elementCache[$this->_tag];
$this->_processed = true;
} else {
$this->getContent(is_string($content) ? array('content' => $content) : array());
}
return $this->_result;
}
/**
* Get an input filter instance configured for this Element.
*
* @return modInputFilter|null An input filter instance (or null if one cannot be loaded).
*/
public function & getInputFilter() {
if (!isset ($this->_filters['input']) || !($this->_filters['input'] instanceof modInputFilter)) {
if (!$inputFilterClass= $this->get('input_filter')) {
$inputFilterClass = $this->modx->getOption('input_filter',null,'filters.modInputFilter');
}
if ($filterClass= $this->modx->loadClass($inputFilterClass, '', false, true)) {
if ($filter= new $filterClass($this->modx)) {
$this->_filters['input']= $filter;
}
}
}
return $this->_filters['input'];
}
/**
* Get an output filter instance configured for this Element.
*
* @return modOutputFilter|null An output filter instance (or null if one cannot be loaded).
*/
public function & getOutputFilter() {
if (!isset ($this->_filters['output']) || !($this->_filters['output'] instanceof modOutputFilter)) {
if (!$outputFilterClass= $this->get('output_filter')) {
$outputFilterClass = $this->modx->getOption('output_filter',null,'filters.modOutputFilter');
}
if ($filterClass= $this->modx->loadClass($outputFilterClass, '', false, true)) {
if ($filter= new $filterClass($this->modx)) {
$this->_filters['output']= $filter;
}
}
}
return $this->_filters['output'];
}
/**
* Apply an input filter to a tag.
*
* This is called by default in {@link modTag::process()} after the tag
* properties have been parsed.
*
* @see modElement::filterInput()
*/
public function filterInput() {
$filter = $this->getInputFilter();
if ($filter !== null && $filter instanceof modInputFilter) {
$filter->filter($this);
}
}
/**
* Apply an output filter to a tag.
*
* Call this method in your {modTag::process()} implementation when it is
* appropriate, typically once all processing has been completed, but before
* any caching takes place.
*
* @see modElement::filterOutput()
*/
public function filterOutput() {
$filter = $this->getOutputFilter();
if ($filter !== null && $filter instanceof modOutputFilter) {
$filter->filter($this);
}
}
/**
* Get the raw source content of the tag element.
*
* @param array $options An array of options implementations can use to
* accept language, revision identifiers, or other information to alter the
* behavior of the method.
* @return string The raw source content for the element.
*/
public function getContent(array $options = array()) {
if (!$this->isCacheable() || !is_string($this->_content) || $this->_content === '') {
if (isset($options['content'])) {
$this->_content = $options['content'];
} else {
$this->_content = $this->get('name');
}
}
return $this->_content;
}
/**
* Set the raw source content for the tag element.
*
* @param string $content The content to set
* @param array $options Ignored.
* @return boolean
*/
public function setContent($content, array $options = array()) {
return $this->set('name', $content);
}
/**
* Get the properties for this element instance for processing.
*
* @param array|string $properties An array or string of properties to apply.
* @return array A simple array of properties ready to use for processing.
*/
public function getProperties($properties = null) {
$this->_properties= $this->modx->parser->parseProperties($this->get('properties'));
$set= $this->getPropertySet();
if (!empty($set)) {
$this->_properties= array_merge($this->_properties, $set);
}
if (!empty($properties)) {
$this->_properties= array_merge($this->_properties, $this->modx->parser->parseProperties($properties));
}
return $this->_properties;
}
/**
* Set default properties for this element instance.
*
* @param array|string $properties A property array or property string.
* @param boolean $merge Indicates if properties should be merged with
* existing ones.
* @return boolean true if the properties are set.
*/
public function setProperties($properties, $merge = false) {
$set = false;
$propertyArray = array();
if (is_string($properties)) {
$properties = $this->modx->parser->parsePropertyString($properties);
}
if (is_array($properties)) {
foreach ($properties as $propKey => $property) {
if (is_array($property) && isset($property[5])) {
$propertyArray[$property[0]] = array(
'name' => $property[0],
'desc' => $property[1],
'type' => $property[2],
'options' => $property[3],
'value' => $property[4],
);
} elseif (is_array($property) && isset($property['value'])) {
$propertyArray[$property['name']] = array(
'name' => $property['name'],
'desc' => isset($property['description']) ? $property['description'] : (isset($property['desc']) ? $property['desc'] : ''),
'type' => isset($property['xtype']) ? $property['xtype'] : (isset($property['type']) ? $property['type'] : 'textfield'),
'options' => isset($property['options']) ? $property['options'] : array(),
'value' => $property['value'],
);
} else {
$propertyArray[$propKey] = array(
'name' => $propKey,
'desc' => '',
'type' => 'textfield',
'options' => array(),
'value' => $property
);
}
}
if ($merge && !empty($propertyArray)) {
$existing = $this->get('properties');
if (is_array($existing) && !empty($existing)) {
$propertyArray = array_merge($existing, $propertyArray);
}
}
$set = $this->set('properties', $propertyArray);
}
return $set;
}
/**
* Indicates if the element is cacheable.
*
* @return boolean True if the element can be stored to or retrieved from
* the element cache.
*/
public function isCacheable() {
return $this->_cacheable;
}
/**
* Sets the runtime cacheability of the element.
*
* @param boolean $cacheable Indicates the value to set for cacheability of
* this element.
*/
public function setCacheable($cacheable = true) {
$this->_cacheable = (boolean) $cacheable;
}
/**
* Gets a named property set to use with this modTag instance.
*
* This function will attempt to extract a setName from the tag name using the
* @ symbol to delimit the name of the property set. If a setName parameter is provided,
* the function will override any property set specified in the name by merging both
* property sets.
*
* Here is an example of an tag using the @ modifier to specify a property set name:
* [[~TagName@PropertySetName:FilterCommand=`FilterModifier`?
* &PropertyKey1=`PropertyValue1`
* &PropertyKey2=`PropertyValue2`
* ]]
*
* @param string|null $setName An explicit property set name to search for.
* @return array|null An array of properties or null if no set is found.
*/
public function getPropertySet($setName = null) {
$propertySet= null;
$name = $this->get('name');
if (strpos($name, '@') !== false) {
$psName= '';
$split= xPDO :: escSplit('@', $name);
if ($split && isset($split[1])) {
$name= $split[0];
$psName= $split[1];
$filters= xPDO :: escSplit(':', $setName);
if ($filters && isset($filters[1]) && !empty($filters[1])) {
$psName= $filters[0];
$name.= ':' . $filters[1];
}
$this->set('name', $name);
}
if (!empty($psName)) {
$psObj= $this->modx->getObject('modPropertySet', array('name' => $psName));
if ($psObj) {
$propertySet= $this->modx->parser->parseProperties($psObj->get('properties'));
}
}
}
if (!empty($setName)) {
$propertySetObj= $this->modx->getObject('modPropertySet', array('name' => $setName));
if ($propertySetObj) {
if (is_array($propertySet)) {
$propertySet= array_merge($propertySet, $this->modx->parser->parseProperties($propertySetObj->get('properties')));
} else {
$propertySet= $this->modx->parser->parseProperties($propertySetObj->get('properties'));
}
}
}
return $propertySet;
}
}
/**
* Tag representing a modResource field from the current MODX resource.
*
* [[*content]] Represents the content field from modResource.
*
* @uses modX::$resource The modResource instance being processed by modX.
* @package modx
*/
class modFieldTag extends modTag {
/**
* Overrides modTag::__construct to set the Field Tag token
* {@inheritdoc}
*/
function __construct(modX & $modx) {
parent :: __construct($modx);
$this->setToken('*');
}
/**
* Process the modFieldTag and return the output.
*
* {@inheritdoc}
*/
public function process($properties= null, $content= null) {
if ($this->get('name') === 'content') $this->setCacheable(false);
parent :: process($properties, $content);
if (!$this->_processed) {
$this->_output= $this->_content;
if (is_string($this->_output) && !empty ($this->_output)) {
/* collect element tags in the content and process them */
$maxIterations= intval($this->modx->getOption('parser_max_iterations',null,10));
$this->modx->parser->processElementTags(
$this->_tag,
$this->_output,
$this->modx->parser->isProcessingUncacheable(),
$this->modx->parser->isRemovingUnprocessed(),
'[[',
']]',
array(),
$maxIterations
);
}
$this->filterOutput();
$this->cache();
$this->_processed= true;
}
/* finally, return the processed element content */
return $this->_output;
}
/**
* Get the raw source content of the field.
*
* {@inheritdoc}
*/
public function getContent(array $options = array()) {
if (!$this->isCacheable() || !is_string($this->_content) || $this->_content === '') {
if (isset($options['content']) && !empty($options['content'])) {
$this->_content = $options['content'];
} elseif ($this->modx->resource instanceof modResource) {
if ($this->get('name') == 'content') {
$this->_content = $this->modx->resource->getContent($options);
} else {
$this->_content = $this->modx->resource->get($this->get('name'));
}
}
}
return $this->_content;
}
}
/**
* Represents placeholder tags.
*
* [[+placeholder_key]] Represents a placeholder with name placeholder_key.
*
* @uses modX::getPlaceholder() To retrieve the placeholder value.
* @package modx
*/
class modPlaceholderTag extends modTag {
/**
* Overrides modTag::__construct to set the Placeholder Tag token
* {@inheritdoc}
*/
function __construct(modX & $modx) {
parent :: __construct($modx);
$this->setCacheable(false);
$this->setToken('+');
}
/**
* Processes the modPlaceholderTag, recursively processing nested tags.
*
* Tags in the properties of the tag itself, or the content returned by the
* tag element are processed. Non-cacheable nested tags are only processed
* if this tag element is also non-cacheable.
*
* {@inheritdoc}
*/
public function process($properties= null, $content= null) {
parent :: process($properties, $content);
if (!$this->_processed) {
$this->_output= $this->_content;
if ($this->_output !== null && is_string($this->_output) && !empty($this->_output)) {
/* collect element tags in the content and process them */
$maxIterations= intval($this->modx->getOption('parser_max_iterations',null,10));
$this->modx->parser->processElementTags(
$this->_tag,
$this->_output,
$this->modx->parser->isProcessingUncacheable(),
$this->modx->parser->isRemovingUnprocessed(),
'[[',
']]',
array(),
$maxIterations
);
}
if ($this->_output !== null || $this->modx->parser->startedProcessingUncacheable()) {
$this->filterOutput();
$this->_processed = true;
}
}
/* finally, return the processed element content */
return $this->_output;
}
/**
* Get the raw source content of the field.
*
* {@inheritdoc}
*/
public function getContent(array $options = array()) {
if (!is_string($this->_content)) {
if (isset($options['content'])) {
$this->_content = $options['content'];
} else {
$this->_content = $this->modx->getPlaceholder($this->get('name'));
}
}
return $this->_content;
}
/**
* modPlaceholderTag instances cannot be cacheable.
*
* @return boolean Always returns false.
*/
public function isCacheable() {
return false;
}
/**
* modPlaceholderTag instances cannot be cacheable.
*
* {@inheritdoc}
*/
public function setCacheable($cacheable = true) {}
}
/**
* Represents link tags.
*
* [[~12]] Creates a URL from the specified resource identifier.
*
* @package modx
*/
class modLinkTag extends modTag {
/**
* Overrides modTag::__construct to set the Link Tag token
* {@inheritdoc}
*/
function __constructor(modX & $modx) {
parent :: __construct($modx);
$this->setToken('~');
}
/**
* Processes the modLinkTag, recursively processing nested tags.
*
* {@inheritdoc}
*/
public function process($properties= null, $content= null) {
parent :: process($properties, $content);
if (!$this->_processed) {
$this->_output= $this->_content;
if (is_string($this->_output) && !empty ($this->_output)) {
/* collect element tags in the content and process them */
$maxIterations= intval($this->modx->getOption('parser_max_iterations',null,10));
$this->modx->parser->processElementTags(
$this->_tag,
$this->_output,
$this->modx->parser->isProcessingUncacheable(),
$this->modx->parser->isRemovingUnprocessed(),
'[[',
']]',
array(),
$maxIterations
);
$context = '';
if ($this->modx->getOption('friendly_urls', null, false)) {
if (array_key_exists('context', $this->_properties)) {
$context = $this->_properties['context'];
}
if ($context) {
$resource = $this->modx->findResource($this->_output, $context);
if ($resource) {
$this->_output = $resource;
}
}
}
if (!empty($this->_output)) {
$qs = '';
$scheme = $this->modx->getOption('link_tag_scheme',null,-1);
$options = array();
if (is_array($this->_properties) && !empty($this->_properties)) {
$qs = array();
if (array_key_exists('context', $this->_properties)) {
$context = $this->_properties['context'];
unset($this->_properties['context']);
}
if (array_key_exists('scheme', $this->_properties)) {
$scheme = $this->_properties['scheme'];
unset($this->_properties['scheme']);
if (is_numeric($scheme)) $scheme = (integer) $scheme;
}
if (array_key_exists('use_weblink_target', $this->_properties)) {
$options['use_weblink_target'] = $this->_properties['use_weblink_target'];
unset($this->_properties['use_weblink_target']);
}
foreach ($this->_properties as $propertyKey => $propertyValue) {
if (in_array($propertyKey, array('context', 'scheme', 'use_weblink_target'))) continue;
$qs[]= "{$propertyKey}={$propertyValue}";
}
if ($qs= implode('&', $qs)) {
$qs= rawurlencode($qs);
$qs= str_replace(array('%26','%3D'),array('&','='),$qs);
}
}
$this->_output= $this->modx->makeUrl($this->_output, $context, $qs, $scheme, $options);
}
}
if (!empty($this->_output)) {
$this->filterOutput();
$this->cache();
$this->_processed= true;
}
if (empty($this->_output)) {
$this->modx->log(
modX::LOG_LEVEL_ERROR,
'Bad link tag `' . $this->_tag . '` encountered',
'',
$this->modx->resource
? "resource {$this->modx->resource->id}"
: ($_SERVER['REQUEST_URI'] ? "uri {$_SERVER['REQUEST_URI']}" : '')
);
}
}
/* finally, return the processed element content */
return $this->_output;
}
}
/**
* Represents Lexicon tags, for localized strings.
*
* [[%word_or_phase]] Returns the lexicon representation of 'word_or_phrase' for
* the currently loaded language.
*
* @package modx
*/
class modLexiconTag extends modTag {
/**
* Overrides modTag::__construct to set the Lexicon Tag token
* {@inheritdoc}
*/
function __construct(modX & $modx) {
parent :: __construct($modx);
$this->setToken('%');
}
/**
* Processes a modLexiconTag, recursively processing nested tags.
*
* {@inheritdoc}
*/
public function process($properties= null, $content= null) {
parent :: process($properties, $content);
if (!$this->_processed) {
$this->_output= $this->_content;
if (is_string($this->_output) && !empty ($this->_output)) {
/* collect element tags in the content and process them */
$maxIterations= intval($this->modx->getOption('parser_max_iterations',null,10));
$this->modx->parser->processElementTags(
$this->_tag,
$this->_output,
$this->modx->parser->isProcessingUncacheable(),
$this->modx->parser->isRemovingUnprocessed(),
'[[',
']]',
array(),
$maxIterations
);
}
$this->filterOutput();
$this->cache();
$this->_processed= true;
}
/* finally, return the processed element content */
return $this->_output;
}
/**
* Get the raw source content of the link.
*
* {@inheritdoc}
*/
public function getContent(array $options = array()) {
if (!is_string($this->_content) || $this->_content === '') {
if (isset($options['content'])) {
$this->_content = $options['content'];
} else {
if (!is_object($this->modx->lexicon)) {
$this->modx->getService('lexicon','modLexicon');
}
$topic = !empty($this->_properties['topic']) ? $this->_properties['topic'] : 'default';
$namespace = !empty($this->_properties['namespace']) ? $this->_properties['namespace'] : 'core';
$language = !empty($this->_properties['language']) ? $this->_properties['language'] : $this->modx->getOption('cultureKey',null,'en');
$this->modx->lexicon->load($language.':'.$namespace.':'.$topic);
$this->_content= $this->modx->lexicon($this->get('name'), $this->_properties,$language);
}
}
return $this->_content;
}
}