<?php
/**
* Base class for defining the formatting used by the BB code parser.
* This class implements HTML formatting.
*
* @package XenForo_BbCode
*/
class XenForo_BbCode_Formatter_Base
{
/**
* Lookup array that translates a smilie replacement text to an untypeable
* sentinel value (\0-id-\0).
*
* @var array Format: [smilie replacement text] => sentinel value
*/
protected $_smilieTranslate = array();
/**
* Essentially the reverse of the above lookup, this one translates a smilie ID
* to the actual "rich" replacement (for HTML, an image tag).
*
* @var array Format: [smilie id] => final replacement
*/
protected $_smilieReverse = array();
/**
* Array to store smilie paths for [IMG] lookup
*
* @var array Format [path] => smilie ID
*/
protected $_smiliePaths = array();
/**
* List of media sites that are known.
*
* @var array Format: [media site id] => info
*/
protected $_mediaSites = array();
/**
* List of tags this formatter knows about.
*
* @var array|****
*/
protected $_tags = ****;
/**
* View for rendering tags that require templates.
*
* @var XenForo_View|****
*/
protected $_view = ****;
/**
* String used for outputting [IMG] tags. Will be passed the following params:
* 1 URL
* 2 Additional CSS classes
*
* @var string
*/
protected $_imageTemplate = '<img src="%1$s" class="bbCodeImage%2$s" alt="[​IMG]" data-url="%3$s" />';
/**
* String used for outputting smilies. Will be passed the following params:
* 1 Image URL
* 2 Smilie text
* 3 Smilie title
*
* @var string
*/
protected $_smilieTemplate = '<img src="%1$s" class="mceSmilie" alt="%2$s" title="%3$s %2$s" />';
/**
* String used for outputting smilies as sprites. Will be passed the following params:
* 1 Smilie ID
* 2 Smilie text
* 3 Smilie title
*
* @var string
*/
protected $_smilieSpriteTemplate = '<img src="styles/default/xenforo/clear.png" class="mceSmilieSprite mceSmilie%1$d" alt="%2$s" title="%3$s %2$s" />';
/**
* Cache to store processed smilie URLs, to avoid having to process them for every single smilie.
*
* @var array
*/
protected $_smilieUrlCache = array();
/**
* List of ignored users, for quoted content mostly.
*
* @var array Key: user ID, value: user name
*/
protected $_ignoredUsers = array();
/**
* Direction of the text on the page by default.
*
* @var string LTR or RTL
*/
protected $_textDirection = 'LTR';
/**
* Maximum depth of tags that will be parsed. 0 to disable.
*
* @var int
*/
protected $_tagDepthLimit = 20;
/**
* Constructor.
*/
public function __construct()
{
$this->_tags = $this->getTags();
$this->preLoadData();
if (XenForo_Visitor::hasInstance())
{
$visitor = XenForo_Visitor::getInstance();
if (!empty($visitor['ignoredUsers']))
{
$this->_ignoredUsers = $visitor['ignoredUsers'];
}
$language = $visitor->getLanguage();
$this->_textDirection = $language['text_direction'];
}
}
/**
* Pre-loads any required data, such as templates or phrases that may be be used.
*/
public function preLoadData()
{
}
/**
* Add the specified list of smilies to the list that will be processed.
*
* @param array $smilies List of smilies with data from the DB (smilie_id, smilieText [array], image_url)
*/
public function addSmilies(array $smilies)
{
foreach ($smilies AS $smilie)
{
foreach ($smilie['smilieText'] AS $text)
{
$this->_smilieTranslate[$text] = "\0" . $smilie['smilie_id'] . "\0";
}
if (empty($smilie['sprite_params']))
{
$this->_smilieReverse[$smilie['smilie_id']] = $this->_processSmilieTemplate($smilie);
}
else
{
$this->_smilieReverse[$smilie['smilie_id']] = $this->_processSmilieSpriteTemplate($smilie);
}
$this->_smiliePaths[$smilie['image_url']] = $smilie['smilie_id'];
}
}
/**
* Populates the image smilie template with data
*
* @param array $smilie
*
* @return string
*/
protected function _processSmilieTemplate(array $smilie)
{
return sprintf($this->_smilieTemplate,
$this->_prepareSmilieUrl($smilie['image_url']),
htmlspecialchars(reset($smilie['smilieText'])),
htmlspecialchars($smilie['title'])
);
}
/**
* Populates the sprite smilie template with data
*
* @param array $smilie
*
* @return string
*/
protected function _processSmilieSpriteTemplate(array $smilie)
{
return sprintf($this->_smilieSpriteTemplate,
$smilie['smilie_id'],
htmlspecialchars(reset($smilie['smilieText'])),
htmlspecialchars($smilie['title']),
$this->_prepareSmilieUrl($smilie['image_url']),
$smilie['sprite_params']['w'],
$smilie['sprite_params']['h'],
$smilie['sprite_params']['x'],
$smilie['sprite_params']['y']
);
}
/**
* Prepares a smilie URL for use in an <img /> tag. Fetches the result from cache if possible.
*
* @param string $smilieUrl
*
* @return string
*/
protected function _prepareSmilieUrl($smilieUrl)
{
if (!isset($this->_smilieUrlCache[$smilieUrl]))
{
$this->_smilieUrlCache[$smilieUrl] = $this->_prepareSmilieUrlInternal($smilieUrl);
}
return $this->_smilieUrlCache[$smilieUrl];
}
/**
* Prepares a smilie URL for use in an <img /> tag.
*
* @param string $smilieUrl
*
* @return string
*/
protected function _prepareSmilieUrlInternal($smilieUrl)
{
return htmlspecialchars($smilieUrl);
}
/**
* Adds to the list of acceptable media sites.
*
* @param array $sites
*/
public function addMediaSites(array $sites)
{
$this->_mediaSites = array_merge($this->_mediaSites, $sites);
}
/**
* Sets the view that is used to render tags requiring templates.
*
* @param XenForo_View $view
*/
public function setView(XenForo_View $view = ****)
{
$this->_view = $view;
if ($view)
{
$this->preLoadTemplates($view);
}
}
/**
* @return ****|XenForo_View
*/
public function getView()
{
return $this->_view;
}
/**
* Tells the view to pre-load the templates that are required.
*
* @param XenForo_View $view
*/
public function preLoadTemplates(XenForo_View $view)
{
$view->preLoadTemplate('bb_code_tag_code');
$view->preLoadTemplate('bb_code_tag_php');
$view->preLoadTemplate('bb_code_tag_html');
$view->preLoadTemplate('bb_code_tag_quote');
$view->preLoadTemplate('bb_code_tag_attach');
$view->preLoadTemplate('bb_code_tag_spoiler');
}
/**
* Get the list of parsable tags and their parsing rules.
*
* @return array
*/
public function getTags()
{
if ($this->_tags !== ****)
{
return $this->_tags;
}
return array(
'b' => array(
'hasOption' => false,
'replace' => array('<b>', '</b>')
),
'i' => array(
'hasOption' => false,
'replace' => array('<i>', '</i>')
),
'u' => array(
'hasOption' => false,
'replace' => array('<span style="text-decoration: underline">', '</span>')
),
's' => array(
'hasOption' => false,
'replace' => array('<span style="text-decoration: line-through">', '</span>')
),
'color' => array(
'hasOption' => true,
'optionRegex' => '/^(rgb\(\s*\d+%?\s*,\s*\d+%?\s*,\s*\d+%?\s*\)|#[a-f0-9]{6}|#[a-f0-9]{3}|[a-z]+)$/i',
'replace' => array('<span style="color: %s">', '</span>')
),
'font' => array(
'hasOption' => true,
'optionRegex' => '/^[a-z0-9 \-]+$/i', // regex matched to HTML->BB code regex
'replace' => array('<span style="font-family: \'%s\'">', '</span>')
),
'size' => array(
'hasOption' => true,
'optionRegex' => '/^[0-9]+(px)?$/i',
'callback' => array($this, 'renderTagSize'),
),
'left' => array(
'hasOption' => false,
'callback' => array($this, 'renderTagAlign'),
'trimLeadingLinesAfter' => 1,
),
'center' => array(
'hasOption' => false,
'callback' => array($this, 'renderTagAlign'),
'trimLeadingLinesAfter' => 1,
),
'right' => array(
'hasOption' => false,
'callback' => array($this, 'renderTagAlign'),
'trimLeadingLinesAfter' => 1,
),
'indent' => array(
'trimLeadingLinesAfter' => 1,
'optionRegex' => '/^[0-9]+$/',
'callback' => array($this, 'renderTagIndent')
),
'url' => array(
'parseCallback' => array($this, 'parseValidatePlainIfNoOption'),
'callback' => array($this, 'renderTagUrl'),
),
'email' => array(
'parseCallback' => array($this, 'parseValidatePlainIfNoOption'),
'callback' => array($this, 'renderTagEmail')
),
'img' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagImage')
),
'h1' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagH2')
),
'h2' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagH2')
),
'h3' => array(
'hasOption' => false,
'plainChildren' => true,
'callback' => array($this, 'renderTagH3')
),
'quote' => array(
'trimLeadingLinesAfter' => 2,
'callback' => array($this, 'renderTagQuote')
),
'code' => array(
'parseCallback' => array($this, 'parseValidateTagCode'),
'stopSmilies' => true,
'stopLineBreakConversion' => true,
'trimLeadingLinesAfter' => 2,
'callback' => array($this, 'renderTagCode')
),
'php' => array(
'hasOption' => false,
'plainChildren' => true,
'stopSmilies' => true,
'stopLineBreakConversion' => true,
'trimLeadingLinesAfter' => 2,
'callback' => array($this, 'renderTagPhp')
),
'html' => array(
'hasOption' => false,
'plainChildren' => true,
'stopSmilies' => true,
'stopLineBreakConversion' => true,
'trimLeadingLinesAfter' => 2,
'callback' => array($this, 'renderTagHtml')
),
'list' => array(
'trimLeadingLinesAfter' => 1,
'callback' => array($this, 'renderTagList')
),
'plain' => array(
'hasOption' => false,
'plainChildren' => true,
'stopSmilies' => true,
'replace' => array('', '')
),
'media' => array(
'hasOption' => true,
'plainChildren' => true,
'callback' => array($this, 'renderTagMedia')
),
'spoiler' => array(
'trimLeadingLinesAfter' => 1,
'callback' => array($this, 'renderTagSpoiler')
),
'attach' => array(
'plainChildren' => true,
'callback' => array($this, 'renderTagAttach')
),
'user' => array(
'hasOption' => true,
'stopSmilies' => true,
'callback' => array($this, 'renderTagUser')
)
);
}
public function addCustomTags(array $tags)
{
foreach ($tags AS $tagName => $tag)
{
$tagInfo = $this->_setupCustomTagInfo($tagName, $tag);
if ($tagInfo)
{
$this->_tags[$tagName] = $tagInfo;
}
}
}
protected function _setupCustomTagInfo($tagName, array $tag)
{
$output = array();
if ($tag['bb_code_mode'] == 'replace')
{
$output['replace'] = $tag['replace_html'];
}
else if ($tag['bb_code_mode'] == 'callback')
{
$output['callback'] = array($tag['callback_class'], $tag['callback_method']);
}
if ($tag['has_option'] == 'yes')
{
$output['hasOption'] = true;
}
else if ($tag['has_option'] == 'no')
{
$output['hasOption'] = false;
}
if (strlen($tag['option_regex']))
{
$output['optionRegex'] = $tag['option_regex'];
}
if ($tag['trim_lines_after'])
{
$output['trimLeadingLinesAfter'] = $tag['trim_lines_after'];
}
if ($tag['plain_children'])
{
$output['plainChildren'] = true;
}
if ($tag['disable_smilies'])
{
$output['stopSmilies'] = true;
}
if ($tag['disable_nl2br'])
{
$output['stopLineBreakConversion'] = true;
}
if ($tag['allow_empty'])
{
$output['keepEmpty'] = true;
}
$output['allowSignature'] = $tag['allow_signature'];
return $output;
}
/**
* Allows the text to be filtered before parsing.
*
* @param string $text
*
* @return string
*/
public function preFilterText($text)
{
return $text;
}
/**
* Resets rendering state and renders a parsed BB code tree
* to the required output format. Note that this initializes the default states,
* so it is likely not the correct function to call for child tags.
*
* @param array $tree Tree from {@link parse()}.
* @param array $extraStates A list of extra states to push into the formatter
*
* @return string Output text
*/
public function renderTree(array $tree, array $extraStates = array())
{
$rendererStates = $extraStates + array(
'stopSmilies' => 0,
'stopLineBreakConversion' => 0,
'tagDataStack' => array(),
'noFollowDefault' => true, // add nofollow attributes
'shortenUrl' => true, // add ... in middle of long URLs
'lightBox' => true, // add 'LbImage' class to [IMG] output
'imgToSmilie' => false, // attempt to convert [IMG] to smilie if URL matches
'disableProxying' => false, // disable image and link proxying
);
$output = $this->renderSubTree($tree, $rendererStates);
return $this->filterFinalOutput($output);
}
/**
* Renders a parsed BB code tree to the required output format. This does
* not reset the rendering states, meaning it is ok for recursive calls.
*
* @param array $tree Tree from {@link parse()}
* @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
*
* @return string Output text
*/
public function renderSubTree(array $tree, array $rendererStates)
{
$output = '';
$trimLeadingLines = 0;
foreach ($tree AS $element)
{
$output .= $this->renderTreeElement($element, $rendererStates, $trimLeadingLines);
}
return $output;
}
/**
* Renders a tree element, that be a tag (valid or not) or a string.
*
* @param array|string $element Tree element
* @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
* @param integer $trimLeadingLines By reference. Number of leading lines to strip off next element.
*
* @return string Rendered element.
*/
public function renderTreeElement($element, array $rendererStates, &$trimLeadingLines)
{
if (is_array($element))
{
return $this->renderTag($element, $rendererStates, $trimLeadingLines);
}
else
{
return $this->renderString($element, $rendererStates, $trimLeadingLines);
}
}
/**
* Renders a string tree element.
*
* @param string $string
* @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
* @param integer $trimLeadingLines By reference. Number of leading lines to strip off next element.
*
* @return string Rendered string
*/
public function renderString($string, array $rendererStates, &$trimLeadingLines)
{
if ($trimLeadingLines)
{
$string = $this->trimLeadingLines($string, $trimLeadingLines);
$trimLeadingLines = 0;
}
return $this->filterString($string, $rendererStates);
}
/**
* Trims the given number of leading blank lines off of the given string.
*
* @param string $string
* @param integer $amount
*
* @return string
*/
public function trimLeadingLines($string, $amount)
{
$amount = intval($amount);
if ($amount <= 0)
{
return $string;
}
return preg_replace('#^([ \t]*\r?\n){1,' . $amount . '}#i', '', $string);
}
/**
* Renders a tag. This tag may be valid or invalid.
*
* @param array $element Tag element.
* @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
* @param integer $trimLeadingLines By reference. Number of leading lines to strip from next element. May be modified by tag.
*
* @return string Rendered tag.
*/
public function renderTag(array $element, array $rendererStates, &$trimLeadingLines)
{
$trimLeadingLines = 0;
if (isset($rendererStates['tagDataStack']))
{
$rendererStates['tagDataStack'][] = $element;
}
$tagDepthReached = (
$this->_tagDepthLimit
&& !empty($rendererStates['tagDataStack'])
&& count($rendererStates['tagDataStack']) > $this->_tagDepthLimit
);
$tagInfo = $this->_getTagRule($element['tag']);
if (!$tagInfo || $tagDepthReached)
{
$output = $this->renderInvalidTag($element, $rendererStates);
}
else
{
if (!empty($tagInfo['stopSmilies']))
{
$rendererStates['stopSmilies']++;
}
if (!empty($tagInfo['stopLineBreakConversion']))
{
$rendererStates['stopLineBreakConversion']++;
}
$output = $this->renderValidTag($tagInfo, $element, $rendererStates);
if (!empty($tagInfo['trimLeadingLinesAfter']))
{
$trimLeadingLines = $tagInfo['trimLeadingLinesAfter'];
}
}
return $output;
}
/**
* Gets information about the specified tag.
*
* @param string $tagName
*
* @return array|false
*/
protected function _getTagRule($tagName)
{
$tagName = strtolower($tagName);
if (!empty($this->_tags[$tagName]) && is_array($this->_tags[$tagName]))
{
return $this->_tags[$tagName];
}
else
{
return false;
}
}
/**
* Renders an invalid tag. This tag is simply displayed in its original form.
*
* @param array $tag Tag data from tree
* @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
*
* @return string Rendered version
*/
public function renderInvalidTag(array $tag, array $rendererStates)
{
return $this->renderTagUnparsed($tag, $rendererStates);
}
/**
* Renders a tag as if it's unparsed (in its original form).
*
* @param array $tag Tag data from tree
* @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
*
* @return string Rendered version
*/
public function renderTagUnparsed(array $tag, array $rendererStates)
{
if (!empty($tag['original']) && is_array($tag['original']))
{
list($prepend, $append) = $tag['original'];
}
else
{
$prepend = '';
$append = '';
}
$output = $this->filterString($prepend, $rendererStates)
. $this->renderSubTree($tag['children'], $rendererStates)
. $this->filterString($append, $rendererStates);
return $output;
}
/**
* Renders a tag.
*
* @param array $tagInfo Information about how to parse the tag
* @param array $tag Tag data from tree
* @param array $rendererStates Renderer states to push down. Except in specific cases, cannot be pushed up.
*
* @return string Rendered version
*/
public function renderValidTag(array $tagInfo, array $tag, array $rendererStates)
{
if (!empty($tagInfo['callback']))
{
if (is_array($tagInfo['callback']) && $tagInfo['callback'][0] == '$this')
{
$tagInfo['callback'][0] = $this;
}
if (!is_callable($tagInfo['callback']))
{
return $this->renderInvalidTag($tag, $rendererStates);
}
return call_user_func($tagInfo['callback'], $tag, $rendererStates, $this);
}
else if (!empty($tagInfo['replace']))
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
$option = $this->filterString($tag['option'], array_merge($rendererStates, array(
'stopSmilies' => true,
'stopLineBreakConversion' => true
)));
if (empty($tagInfo['keepEmpty']) && trim($text) === '')
{
return $text;
}
if (is_array($tagInfo['replace']))
{
list($prepend, $append) = $tagInfo['replace'];
return $this->_wrapInHtml($prepend, $append, $text, $option);
}
else
{
return strtr($tagInfo['replace'], array(
'{text}' => $text,
'{option}' => $option
));
}
}
else
{
return $this->renderInvalidTag($tag, $rendererStates);
}
}
protected function _wrapInHtml($prepend, $append, $text, $option = ****)
{
if ($option === ****)
{
return $prepend . $text . $append;
}
else
{
return sprintf($prepend, $option) . $text . sprintf($append, $option);
}
}
/**
* Similar to rendering the tree, but this function renders all tags to plain text
* (as if they weren't special tags). This can be useful for functions that can only
* take plain text children.
*
* Note that this output is not escaped in anyway!
*
* @param array $tree Tree or sub-tree to stringify
*
* @return string Tree as a string (like the original input)
*/
public function stringifyTree(array $tree)
{
$output = '';
foreach ($tree AS $element)
{
if (is_array($element))
{
if (!empty($element['original']) && is_array($element['original']))
{
list($prepend, $append) = $element['original'];
}
else
{
$prepend = '';
$append = '';
}
$output .= $prepend . $this->stringifyTree($element['children']) . $append;
}
else
{
$output .= strval($element);
}
}
return $output;
}
/**
* Filter a string for the current output format. A string is simply the text
* between tags. This function is responsible for things like word wrap and smilies
* and output escaping.
*
* @param string $string
* @param array $rendererStates List of states the renderer may be in
*
* @return string Filtered/escaped string
*/
public function filterString($string, array $rendererStates)
{
$string = XenForo_Helper_String::censorString($string);
if (empty($rendererStates['stopSmilies']))
{
$string = $this->replaceSmiliesInText($string, 'htmlspecialchars');
}
else
{
$string = htmlspecialchars($string);
}
if (empty($rendererStates['stopLineBreakConversion']))
{
$string = nl2br($string);
}
return $string;
}
/**
* Filters the final string output.
*
* @param string $output
*
* @return string
*/
public function filterFinalOutput($output)
{
return trim($output);
}
/**
* Gets a valid, full URL if possible. False is returned if not possible.
*
* @param string $url URL to validate
*
* @return string|false
*/
protected function _getValidUrl($url)
{
$url = trim($url);
if (!$url)
{
return false;
}
switch ($url[0])
{
case '#':
case '/':
case ' ':
case "\r":
case "\n":
return false;
}
if (preg_match('/\r?\n/', $url))
{
return false;
}
if (preg_match('#^https?://#i', $url))
{
return $url;
}
return 'http://' . $url;
}
/**
* Renders an indent tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagIndent(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
if (trim($text) === '')
{
$text = '<br />';
}
if (isset($tag['option']))
{
$amount = intval($tag['option']);
if ($amount > 10)
{
$amount = 10;
}
}
else
{
$amount = 1;
}
$invisibleSpace = $this->_endsInBlockTag($text) ? '' : '​';
if ($amount < 1)
{
return $this->_wrapInHtml('<div>', $invisibleSpace . '</div>', $text);
}
else
{
$paddingSide = ($this->_textDirection == 'RTL' ? 'padding-right' : 'padding-left');
return $this->_wrapInHtml('<div style="' . $paddingSide . ': ' . (30 * $amount) . 'px">', $invisibleSpace . '</div>', $text);
}
}
/**
* Renders an alignment (left, center, right) tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagAlign(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
$invisibleSpace = $this->_endsInBlockTag($text) ? '' : '​';
switch (strtolower($tag['tag']))
{
case 'left':
case 'center':
case 'right':
return $this->_wrapInHtml('<div style="text-align: ' . $tag['tag'] . '">', $invisibleSpace. '</div>', $text);
default:
return $this->_wrapInHtml('<div>', $invisibleSpace. '</div>', $text);
}
}
protected function _endsInBlockTag($text)
{
return preg_match('#</(p|div)>$#i', substr(rtrim($text), -6));
}
/**
* Renders a size tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagSize(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
if (trim($text) === '')
{
return $text;
}
$size = $this->getTextSize($tag['option']);
if ($size)
{
return $this->_wrapInHtml('<span style="font-size: ' . htmlspecialchars($size) . '">', '</span>', $text);
}
else
{
return $text;
}
}
/**
* Gets the effective text size to use.
*
* @param string $inputSize
*
* @return string|false
*/
public function getTextSize($inputSize)
{
if (strval(intval($inputSize)) == strval($inputSize))
{
// int only, translate size
if ($inputSize <= 0)
{
$size = false;
}
else
{
switch ($inputSize)
{
case 1: $size = '9px'; break;
case 2: $size = '10px'; break;
case 3: $size = '12px'; break;
case 4: $size = '15px'; break;
case 5: $size = '18px'; break;
case 6: $size = '22px'; break;
case 7:
default:
$size = '26px';
}
}
}
else
{
// int and unit
if (preg_match('/^([0-9]+)px$/i', $inputSize, $match))
{
if ($match[1] < 8)
{
$size = '8px';
}
else if ($match[1] > 36)
{
$size = '36px';
}
else
{
$size = $inputSize;
}
}
else
{
$size = false;
}
}
return $size;
}
/**
* Disables parsing of child tags if this tag does not have an option.
* Useful for tags like url/email, where the address may be in the body
* of the tag.
*
* @param array $tagInfo Info about the tag we're parsing.
* @param string|**** $tagOption Any option passed into the tag
*
* @return array|boolean True if tag is ok as is, array to change states, false to reject tag
*/
public function parseValidatePlainIfNoOption(array $tagInfo, $tagOption)
{
if (empty($tagOption))
{
return array('plainChildren' => true);
}
else
{
return true;
}
}
/**
* Renders a URL tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagUrl(array $tag, array $rendererStates)
{
if (!empty($tag['option']))
{
$url = $tag['option'];
$text = $this->renderSubTree($tag['children'], $rendererStates);
}
else
{
$url = $this->stringifyTree($tag['children']);
$text = rawurldecode($url);
if (!preg_match('/./u', $text))
{
$text = $url;
}
$text = XenForo_Helper_String::censorString($text);
if (!empty($rendererStates['shortenUrl']))
{
$length = utf8_strlen($text);
if ($length > 100)
{
$text = utf8_substr_replace($text, '...', 35, $length - 35 - 45);
}
}
$text = htmlspecialchars($text);
}
$url = $this->_getValidUrl($url);
if (!$url)
{
return $text;
}
else
{
list($class, $target, $type) = XenForo_Helper_String::getLinkClassTarget($url);
if ($type == 'internal')
{
$noFollow = '';
}
else
{
$noFollow = (empty($rendererStates['noFollowDefault']) ? '' : ' rel="nofollow"');
}
$href = XenForo_Helper_String::censorString($url);
if ($rendererStates['disableProxying'])
{
$proxyHref = false;
}
else
{
$proxyHref = $this->_handleLinkProxyOption($href, $type);
}
$proxyAttr = '';
if ($proxyHref)
{
$proxyAttr = ' data-proxy-href="' . htmlspecialchars($proxyHref) . '"';
$class .= ' ProxyLink';
}
$class = $class ? " class=\"$class\"" : '';
$target = $target ? " target=\"$target\"" : '';
return $this->_wrapInHtml(
'<a href="' . htmlspecialchars($href) . '"' . $target . $class . $proxyAttr . $noFollow . '>',
'</a>',
$text
);
}
}
/**
* Renders an email tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagEmail(array $tag, array $rendererStates)
{
if (!empty($tag['option']))
{
$email = $tag['option'];
$text = $this->renderSubTree($tag['children'], $rendererStates);
}
else
{
$email = $this->stringifyTree($tag['children'], $rendererStates);
$text = $this->filterString($email, $rendererStates);
}
if (strpos($email, '@') === false)
{
// invalid URL, ignore
return $text;
}
$email = XenForo_Helper_String::censorString($email);
return $this->_wrapInHtml('<a href="mailto:' . htmlspecialchars($email) . '">', '</a>', $text);
}
/**
* Renders a img tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagImage(array $tag, array $rendererStates)
{
$url = $this->stringifyTree($tag['children']);
$validUrl = $this->_getValidUrl($url);
if (!$validUrl)
{
return $this->filterString($url, $rendererStates);
}
$censored = XenForo_Helper_String::censorString($validUrl);
if ($censored != $validUrl)
{
return $this->filterString($url, $rendererStates);
}
// attempts to convert smilies posted as [IMG] tags back into smilies
if ($rendererStates['imgToSmilie'])
{
foreach ($this->_smiliePaths AS $smiliePath => $smilieId)
{
if (strpos($url, $smiliePath) !== false && substr($url, strlen($smiliePath) * -1) == $smiliePath)
{
return $this->_smilieReverse[$smilieId];
}
}
}
if ($rendererStates['disableProxying'])
{
$imageUrl = $validUrl;
}
else
{
$imageUrl = $this->_handleImageProxyOption($validUrl, $rendererStates);
}
return sprintf($this->_imageTemplate,
htmlspecialchars($imageUrl),
$rendererStates['lightBox'] ? ' LbImage' : '',
htmlspecialchars($validUrl)
);
}
/**
* Pass an image URL to the image proxy system if appropriate
*
* @param $url
*
* @return string
*/
protected function _handleImageProxyOption($url)
{
list($class, $target, $type, $schemeMatch) = XenForo_Helper_String::getLinkClassTarget($url);
if (($type == 'external' || !$schemeMatch))
{
$options = XenForo_Application::getOptions();
if (!empty($options->imageLinkProxy['images']))
{
$url = $this->_generateProxyLink('image', $url);
}
}
return $url;
}
/**
* Pass a link URL to the proxy / redirect system if appropriate
*
* @param string $url
* @param string $linkType
*
* @return string|false
*/
protected function _handleLinkProxyOption($url, $linkType)
{
if ($linkType == 'external')
{
$options = XenForo_Application::getOptions();
if (!empty($options->imageLinkProxy['links']))
{
return $this->_generateProxyLink('link', $url);
}
}
return false;
}
protected function _generateProxyLink($proxyType, $url)
{
$hash = hash_hmac('md5', $url,
XenForo_Application::getConfig()->globalSalt . XenForo_Application::getOptions()->imageLinkProxyKey
);
return 'proxy.php?' . $proxyType . '=' . urlencode($url) . '&hash=' . $hash;
}
/**
* Modifies the parsing options for a code tag. Users must explicitly
* opt in to allow BB codes to be used within.
*
* @param array $tagInfo Info about the tag we're parsing.
* @param string|**** $tagOption Any option passed into the tag
*
* @return array|boolean True if tag is ok as is, array to change states, false to reject tag
*/
public function parseValidateTagCode(array $tagInfo, $tagOption)
{
if (strtolower($tagOption) == 'rich')
{
return true;
}
else
{
return array('plainChildren' => true);
}
}
/**
* Renders a code tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagCode(array $tag, array $rendererStates)
{
switch (strtolower(strval($tag['option'])))
{
case 'php':
return $this->renderTagPhp($tag, $rendererStates);
case 'html':
return $this->renderTagHtml($tag, $rendererStates);
}
$content = $this->renderSubTree($tag['children'], $rendererStates);
if ($this->_view)
{
$template = $this->_view->createTemplateObject('bb_code_tag_code', array(
'content' => $content
));
return $template->render();
}
else
{
return $this->_wrapInHtml('<pre style="margin: 1em auto" title="Code">', '</pre>', $content);
}
}
/**
* Renders an HTML tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagHtml(array $tag, array $rendererStates)
{
$content = $this->stringifyTree($tag['children']);
$content = $this->filterString($content, $rendererStates);
if ($this->_view)
{
$template = $this->_view->createTemplateObject('bb_code_tag_html', array(
'content' => $content
));
return $template->render();
}
else
{
return $this->_wrapInHtml('<pre style="margin: 1em auto" title="HTML">', '</pre>', $content);
}
}
/**
* Renders a PHP tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagPhp(array $tag, array $rendererStates)
{
$content = $this->stringifyTree($tag['children']);
$content = XenForo_Helper_String::censorString($content);
$content = preg_replace('/^[ \t]*\r?\n/', '', $content);
if (strpos($content, '<?') == false)
{
$tagAdded = true;
$content = "<?php\n$content";
}
else
{
$tagAdded = false;
}
$content = highlight_string($content, true);
if ($tagAdded)
{
$content = preg_replace(
'#<\?php<br\s*/?>#',
'',
$content,
1
);
}
if ($this->_view)
{
$template = $this->_view->createTemplateObject('bb_code_tag_php', array(
'content' => $content
));
return $template->render();
}
else
{
return $this->_wrapInHtml('<div style="margin: 1em auto" title="PHP">', '</div>', $content);
}
}
/**
* Renders a quote tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagQuote(array $tag, array $rendererStates)
{
$keys = array_keys($tag['children']);
if (!$keys)
{
return '';
}
$first = reset($keys);
$last = end($keys);
if (is_string($tag['children'][$first]))
{
$tag['children'][$first] = ltrim($tag['children'][$first]);
}
if (is_string($tag['children'][$last]))
{
$tag['children'][$last] = rtrim($tag['children'][$last]);
}
$content = $this->renderSubTree($tag['children'], $rendererStates);
if ($content === '')
{
return '';
}
$source = false;
$attributes = array();
/*
* NOTE: changes to this code must also be reflected in
* XenForo_Model_Post::alertQuotedMembers()
*/
if ($tag['option'])
{
$parts = explode(',', $tag['option']);
$name = $this->filterString(array_shift($parts),
array_merge($rendererStates, array(
'stopSmilies' => true,
'stopLineBreakConversion' => true
))
);
foreach ($parts AS $part)
{
$partAttributes = explode(':', $part, 2);
if (isset($partAttributes[1]))
{
$attrName = trim($partAttributes[0]);
$attrValue = trim($partAttributes[1]);
if ($attrName !== '' && $attrValue !== '')
{
$attributes[$attrName] = $attrValue;
}
}
}
list($firstName, $firstValue) = each($attributes);
if ($firstName && $firstName != 'member')
{
$source = array('type' => $firstName, 'id' => intval($firstValue));
}
}
else
{
$name = false;
}
if ($this->_view)
{
$template = $this->_view->createTemplateObject('bb_code_tag_quote', array(
'content' => $content,
'nameHtml' => $name,
'source' => $source,
'attributes' => $attributes,
'ignored' => (isset($attributes['member']) && isset($this->_ignoredUsers[intval($attributes['member'])]))
));
return $template->render();
}
else
{
return $this->_renderTagQuoteFallback($name, $content);
}
}
/**
* Returns HTML output for a quote tag when the view is not available
*
* @param string $name Name of quoted user
* @param string $content Quoted text
*
* @return string
*/
protected function _renderTagQuoteFallback($name, $content)
{
if ($name)
{
$name = '<div>' . $name . '</div>';
}
return $this->_wrapInHtml('<blockquote>', '</blockquote>', $name . $content);
}
/**
* Renders a list tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagList(array $tag, array $rendererStates)
{
$listType = ($tag['option'] == '1' ? 'ol' : 'ul');
$elements = array();
$lastElement = '';
$trimLeadingLines = 0;
foreach ($tag['children'] AS $child)
{
if (is_array($child))
{
$childText = $this->renderTag($child, $rendererStates, $trimLeadingLines);
if (preg_match('#^<(ul|ol)#', $childText))
{
$lastElement = rtrim($lastElement);
if (substr($lastElement, -6) == '<br />')
{
$lastElement = substr($lastElement, 0, -6);
}
}
$lastElement .= $childText;
}
else
{
if (strpos($child, '[*]') !== false)
{
$parts = explode('[*]', $child);
$beforeFirst = array_shift($parts);
if ($lastElement !== '' || trim($beforeFirst) !== '')
{
$lastElement .= $this->renderString($beforeFirst, $rendererStates, $trimLeadingLines);
}
foreach ($parts AS $part)
{
$this->_appendListElement($elements, $lastElement);
$lastElement = $this->renderString($part, $rendererStates, $trimLeadingLines);
}
}
else
{
$lastElement .= $this->renderString($child, $rendererStates, $trimLeadingLines);
}
}
}
$this->_appendListElement($elements, $lastElement);
if (!$elements)
{
return '';
}
return $this->_renderListOutput($listType, $elements);
}
/**
* Given already parsed list elements, gets the output for the list.
*
* @param string $listType Type of list (ol or ul)
* @param array $elements List of elements in the list. These are already rendered.
*
* @return string
*/
protected function _renderListOutput($listType, array $elements)
{
return "<$listType>\n<li>" . implode("</li>\n<li>", $elements) . "</li>\n</$listType>";
}
/**
* Appends a list element if it is not empty.
*
* @param array $elements By reference. List of existing elements.
* @param string $appendString String to append (if not empty)
*/
protected function _appendListElement(array &$elements, $appendString)
{
if ($appendString !== '')
{
$appendString = rtrim($appendString);
if (substr($appendString, -6) == '<br />')
{
$appendString = substr($appendString, 0, -6);
}
$elements[] = $appendString;
}
}
public function renderTagAttach(array $tag, array $rendererStates)
{
$id = intval($this->stringifyTree($tag['children']));
if (!$id)
{
return '';
}
if (!$this->_view)
{
$phrase = new XenForo_Phrase('view_attachment_x', array('name' => $id));
return '<a href="' . XenForo_Link::buildPublicLink('full:attachments', array('attachment_id' => $id)) . '">' . $phrase . '</a>';
}
if (empty($rendererStates['attachments'][$id]))
{
$attachment = array('attachment_id' => $id);
$validAttachment = false;
$canView = false;
}
else
{
$attachment = $rendererStates['attachments'][$id];
$validAttachment = true;
$canView = empty($rendererStates['viewAttachments']) ? false : true;
}
$template = $this->_view->createTemplateObject('bb_code_tag_attach', array(
'attachment' => $attachment,
'validAttachment' => $validAttachment,
'canView' => $canView,
'full' => (strtolower($tag['option']) == 'full')
));
return $template->render();
}
public function renderTagUser(array $tag, array $rendererStates)
{
$content = $this->renderSubTree($tag['children'], $rendererStates);
if ($content === '')
{
return '';
}
$userId = intval($tag['option']);
if (!$userId)
{
return $content;
}
$link = XenForo_Link::buildPublicLink('full:members', array('user_id' => $userId));
$username = $this->stringifyTree($tag['children']);
return $this->_wrapInHtml('<a href="' . htmlspecialchars($link) . '" class="username" data-user="' . $userId . ', ' . htmlspecialchars($username) . '">', '</a>', $content);
}
/**
* Renders a media tag. Media tags embed rich media (usually videos). To embed a video,
* the source must be known.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagMedia(array $tag, array $rendererStates)
{
$mediaKey = trim($this->stringifyTree($tag['children']));
if (preg_match('#[&?"\'<>\r\n]#', $mediaKey) || strpos($mediaKey, '..') !== false)
{
return '';
}
$censored = XenForo_Helper_String::censorString($mediaKey);
if ($censored != $mediaKey)
{
return '';
}
$mediaSiteId = strtolower($tag['option']);
if ($mediaSiteId == 'youtube')
{
// youtube iframe embed bug workaround
$mediaKey = str_replace('/', '', $mediaKey);
}
if (isset($this->_mediaSites[$mediaSiteId]))
{
$embedHtml = $this->_getMediaSiteHtmlFromCallback($mediaKey, $this->_mediaSites[$mediaSiteId], $mediaSiteId);
if (!$embedHtml)
{
$embedHtml = strtr($this->_mediaSites[$mediaSiteId]['embed_html'], array(
'{$id}' => rawurlencode($mediaKey),
'{$id:digits}' => intval($mediaKey)
));
}
return $embedHtml;
}
else
{
return '';
}
}
/**
* Renders a spoiler tag.
*
* @param array $tag Information about the tag reference; keys: tag, option, children
* @param array $rendererStates Renderer states to push down
*
* @return string Rendered tag
*/
public function renderTagSpoiler(array $tag, array $rendererStates)
{
$keys = array_keys($tag['children']);
if (!$keys)
{
return '';
}
$first = reset($keys);
$last = end($keys);
if (is_string($tag['children'][$first]))
{
$tag['children'][$first] = ltrim($tag['children'][$first]);
}
if (is_string($tag['children'][$last]))
{
$tag['children'][$last] = rtrim($tag['children'][$last]);
}
$content = $this->renderSubTree($tag['children'], $rendererStates);
if ($content === '')
{
return '';
}
if ($tag['option'])
{
$title = $this->filterString($tag['option'],
array_merge($rendererStates, array(
'stopSmilies' => true,
'stopLineBreakConversion' => true
))
);
}
else
{
$title = false;
}
if ($this->_view)
{
$template = $this->_view->createTemplateObject('bb_code_tag_spoiler', array(
'content' => $content,
'titleHtml' => $title
));
return $template->render();
}
else
{
return $this->_renderTagSpoilerFallback($title, $content, $rendererStates);
}
}
/**
* Returns HTML output for a spoiler tag when the view is not available
*
* @param string $title Title of spoiler
* @param string $content Spoiler text
* @param array $rendererStates
*
* @return string
*/
protected function _renderTagSpoilerFallback($title, $content, array $rendererStates)
{
if (!empty($rendererStates['spoilerTextWithFallback']))
{
return '<div>' . $content . '</div>';
}
else
{
$spoilerText = new XenForo_Phrase('spoiler');
return '<div><b>' . ($title ? ($spoilerText . ': ' . $title) : $spoilerText) . '</b></div>';
}
}
/**
* Attempts to fetch media tag embed HTML using the callback method defined for a media site, if one is specified.
*
* @param string $mediaKey
* @param array $site Information about the site to render this media
* @param string $siteId
*
* @return string|boolean Returns false if callback is invalid
*/
protected function _getMediaSiteHtmlFromCallback($mediaKey, array $site, $siteId)
{
if (!empty($site['callback']) && is_array($site['callback']))
{
$class = $site['callback'][0];
$method = $site['callback'][1];
if (XenForo_Application::autoload($class) && method_exists($class, $method))
{
return call_user_func_array($site['callback'], array($mediaKey, $site, $siteId));
}
}
return false;
}
/**
* Replaces smilie strings in text with the appropriate "rich" markup.
* This method also escapes the output before the smilies are ultimately replaced.
* This is necessary to prevent the rich output from being escaped.
*
* @param string $text Text to replace in
* @param mixed $escapeCallback Callback for escaping. If empty, no escaping is done.
*
* @return string
*/
public function replaceSmiliesInText($text, $escapeCallback = '')
{
if ($this->_smilieTranslate)
{
$text = strtr($text, $this->_smilieTranslate);
}
if ($escapeCallback)
{
if ($escapeCallback == 'htmlspecialchars')
{
$text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}
else
{
$text = call_user_func($escapeCallback, $text);
}
}
if ($this->_smilieTranslate)
{
$split = preg_split("#\\0(\d+)\\0#", $text, -1, PREG_SPLIT_DELIM_CAPTURE);
$text = '';
foreach ($split AS $key => $value)
{
// odd keys contain the delimiter we want
if ($key % 2 == 0)
{
$text .= $value;
}
else if (isset($this->_smilieReverse[$value]))
{
$text .= $this->_smilieReverse[$value];
}
}
}
return $text;
}
/**
* Create the specified BB code formatter.
*
* @param string $class Name of the class. If empty, uses this class; if doesn't contain an underscore, assumes a partial name
* @param array|boolean Set of options to configure formatter; defaults to pulling as necessary; if false, doesn't look in registry etc
*
* @return XenForo_BbCode_Formatter_Base
*/
public static function create($class = '', $options = array())
{
if (!$class)
{
$class = __CLASS__;
}
else if (strpos($class, '_') === false)
{
$class = 'XenForo_BbCode_Formatter_' . $class;
}
$class = XenForo_Application::resolveDynamicClass($class, 'bb_code');
/** @var XenForo_BbCode_Formatter_Base $formatter */
$formatter = new $class();
if (is_array($options))
{
$baseOptions = array(
'smilies' => ****,
'bbCode' => ****,
'view' => ****
);
$options = array_merge($baseOptions, $options);
}
else
{
$options = array(
'smilies' => array(),
// omit bbCode as we basically always want this for custom tags
'bbCode' => ****,
'view' => false
);
}
if (!is_array($options['smilies']))
{
if (XenForo_Application::isRegistered('smilies'))
{
$options['smilies'] = XenForo_Application::get('smilies');
}
else
{
$options['smilies'] = XenForo_Model::create('XenForo_Model_Smilie')->getAllSmiliesForCache();
XenForo_Application::set('smilies', $options['smilies']);
}
}
if ($options['smilies'])
{
$formatter->addSmilies($options['smilies']);
}
if (!is_array($options['bbCode']))
{
if (XenForo_Application::isRegistered('bbCode'))
{
$options['bbCode'] = XenForo_Application::get('bbCode');
}
else
{
$options['bbCode'] = XenForo_Model::create('XenForo_Model_BbCode')->getBbCodeCache();
XenForo_Application::set('bbCode', $options['bbCode']);
}
}
if (!empty($options['bbCode']['mediaSites']))
{
$formatter->addMediaSites($options['bbCode']['mediaSites']);
}
if (!empty($options['bbCode']['bbCodes']))
{
$formatter->addCustomTags($options['bbCode']['bbCodes']);
}
if ($options['view'])
{
$formatter->setView($options['view']);
}
return $formatter;
}
public function renderTagH1(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
return '<h1>' . $text . '</h1>';
}
public function renderTagH2(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
return '<h2>' . $text . '</h2>';
}
public function renderTagH3(array $tag, array $rendererStates)
{
$text = $this->renderSubTree($tag['children'], $rendererStates);
return '<h3>' . $text . '</h3>';
}
}