|
<?php |
|
|
|
/** |
|
* Parse bulletin board code in a string, as well as smileys optionally. |
|
* |
|
* What it does: |
|
* - only parses bbc tags which are not disabled in disabledBBC. |
|
* - handles basic HTML, if enablePostHTML is on. |
|
* - caches the from/to replace regular expressions so as not to reload them every time a string is parsed. |
|
* - only parses smileys if smileys is true. |
|
* - does nothing if the enableBBC setting is off. |
|
* - uses the cache_id as a unique identifier to facilitate any caching it may do. |
|
* - returns the modified message. |
|
* |
|
* @param string|false $message if false return list of enabled bbc codes |
|
* @param bool|string $smileys = true |
|
* @param string $cache_id = '' |
|
* @param string[]|null $parse_tags array of tags to parse, null for all |
|
* @return string |
|
*/ |
|
function parse_bbc($message, $smileys = true, $cache_id = '', $parse_tags = array()) |
|
{ |
|
global $txt, $scripturl, $context, $modSettings, $user_info; |
|
|
|
static $bbc_codes = array(), $itemcodes = array(), $no_autolink_tags = array(); |
|
static $disabled, $default_disabled, $parse_tag_cache; |
|
|
|
// Don't waste cycles |
|
if ($message === '') |
|
return ''; |
|
|
|
// Clean up any cut/paste issues we may have |
|
$message = sanitizeMSCutPaste($message); |
|
|
|
// If the load average is too high, don't parse the BBC. |
|
if (!empty($modSettings['bbc']) && $modSettings['current_load'] >= $modSettings['bbc']) |
|
{ |
|
$context['disabled_parse_bbc'] = true; |
|
return $message; |
|
} |
|
|
|
if ($smileys !== null && ($smileys == '1' || $smileys == '0')) |
|
$smileys = (bool) $smileys; |
|
|
|
if (empty($modSettings['enableBBC']) && $message !== false) |
|
{ |
|
if ($smileys === true) |
|
parsesmileys($message); |
|
|
|
return $message; |
|
} |
|
|
|
// Allow addons access before entering the main parse_bbc loop |
|
call_integration_hook('integrate_pre_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags)); |
|
|
|
// Sift out the bbc for a performance improvement. |
|
if (empty($bbc_codes) || $message === false) |
|
{ |
|
if (!empty($modSettings['disabledBBC'])) |
|
{ |
|
$temp = explode(',', strtolower($modSettings['disabledBBC'])); |
|
|
|
foreach ($temp as $tag) |
|
$disabled[trim($tag)] = true; |
|
} |
|
|
|
/* The following bbc are formatted as an array, with keys as follows: |
|
|
|
tag: the tag's name - should be lowercase! |
|
|
|
type: one of... |
|
- (missing): [tag]parsed content[/tag] |
|
- unparsed_equals: [tag=xyz]parsed content[/tag] |
|
- parsed_equals: [tag=parsed data]parsed content[/tag] |
|
- unparsed_content: [tag]unparsed content[/tag] |
|
- closed: [tag], [tag/], [tag /] |
|
- unparsed_commas: [tag=1,2,3]parsed content[/tag] |
|
- unparsed_commas_content: [tag=1,2,3]unparsed content[/tag] |
|
- unparsed_equals_content: [tag=...]unparsed content[/tag] |
|
|
|
parameters: an optional array of parameters, for the form |
|
[tag abc=123]content[/tag]. The array is an associative array |
|
where the keys are the parameter names, and the values are an |
|
array which may contain the following: |
|
- match: a regular expression to validate and match the value. |
|
- quoted: true if the value should be quoted. |
|
- validate: callback to evaluate on the data, which is $data. |
|
- value: a string in which to replace $1 with the data. |
|
either it or validate may be used, not both. |
|
- optional: true if the parameter is optional. |
|
|
|
test: a regular expression to test immediately after the tag's |
|
'=', ' ' or ']'. Typically, should have a \] at the end. |
|
Optional. |
|
|
|
content: only available for unparsed_content, closed, |
|
unparsed_commas_content, and unparsed_equals_content. |
|
$1 is replaced with the content of the tag. Parameters |
|
are replaced in the form {param}. For unparsed_commas_content, |
|
$2, $3, ..., $n are replaced. |
|
|
|
before: only when content is not used, to go before any |
|
content. For unparsed_equals, $1 is replaced with the value. |
|
For unparsed_commas, $1, $2, ..., $n are replaced. |
|
|
|
after: similar to before in every way, except that it is used |
|
when the tag is closed. |
|
|
|
disabled_content: used in place of content when the tag is |
|
disabled. For closed, default is '', otherwise it is '$1' if |
|
block_level is false, '<div>$1</div>' elsewise. |
|
|
|
disabled_before: used in place of before when disabled. Defaults |
|
to '<div>' if block_level, '' if not. |
|
|
|
disabled_after: used in place of after when disabled. Defaults |
|
to '</div>' if block_level, '' if not. |
|
|
|
block_level: set to true the tag is a "block level" tag, similar |
|
to HTML. Block level tags cannot be nested inside tags that are |
|
not block level, and will not be implicitly closed as easily. |
|
One break following a block level tag may also be removed. |
|
|
|
trim: if set, and 'inside' whitespace after the begin tag will be |
|
removed. If set to 'outside', whitespace after the end tag will |
|
meet the same fate. |
|
|
|
validate: except when type is missing or 'closed', a callback to |
|
validate the data as $data. Depending on the tag's type, $data |
|
may be a string or an array of strings (corresponding to the |
|
replacement.) |
|
|
|
quoted: when type is 'unparsed_equals' or 'parsed_equals' only, |
|
may be not set, 'optional', or 'required' corresponding to if |
|
the content may be quoted. This allows the parser to read |
|
[tag="abc]def[esdf]"] properly. |
|
|
|
require_parents: an array of tag names, or not set. If set, the |
|
enclosing tag *must* be one of the listed tags, or parsing won't |
|
occur. |
|
|
|
require_children: similar to require_parents, if set children |
|
won't be parsed if they are not in the list. |
|
|
|
disallow_children: similar to, but very different from, |
|
require_children, if it is set the listed tags will not be |
|
parsed inside the tag. |
|
|
|
disallow_parents: similar to, but very different from, |
|
require_parents, if it is set the listed tags will not be |
|
parsed inside the tag. |
|
|
|
parsed_tags_allowed: an array restricting what BBC can be in the |
|
parsed_equals parameter, if desired. |
|
*/ |
|
|
|
$codes = array( |
|
// @todo Just to test unparsed_commas, but remove this autacity ! |
|
array( |
|
'tag' => 'glow', |
|
'type' => 'unparsed_commas', |
|
'test' => '[#0-9a-zA-Z\-]{3,12},([012]\d{1,2}|\d{1,2})(,[^]]+)?\]', |
|
'before' => '<table style="border: 0; border-spacing: 0; padding: 0; display: inline; vertical-align: middle; font: inherit;"><tr><td style="filter: Glow(color=$1, strength=$2); font: inherit;">', |
|
'after' => '</td></tr></table> ', |
|
), |
|
array( |
|
'tag' => 'abbr', |
|
'type' => 'unparsed_equals', |
|
'before' => '<abbr title="$1">', |
|
'after' => '</abbr>', |
|
'quoted' => 'optional', |
|
'disabled_after' => ' ($1)', |
|
), |
|
array( |
|
'tag' => 'anchor', |
|
'type' => 'unparsed_equals', |
|
'test' => '[#]?([A-Za-z][A-Za-z0-9_\-]*)\]', |
|
'before' => '<span id="post_$1">', |
|
'after' => '</span>', |
|
), |
|
array( |
|
'tag' => 'b', |
|
'before' => '<strong class="bbc_strong">', |
|
'after' => '</strong>', |
|
), |
|
array( |
|
'tag' => 'br', |
|
'type' => 'closed', |
|
'content' => '<br />', |
|
), |
|
array( |
|
'tag' => 'center', |
|
'before' => '<div class="centertext">', |
|
'after' => '</div>', |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'code', |
|
'type' => 'unparsed_content', |
|
'content' => '<div class="codeheader">' . $txt['code'] . ': <a href="javascript:void(0);" onclick="return elkSelectText(this);" class="codeoperation">' . $txt['code_select'] . '</a></div><pre class="bbc_code prettyprint">$1</pre>', |
|
'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) { |
|
if (!isset($disabled['code'])) |
|
$data = str_replace("\t", "<span class=\"tab\">\t</span>", $data); |
|
}, |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'code', |
|
'type' => 'unparsed_equals_content', |
|
'content' => '<div class="codeheader">' . $txt['code'] . ': ($2) <a href="#" onclick="return elkSelectText(this);" class="codeoperation">' . $txt['code_select'] . '</a></div><pre class="bbc_code prettyprint">$1</pre>', |
|
'validate' => isset($disabled['code']) ? null : function(&$tag, &$data, $disabled) { |
|
if (!isset($disabled['code'])) |
|
$data[0] = str_replace("\t", "<span class=\"tab\">\t</span>", $data[0]); |
|
}, |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'color', |
|
'type' => 'unparsed_equals', |
|
'test' => '(#[\da-fA-F]{3}|#[\da-fA-F]{6}|[A-Za-z]{1,20}|rgb\((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\s?,\s?){2}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\))\]', |
|
'before' => '<span style="color: $1;" class="bbc_color">', |
|
'after' => '</span>', |
|
), |
|
array( |
|
'tag' => 'email', |
|
'type' => 'unparsed_content', |
|
'content' => '<a href="mailto:$1" class="bbc_email">$1</a>', |
|
'validate' => function(&$tag, &$data, $disabled) { |
|
$data = strtr($data, array('<br />' => '')); |
|
}, |
|
), |
|
array( |
|
'tag' => 'email', |
|
'type' => 'unparsed_equals', |
|
'before' => '<a href="mailto:$1" class="bbc_email">', |
|
'after' => '</a>', |
|
'disallow_children' => array('email', 'ftp', 'url', 'iurl'), |
|
'disabled_after' => ' ($1)', |
|
), |
|
array( |
|
'tag' => 'footnote', |
|
'before' => '<sup class="bbc_footnotes">%fn%', |
|
'after' => '%fn%</sup>', |
|
'disallow_parents' => array('footnote', 'code', 'anchor', 'url', 'iurl'), |
|
'disallow_before' => '', |
|
'disallow_after' => '', |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'font', |
|
'type' => 'unparsed_equals', |
|
'test' => '[A-Za-z0-9_,\-\s]+?\]', |
|
'before' => '<span style="font-family: $1;" class="bbc_font">', |
|
'after' => '</span>', |
|
), |
|
array( |
|
'tag' => 'ftp', |
|
'type' => 'unparsed_content', |
|
'content' => '<a href="$1" class="bbc_ftp new_win" target="_blank">$1</a>', |
|
'validate' => function(&$tag, &$data, $disabled) { |
|
$data = strtr($data, array('<br />' => '')); |
|
if (strpos($data, 'ftp://') !== 0 && strpos($data, 'ftps://') !== 0) |
|
$data = 'ftp://' . $data; |
|
}, |
|
), |
|
array( |
|
'tag' => 'ftp', |
|
'type' => 'unparsed_equals', |
|
'before' => '<a href="$1" class="bbc_ftp new_win" target="_blank">', |
|
'after' => '</a>', |
|
'validate' => function(&$tag, &$data, $disabled) { |
|
if (strpos($data, 'ftp://') !== 0 && strpos($data, 'ftps://') !== 0) |
|
$data = 'ftp://' . $data; |
|
}, |
|
'disallow_children' => array('email', 'ftp', 'url', 'iurl'), |
|
'disabled_after' => ' ($1)', |
|
), |
|
array( |
|
'tag' => 'hr', |
|
'type' => 'closed', |
|
'content' => '<hr />', |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'i', |
|
'before' => '<em>', |
|
'after' => '</em>', |
|
), |
|
array( |
|
'tag' => 'img', |
|
'type' => 'unparsed_content', |
|
'parameters' => array( |
|
'alt' => array('optional' => true), |
|
'width' => array('optional' => true, 'value' => 'width:100%;max-width:$1px;', 'match' => '(\d+)'), |
|
'height' => array('optional' => true, 'value' => 'max-height:$1px;', 'match' => '(\d+)'), |
|
), |
|
'content' => '<img src="$1" alt="{alt}" style="{width}{height}" class="bbc_img resized" />', |
|
'validate' => function(&$tag, &$data, $disabled) { |
|
$data = strtr($data, array('<br />' => '')); |
|
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0) |
|
$data = 'http://' . $data; |
|
}, |
|
'disabled_content' => '($1)', |
|
), |
|
array( |
|
'tag' => 'img', |
|
'type' => 'unparsed_content', |
|
'content' => '<img src="$1" alt="" class="bbc_img" />', |
|
'validate' => function(&$tag, &$data, $disabled) { |
|
$data = strtr($data, array('<br />' => '')); |
|
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0) |
|
$data = 'http://' . $data; |
|
}, |
|
'disabled_content' => '($1)', |
|
), |
|
array( |
|
'tag' => 'iurl', |
|
'type' => 'unparsed_content', |
|
'content' => '<a href="$1" class="bbc_link">$1</a>', |
|
'validate' => function(&$tag, &$data, $disabled) { |
|
$data = strtr($data, array('<br />' => '')); |
|
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0) |
|
$data = 'http://' . $data; |
|
}, |
|
), |
|
array( |
|
'tag' => 'iurl', |
|
'type' => 'unparsed_equals', |
|
'before' => '<a href="$1" class="bbc_link">', |
|
'after' => '</a>', |
|
'validate' => function(&$tag, &$data, $disabled) { |
|
if ($data[0] === '#') |
|
$data = '#post_' . substr($data, 1); |
|
elseif (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0) |
|
$data = 'http://' . $data; |
|
}, |
|
'disallow_children' => array('email', 'ftp', 'url', 'iurl'), |
|
'disabled_after' => ' ($1)', |
|
), |
|
array( |
|
'tag' => 'left', |
|
'before' => '<div style="text-align: left;">', |
|
'after' => '</div>', |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'li', |
|
'before' => '<li>', |
|
'after' => '</li>', |
|
'trim' => 'outside', |
|
'require_parents' => array('list'), |
|
'block_level' => true, |
|
'disabled_before' => '', |
|
'disabled_after' => '<br />', |
|
), |
|
array( |
|
'tag' => 'list', |
|
'before' => '<ul class="bbc_list">', |
|
'after' => '</ul>', |
|
'trim' => 'inside', |
|
'require_children' => array('li', 'list'), |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'list', |
|
'parameters' => array( |
|
'type' => array('match' => '(none|disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman|lower-alpha|upper-alpha|lower-greek|lower-latin|upper-latin|hebrew|armenian|georgian|cjk-ideographic|hiragana|katakana|hiragana-iroha|katakana-iroha)'), |
|
), |
|
'before' => '<ul class="bbc_list" style="list-style-type: {type};">', |
|
'after' => '</ul>', |
|
'trim' => 'inside', |
|
'require_children' => array('li'), |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'me', |
|
'type' => 'unparsed_equals', |
|
'before' => '<div class="meaction"> $1 ', |
|
'after' => '</div>', |
|
'quoted' => 'optional', |
|
'block_level' => true, |
|
'disabled_before' => '/me ', |
|
'disabled_after' => '<br />', |
|
), |
|
array( |
|
'tag' => 'member', |
|
'type' => 'unparsed_equals', |
|
'test' => '[\d*]', |
|
'before' => '<span class="bbc_mention"><a href="' . $scripturl . '?action=profile;u=$1">@', |
|
'after' => '</a></span>', |
|
'disabled_before' => '@', |
|
'disabled_after' => '', |
|
), |
|
array( |
|
'tag' => 'nobbc', |
|
'type' => 'unparsed_content', |
|
'content' => '$1', |
|
), |
|
array( |
|
'tag' => 'pre', |
|
'before' => '<pre class="bbc_pre">', |
|
'after' => '</pre>', |
|
), |
|
array( |
|
'tag' => 'quote', |
|
'before' => '<div class="quoteheader">' . $txt['quote'] . '</div><blockquote>', |
|
'after' => '</blockquote>', |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'quote', |
|
'parameters' => array( |
|
'author' => array('match' => '(.{1,192}?\]?)', 'quoted' => true), |
|
), |
|
'before' => '<div class="quoteheader">' . $txt['quote_from'] . ': {author}</div><blockquote>', |
|
'after' => '</blockquote>', |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'quote', |
|
'type' => 'parsed_equals', |
|
'before' => '<div class="quoteheader">' . $txt['quote_from'] . ': $1</div><blockquote>', |
|
'after' => '</blockquote>', |
|
'quoted' => 'optional', |
|
// Don't allow everything to be embedded with the author name. |
|
'parsed_tags_allowed' => array('url', 'iurl', 'ftp'), |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'quote', |
|
'parameters' => array( |
|
'author' => array('match' => '([^<>]{1,192}?)'), |
|
'link' => array('match' => '(?:board=\d+;)?((?:topic|threadid)=[\dmsg#\./]{1,40}(?:;start=[\dmsg#\./]{1,40})?|msg=\d{1,40}|action=profile;u=\d+)'), |
|
'date' => array('match' => '(\d+)', 'validate' => 'htmlTime'), |
|
), |
|
'before' => '<div class="quoteheader"><a href="' . $scripturl . '?{link}">' . $txt['quote_from'] . ': {author} ' . ($modSettings['todayMod'] == 3 ? ' - ' : $txt['search_on']) . ' {date}</a></div><blockquote>', |
|
'after' => '</blockquote>', |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'quote', |
|
'parameters' => array( |
|
'author' => array('match' => '(.{1,192}?)'), |
|
), |
|
'before' => '<div class="quoteheader">' . $txt['quote_from'] . ': {author}</div><blockquote>', |
|
'after' => '</blockquote>', |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'right', |
|
'before' => '<div style="text-align: right;">', |
|
'after' => '</div>', |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 's', |
|
'before' => '<del>', |
|
'after' => '</del>', |
|
), |
|
array( |
|
'tag' => 'size', |
|
'type' => 'unparsed_equals', |
|
'test' => '([1-9][\d]?p[xt]|small(?:er)?|large[r]?|x[x]?-(?:small|large)|medium|(0\.[1-9]|[1-9](\.[\d][\d]?)?)?em)\]', |
|
'before' => '<span style="font-size: $1;" class="bbc_size">', |
|
'after' => '</span>', |
|
'disallow_parents' => array('size'), |
|
'disallow_before' => '<span>', |
|
'disallow_after' => '</span>', |
|
), |
|
array( |
|
'tag' => 'size', |
|
'type' => 'unparsed_equals', |
|
'test' => '[1-7]\]', |
|
'before' => '<span style="font-size: $1;" class="bbc_size">', |
|
'after' => '</span>', |
|
'validate' => function(&$tag, &$data, $disabled) { |
|
$sizes = array(1 => 0.7, 2 => 1.0, 3 => 1.35, 4 => 1.45, 5 => 2.0, 6 => 2.65, 7 => 3.95); |
|
$data = $sizes[$data] . 'em'; |
|
}, |
|
'disallow_parents' => array('size'), |
|
'disallow_before' => '<span>', |
|
'disallow_after' => '</span>', |
|
), |
|
array( |
|
'tag' => 'spoiler', |
|
'before' => '<span class="spoilerheader">' . $txt['spoiler'] . '</span><div class="spoiler"><div class="bbc_spoiler" style="display: none;">', |
|
'after' => '</div></div>', |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'sub', |
|
'before' => '<sub>', |
|
'after' => '</sub>', |
|
), |
|
array( |
|
'tag' => 'sup', |
|
'before' => '<sup>', |
|
'after' => '</sup>', |
|
), |
|
array( |
|
'tag' => 'table', |
|
'before' => '<div class="bbc_table_container"><table class="bbc_table">', |
|
'after' => '</table></div>', |
|
'trim' => 'inside', |
|
'require_children' => array('tr'), |
|
'block_level' => true, |
|
), |
|
array( |
|
'tag' => 'td', |
|
'before' => '<td>', |
|
'after' => '</td>', |
|
'require_parents' => array('tr'), |
|
'trim' => 'outside', |
|
'block_level' => true, |
|
'disabled_before' => '', |
|
'disabled_after' => '', |
|
), |
|
array( |
|
'tag' => 'th', |
|
'before' => '<th>', |
|
'after' => '</th>', |
|
'require_parents' => array('tr'), |
|
'trim' => 'outside', |
|
'block_level' => true, |
|
'disabled_before' => '', |
|
'disabled_after' => '', |
|
), |
|
array( |
|
'tag' => 'tr', |
|
'before' => '<tr>', |
|
'after' => '</tr>', |
|
'require_parents' => array('table'), |
|
'require_children' => array('td', 'th'), |
|
'trim' => 'both', |
|
'block_level' => true, |
|
'disabled_before' => '', |
|
'disabled_after' => '', |
|
), |
|
array( |
|
'tag' => 'tt', |
|
'before' => '<span class="bbc_tt">', |
|
'after' => '</span>', |
|
), |
|
array( |
|
'tag' => 'u', |
|
'before' => '<span class="bbc_u">', |
|
'after' => '</span>', |
|
), |
|
array( |
|
'tag' => 'url', |
|
'type' => 'unparsed_content', |
|
'content' => '<a href="$1" class="bbc_link" target="_blank">$1</a>', |
|
'validate' => function(&$tag, &$data, $disabled) { |
|
$data = strtr($data, array('<br />' => '')); |
|
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0) |
|
$data = 'http://' . $data; |
|
}, |
|
), |
|
array( |
|
'tag' => 'url', |
|
'type' => 'unparsed_equals', |
|
'before' => '<a href="$1" class="bbc_link" target="_blank">', |
|
'after' => '</a>', |
|
'validate' => function(&$tag, &$data, $disabled) { |
|
if (strpos($data, 'http://') !== 0 && strpos($data, 'https://') !== 0) |
|
$data = 'http://' . $data; |
|
}, |
|
'disallow_children' => array('email', 'ftp', 'url', 'iurl'), |
|
'disabled_after' => ' ($1)', |
|
), |
|
); |
|
|
|
// Inside these tags autolink is not recommendable. |
|
$no_autolink_tags = array( |
|
'url', |
|
'iurl', |
|
'ftp', |
|
'email', |
|
); |
|
// So the parser won't skip them. |
|
$itemcodes = array( |
|
'*' => 'disc', |
|
'@' => 'disc', |
|
'+' => 'square', |
|
'x' => 'square', |
|
'#' => 'decimal', |
|
'0' => 'decimal', |
|
'o' => 'circle', |
|
'O' => 'circle', |
|
); |
|
|
|
// Let addons add new BBC without hassle. |
|
call_integration_hook('integrate_bbc_codes', array(&$codes, &$no_autolink_tags, &$itemcodes)); |
|
|
|
// This is mainly for the bbc manager, so it's easy to add tags above. Custom BBC should be added above this line. |
|
if ($message === false) |
|
{ |
|
if (isset($temp_bbc)) |
|
$bbc_codes = $temp_bbc; |
|
return $codes; |
|
} |
|
|
|
if (!isset($disabled['li']) && !isset($disabled['list'])) |
|
{ |
|
foreach ($itemcodes as $c => $dummy) |
|
$bbc_codes[$c] = array(); |
|
} |
|
|
|
foreach ($codes as $code) |
|
$bbc_codes[substr($code['tag'], 0, 1)][] = $code; |
|
} |
|
|
|
// If we are not doing every enabled tag then create a cache for this parsing group. |
|
if ($parse_tags !== array() && is_array($parse_tags)) |
|
{ |
|
$temp_bbc = $bbc_codes; |
|
$tags_cache_id = implode(',', $parse_tags); |
|
|
|
if (!isset($default_disabled)) |
|
$default_disabled = isset($disabled) ? $disabled : array(); |
|
|
|
// Already cached, use it, otherwise create it |
|
if (isset($parse_tag_cache[$tags_cache_id])) |
|
list ($bbc_codes, $disabled) = $parse_tag_cache[$tags_cache_id]; |
|
else |
|
{ |
|
foreach ($bbc_codes as $key_bbc => $bbc) |
|
{ |
|
foreach ($bbc as $key_code => $code) |
|
{ |
|
if (!in_array($code['tag'], $parse_tags)) |
|
{ |
|
$disabled[$code['tag']] = true; |
|
unset($bbc_codes[$key_bbc][$key_code]); |
|
} |
|
} |
|
} |
|
|
|
$parse_tag_cache[$tags_cache_id] = array($bbc_codes, $disabled); |
|
} |
|
} |
|
elseif (isset($default_disabled)) |
|
$disabled = $default_disabled; |
|
|
|
// Shall we take the time to cache this? |
|
if ($cache_id !== '' && !empty($modSettings['cache_enable']) && (($modSettings['cache_enable'] >= 2 && isset($message[1000])) || isset($message[2400])) && empty($parse_tags)) |
|
{ |
|
// It's likely this will change if the message is modified. |
|
$cache_key = 'parse:' . $cache_id . '-' . md5(md5($message) . '-' . $smileys . (empty($disabled) ? '' : implode(',', array_keys($disabled))) . serialize($context['browser']) . $txt['lang_locale'] . $user_info['time_offset'] . $user_info['time_format']); |
|
|
|
if (($temp = cache_get_data($cache_key, 240)) !== null) |
|
return $temp; |
|
|
|
$cache_t = microtime(true); |
|
} |
|
|
|
if ($smileys === 'print') |
|
{ |
|
// Colors can't well be displayed... supposed to be black and white. |
|
$disabled['color'] = true; |
|
$disabled['me'] = true; |
|
|
|
// Links are useless on paper... just show the link. |
|
$disabled['url'] = true; |
|
$disabled['iurl'] = true; |
|
$disabled['email'] = true; |
|
|
|
// @todo Change maybe? |
|
if (!isset($_GET['images'])) |
|
$disabled['img'] = true; |
|
|
|
// @todo Interface/setting to add more? |
|
} |
|
|
|
$open_tags = array(); |
|
$message = strtr($message, array("\n" => '<br />')); |
|
|
|
// The non-breaking-space looks a bit different each time. |
|
$non_breaking_space = '\x{A0}'; |
|
|
|
$pos = -1; |
|
while ($pos !== false) |
|
{ |
|
$last_pos = isset($last_pos) ? max($pos, $last_pos) : $pos; |
|
$pos = strpos($message, '[', $pos + 1); |
|
|
|
// Failsafe. |
|
if ($pos === false || $last_pos > $pos) |
|
$pos = strlen($message) + 1; |
|
|
|
// Can't have a one letter smiley, URL, or email! (sorry.) |
|
if ($last_pos < $pos - 1) |
|
{ |
|
// Make sure the $last_pos is not negative. |
|
$last_pos = max($last_pos, 0); |
|
|
|
// Pick a block of data to do some raw fixing on. |
|
$data = substr($message, $last_pos, $pos - $last_pos); |
|
|
|
// Take care of some HTML! |
|
if (!empty($modSettings['enablePostHTML']) && strpos($data, '<') !== false) |
|
{ |
|
$data = preg_replace('~<a\s+href=((?:")?)((?:https?://|ftps?://|mailto:)\S+?)\\1>~i', '[url=$2]', $data); |
|
$data = preg_replace('~</a>~i', '[/url]', $data); |
|
|
|
// <br /> should be empty. |
|
$empty_tags = array('br', 'hr'); |
|
foreach ($empty_tags as $tag) |
|
$data = str_replace(array('<' . $tag . '>', '<' . $tag . '/>', '<' . $tag . ' />'), '[' . $tag . ' /]', $data); |
|
|
|
// b, u, i, s, pre... basic tags. |
|
$closable_tags = array('b', 'u', 'i', 's', 'em', 'ins', 'del', 'pre', 'blockquote'); |
|
foreach ($closable_tags as $tag) |
|
{ |
|
$diff = substr_count($data, '<' . $tag . '>') - substr_count($data, '</' . $tag . '>'); |
|
$data = strtr($data, array('<' . $tag . '>' => '<' . $tag . '>', '</' . $tag . '>' => '</' . $tag . '>')); |
|
|
|
if ($diff > 0) |
|
$data = substr($data, 0, -1) . str_repeat('</' . $tag . '>', $diff) . substr($data, -1); |
|
} |
|
|
|
// Do <img ... /> - with security... action= -> action-. |
|
preg_match_all('~<img\s+src=((?:")?)((?:https?://|ftps?://)\S+?)\\1(?:\s+alt=(".*?"|\S*?))?(?:\s?/)?>~i', $data, $matches, PREG_PATTERN_ORDER); |
|
if (!empty($matches[0])) |
|
{ |
|
$replaces = array(); |
|
foreach ($matches[2] as $match => $imgtag) |
|
{ |
|
$alt = empty($matches[3][$match]) ? '' : ' alt=' . preg_replace('~^"|"$~', '', $matches[3][$match]); |
|
|
|
// Remove action= from the URL - no funny business, now. |
|
if (preg_match('~action(=|%3d)(?!dlattach)~i', $imgtag) !== 0) |
|
$imgtag = preg_replace('~action(?:=|%3d)(?!dlattach)~i', 'action-', $imgtag); |
|
|
|
// Check if the image is larger than allowed. |
|
// @todo - We should seriously look at deprecating some of this in favour of CSS resizing. |
|
if (!empty($modSettings['max_image_width']) && !empty($modSettings['max_image_height'])) |
|
{ |
|
// For images, we'll want this. |
|
require_once(SUBSDIR . '/Attachments.subs.php'); |
|
list ($width, $height) = url_image_size($imgtag); |
|
|
|
if (!empty($modSettings['max_image_width']) && $width > $modSettings['max_image_width']) |
|
{ |
|
$height = (int) (($modSettings['max_image_width'] * $height) / $width); |
|
$width = $modSettings['max_image_width']; |
|
} |
|
|
|
if (!empty($modSettings['max_image_height']) && $height > $modSettings['max_image_height']) |
|
{ |
|
$width = (int) (($modSettings['max_image_height'] * $width) / $height); |
|
$height = $modSettings['max_image_height']; |
|
} |
|
|
|
// Set the new image tag. |
|
$replaces[$matches[0][$match]] = '[img width=' . $width . ' height=' . $height . $alt . ']' . $imgtag . '[/img]'; |
|
} |
|
else |
|
$replaces[$matches[0][$match]] = '[img' . $alt . ']' . $imgtag . '[/img]'; |
|
} |
|
|
|
$data = strtr($data, $replaces); |
|
} |
|
} |
|
|
|
if (!empty($modSettings['autoLinkUrls'])) |
|
{ |
|
// Are we inside tags that should be auto linked? |
|
$no_autolink_area = false; |
|
if (!empty($open_tags)) |
|
{ |
|
foreach ($open_tags as $open_tag) |
|
if (in_array($open_tag['tag'], $no_autolink_tags)) |
|
$no_autolink_area = true; |
|
} |
|
|
|
// Don't go backwards. |
|
// @todo Don't think is the real solution.... |
|
$lastAutoPos = isset($lastAutoPos) ? $lastAutoPos : 0; |
|
if ($pos < $lastAutoPos) |
|
$no_autolink_area = true; |
|
$lastAutoPos = $pos; |
|
|
|
if (!$no_autolink_area) |
|
{ |
|
// Parse any URLs.... have to get rid of the @ problems some things cause... stupid email addresses. |
|
if (!isset($disabled['url']) && (strpos($data, '://') !== false || strpos($data, 'www.') !== false) && strpos($data, '[url') === false) |
|
{ |
|
// Switch out quotes really quick because they can cause problems. |
|
$data = strtr($data, array(''' => '\'', ' ' => "\xC2\xA0", '"' => '>">', '"' => '<"<', '<' => '<lt<')); |
|
|
|
// Only do this if the preg survives. |
|
if (is_string($result = preg_replace(array( |
|
'~(?<=[\s>\.(;\'"]|^)((?:http|https)://[\w\-_%@:|]+(?:\.[\w\-_%]+)*(?::\d+)?(?:/[\p{L}\p{N}\-_\~%\.@!,\?&;=#(){}+:\'\\\\]*)*[/\p{L}\p{N}\-_\~%@\?;=#}\\\\])~ui', |
|
'~(?<=[\s>\.(;\'"]|^)((?:ftp|ftps)://[\w\-_%@:|]+(?:\.[\w\-_%]+)*(?::\d+)?(?:/[\w\-_\~%\.@,\?&;=#(){}+:\'\\\\]*)*[/\w\-_\~%@\?;=#}\\\\])~i', |
|
'~(?<=[\s>(\'<]|^)(www(?:\.[\w\-_]+)+(?::\d+)?(?:/[\p{L}\p{N}\-_\~%\.@!,\?&;=#(){}+:\'\\\\]*)*[/\p{L}\p{N}\-_\~%@\?;=#}\\\\])~ui' |
|
), array( |
|
'[url]$1[/url]', |
|
'[ftp]$1[/ftp]', |
|
'[url=http://$1]$1[/url]' |
|
), $data))) |
|
$data = $result; |
|
|
|
$data = strtr($data, array('\'' => ''', "\xC2\xA0" => ' ', '>">' => '"', '<"<' => '"', '<lt<' => '<')); |
|
} |
|
|
|
// Next, emails... |
|
if (!isset($disabled['email']) && strpos($data, '@') !== false && strpos($data, '[email') === false) |
|
{ |
|
$data = preg_replace('~(?<=[\?\s' . $non_breaking_space . '\[\]()*\\\;>]|^)([\w\-\.]{1,80}@[\w\-]+\.[\w\-\.]+[\w\-])(?=[?,\s' . $non_breaking_space . '\[\]()*\\\]|$|<br />| |>|<|"|'|\.(?:\.|;| |\s|$|<br />))~u', '[email]$1[/email]', $data); |
|
$data = preg_replace('~(?<=<br />)([\w\-\.]{1,80}@[\w\-]+\.[\w\-\.]+[\w\-])(?=[?\.,;\s' . $non_breaking_space . '\[\]()*\\\]|$|<br />| |>|<|"|')~u', '[email]$1[/email]', $data); |
|
} |
|
} |
|
} |
|
|
|
$data = strtr($data, array("\t" => ' ')); |
|
|
|
// If it wasn't changed, no copying or other boring stuff has to happen! |
|
if ($data !== substr($message, $last_pos, $pos - $last_pos)) |
|
{ |
|
$message = substr($message, 0, $last_pos) . $data . substr($message, $pos); |
|
|
|
// Since we changed it, look again in case we added or removed a tag. But we don't want to skip any. |
|
$old_pos = strlen($data) + $last_pos; |
|
$pos = strpos($message, '[', $last_pos); |
|
$pos = $pos === false ? $old_pos : min($pos, $old_pos); |
|
} |
|
|
|
} |
|
|
|
// Are we there yet? Are we there yet? |
|
if ($pos >= strlen($message) - 1) |
|
break; |
|
|
|
$tags = strtolower($message[$pos + 1]); |
|
|
|
if ($tags === '/' && !empty($open_tags)) |
|
{ |
|
$pos2 = strpos($message, ']', $pos + 1); |
|
if ($pos2 === $pos + 2) |
|
continue; |
|
|
|
$look_for = strtolower(substr($message, $pos + 2, $pos2 - $pos - 2)); |
|
|
|
$to_close = array(); |
|
$block_level = null; |
|
|
|
do |
|
{ |
|
$tag = array_pop($open_tags); |
|
if (!$tag) |
|
break; |
|
|
|
if (!empty($tag['block_level'])) |
|
{ |
|
// Only find out if we need to. |
|
if ($block_level === false) |
|
{ |
|
array_push($open_tags, $tag); |
|
break; |
|
} |
|
|
|
// The idea is, if we are LOOKING for a block level tag, we can close them on the way. |
|
if (strlen($look_for) > 0 && isset($bbc_codes[$look_for[0]])) |
|
{ |
|
foreach ($bbc_codes[$look_for[0]] as $temp) |
|
if ($temp['tag'] === $look_for) |
|
{ |
|
$block_level = !empty($temp['block_level']); |
|
break; |
|
} |
|
} |
|
|
|
if ($block_level !== true) |
|
{ |
|
$block_level = false; |
|
array_push($open_tags, $tag); |
|
break; |
|
} |
|
} |
|
|
|
$to_close[] = $tag; |
|
} |
|
while ($tag['tag'] !== $look_for); |
|
|
|
// Did we just eat through everything and not find it? |
|
if ((empty($open_tags) && (empty($tag) || $tag['tag'] !== $look_for))) |
|
{ |
|
$open_tags = $to_close; |
|
continue; |
|
} |
|
elseif (!empty($to_close) && $tag['tag'] !== $look_for) |
|
{ |
|
if ($block_level === null && isset($look_for[0], $bbc_codes[$look_for[0]])) |
|
{ |
|
foreach ($bbc_codes[$look_for[0]] as $temp) |
|
if ($temp['tag'] === $look_for) |
|
{ |
|
$block_level = !empty($temp['block_level']); |
|
break; |
|
} |
|
} |
|
|
|
// We're not looking for a block level tag (or maybe even a tag that exists...) |
|
if (!$block_level) |
|
{ |
|
foreach ($to_close as $tag) |
|
array_push($open_tags, $tag); |
|
continue; |
|
} |
|
} |
|
|
|
foreach ($to_close as $tag) |
|
{ |
|
$message = substr_replace($message, "\n" . $tag['after'] . "\n", $pos, $pos2 + 1 - $pos); |
|
|
|
$pos += strlen($tag['after']) + 2; |
|
$pos2 = $pos - 1; |
|
|
|
// See the comment at the end of the big loop - just eating whitespace ;). |
|
if (!empty($tag['block_level']) && substr($message, $pos, 6) === '<br />') |
|
$message = substr_replace($message, '', $pos, 6); |
|
if (!empty($tag['trim']) && $tag['trim'] !== 'inside' && preg_match('~(<br />| |\s)*~', substr($message, $pos), $matches) !== 0) |
|
$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0])); |
|
} |
|
|
|
if (!empty($to_close)) |
|
{ |
|
$to_close = array(); |
|
$pos--; |
|
} |
|
|
|
continue; |
|
} |
|
|
|
// No tags for this character, so just keep going (fastest possible course.) |
|
if (!isset($bbc_codes[$tags])) |
|
{ |
|
continue; |
|
} |
|
|
|
$inside = empty($open_tags) ? null : $open_tags[count($open_tags) - 1]; |
|
|
|
$tag = null; |
|
foreach ($bbc_codes[$tags] as $possible) |
|
{ |
|
$pt_strlen = strlen($possible['tag']); |
|
|
|
// Not a match? |
|
if (substr_compare($message, $possible['tag'], $pos + 1, $pt_strlen, true) !== 0) |
|
continue; |
|
|
|
$next_c = isset($message[$pos + 1 + $pt_strlen]) ? $message[$pos + 1 + $pt_strlen] : ''; |
|
|
|
// A test validation? |
|
if (isset($possible['test']) && preg_match('~^' . $possible['test'] . '~', substr($message, $pos + 1 + $pt_strlen + 1)) === 0) |
|
continue; |
|
// Do we want parameters? |
|
elseif (!empty($possible['parameters'])) |
|
{ |
|
if ($next_c !== ' ') |
|
continue; |
|
} |
|
elseif (isset($possible['type'])) |
|
{ |
|
// Do we need an equal sign? |
|
if ($next_c !== '=' && in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals'))) |
|
continue; |
|
// Maybe we just want a /... |
|
if ($possible['type'] === 'closed' && $next_c !== ']' && substr($message, $pos + 1 + $pt_strlen, 2) !== '/]' && substr($message, $pos + 1 + $pt_strlen, 3) !== ' /]') |
|
continue; |
|
// An immediate ]? |
|
if ($possible['type'] === 'unparsed_content' && $next_c !== ']') |
|
continue; |
|
} |
|
// No type means 'parsed_content', which demands an immediate ] without parameters! |
|
elseif ($next_c !== ']') |
|
continue; |
|
|
|
// Check allowed tree? |
|
if (isset($possible['require_parents']) && ($inside === null || !in_array($inside['tag'], $possible['require_parents']))) |
|
continue; |
|
elseif (isset($inside['require_children']) && !in_array($possible['tag'], $inside['require_children'])) |
|
continue; |
|
// If this is in the list of disallowed child tags, don't parse it. |
|
elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children'])) |
|
continue; |
|
// Not allowed in this parent, replace the tags or show it like regular text |
|
elseif (isset($possible['disallow_parents']) && ($inside !== null && in_array($inside['tag'], $possible['disallow_parents']))) |
|
{ |
|
if (!isset($possible['disallow_before'], $possible['disallow_after'])) |
|
continue; |
|
$possible['before'] = isset($possible['disallow_before']) ? $tag['disallow_before'] : $possible['before']; |
|
$possible['after'] = isset($possible['disallow_after']) ? $tag['disallow_after'] : $possible['after']; |
|
} |
|
|
|
$pos1 = $pos + 1 + $pt_strlen + 1; |
|
|
|
// Quotes can have alternate styling, we do this php-side due to all the permutations of quotes. |
|
if ($possible['tag'] === 'quote') |
|
{ |
|
// Start with standard |
|
$quote_alt = false; |
|
foreach ($open_tags as $open_quote) |
|
{ |
|
// Every parent quote this quote has flips the styling |
|
if ($open_quote['tag'] === 'quote') |
|
$quote_alt = !$quote_alt; |
|
} |
|
// Add a class to the quote to style alternating blockquotes |
|
// @todo - Frankly it makes little sense to allow alternate blockquote |
|
// styling without also catering for alternate quoteheader styling. |
|
// I do remember coding that some time back, but it seems to have gotten |
|
// lost somewhere in the Elk processes. |
|
// Come to think of it, it may be better to append a second class rather |
|
// than alter the standard one. |
|
// - Example: class="bbc_quote" and class="bbc_quote alt_quote". |
|
// This would mean simpler CSS for themes (like default) which do not use the alternate styling, |
|
// but would still allow it for themes that want it. |
|
$possible['before'] = strtr($possible['before'], array('<blockquote>' => '<blockquote class="bbc_' . ($quote_alt ? 'alternate' : 'standard') . '_quote">')); |
|
} |
|
|
|
// This is long, but it makes things much easier and cleaner. |
|
if (!empty($possible['parameters'])) |
|
{ |
|
$preg = array(); |
|
foreach ($possible['parameters'] as $p => $info) |
|
$preg[] = '(\s+' . $p . '=' . (empty($info['quoted']) ? '' : '"') . (isset($info['match']) ? $info['match'] : '(.+?)') . (empty($info['quoted']) ? '' : '"') . ')' . (empty($info['optional']) ? '' : '?'); |
|
|
|
// Okay, this may look ugly and it is, but it's not going to happen much and it is the best way |
|
// of allowing any order of parameters but still parsing them right. |
|
$param_size = count($preg) - 1; |
|
$preg_keys = range(0, $param_size); |
|
$message_stub = substr($message, $pos1 - 1); |
|
|
|
// If an addon adds many parameters we can exceed max_execution time, lets prevent that |
|
// 5040 = 7, 40,320 = 8, (N!) etc |
|
$max_iterations = 5040; |
|
|
|
// Step, one by one, through all possible permutations of the parameters until we have a match |
|
do { |
|
$match_preg = '~^'; |
|
foreach ($preg_keys as $key) |
|
$match_preg .= $preg[$key]; |
|
$match_preg .= '\]~i'; |
|
|
|
// Check if this combination of parameters matches the user input |
|
$match = preg_match($match_preg, $message_stub, $matches) !== 0; |
|
} while (!$match && --$max_iterations && ($preg_keys = pc_next_permutation($preg_keys, $param_size))); |
|
|
|
// Didn't match our parameter list, try the next possible. |
|
if (!$match) |
|
continue; |
|
|
|
$params = array(); |
|
for ($i = 1, $n = count($matches); $i < $n; $i += 2) |
|
{ |
|
$key = strtok(ltrim($matches[$i]), '='); |
|
if (isset($possible['parameters'][$key]['value'])) |
|
$params['{' . $key . '}'] = strtr($possible['parameters'][$key]['value'], array('$1' => $matches[$i + 1])); |
|
elseif (isset($possible['parameters'][$key]['validate'])) |
|
$params['{' . $key . '}'] = $possible['parameters'][$key]['validate']($matches[$i + 1]); |
|
else |
|
$params['{' . $key . '}'] = $matches[$i + 1]; |
|
|
|
// Just to make sure: replace any $ or { so they can't interpolate wrongly. |
|
$params['{' . $key . '}'] = strtr($params['{' . $key . '}'], array('$' => '$', '{' => '{')); |
|
} |
|
|
|
foreach ($possible['parameters'] as $p => $info) |
|
{ |
|
if (!isset($params['{' . $p . '}'])) |
|
$params['{' . $p . '}'] = ''; |
|
} |
|
|
|
$tag = $possible; |
|
|
|
// Put the parameters into the string. |
|
if (isset($tag['before'])) |
|
$tag['before'] = strtr($tag['before'], $params); |
|
if (isset($tag['after'])) |
|
$tag['after'] = strtr($tag['after'], $params); |
|
if (isset($tag['content'])) |
|
$tag['content'] = strtr($tag['content'], $params); |
|
|
|
$pos1 += strlen($matches[0]) - 1; |
|
} |
|
else |
|
$tag = $possible; |
|
break; |
|
} |
|
|
|
// Item codes are complicated buggers... they are implicit [li]s and can make [list]s! |
|
if ($smileys !== false && $tag === null && isset($message[$pos + 2]) && isset($itemcodes[$message[$pos + 1]]) && $message[$pos + 2] === ']' && !isset($disabled['list']) && !isset($disabled['li'])) |
|
{ |
|
if ($message[$pos + 1] == '0' && !in_array($message[$pos - 1], array(';', ' ', "\t", "\n", '>'))) |
|
{ |
|
continue; |
|
} |
|
|
|
$tag = $itemcodes[$message[$pos + 1]]; |
|
|
|
// First let's set up the tree: it needs to be in a list, or after an li. |
|
if ($inside === null || ($inside['tag'] !== 'list' && $inside['tag'] !== 'li')) |
|
{ |
|
$open_tags[] = array( |
|
'tag' => 'list', |
|
'after' => '</ul>', |
|
'block_level' => true, |
|
'require_children' => array('li'), |
|
'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null, |
|
); |
|
$code = '<ul' . ($tag === '' ? '' : ' style="list-style-type: ' . $tag . '"') . ' class="bbc_list">'; |
|
} |
|
// We're in a list item already: another itemcode? Close it first. |
|
elseif ($inside['tag'] === 'li') |
|
{ |
|
array_pop($open_tags); |
|
$code = '</li>'; |
|
} |
|
else |
|
$code = ''; |
|
|
|
// Now we open a new tag. |
|
$open_tags[] = array( |
|
'tag' => 'li', |
|
'after' => '</li>', |
|
'trim' => 'outside', |
|
'block_level' => true, |
|
'disallow_children' => isset($inside['disallow_children']) ? $inside['disallow_children'] : null, |
|
); |
|
|
|
// First, open the tag... |
|
$code .= '<li>'; |
|
$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos + 3); |
|
$pos += strlen($code) - 1 + 2; |
|
|
|
// Next, find the next break (if any.) If there's more itemcode after it, keep it going - otherwise close! |
|
$pos2 = strpos($message, '<br />', $pos); |
|
$pos3 = strpos($message, '[/', $pos); |
|
|
|
if ($pos2 !== false && ($pos2 <= $pos3 || $pos3 === false)) |
|
{ |
|
preg_match('~^(<br />| |\s|\[)+~', substr($message, $pos2 + 6), $matches); |
|
$message = substr_replace($message, (!empty($matches[0]) && substr($matches[0], -1) === '[' ? '[/li]' : '[/li][/list]'), $pos2, 0); |
|
$open_tags[count($open_tags) - 2]['after'] = '</ul>'; |
|
} |
|
// Tell the [list] that it needs to close specially. |
|
else |
|
{ |
|
// Move the li over, because we're not sure what we'll hit. |
|
$open_tags[count($open_tags) - 1]['after'] = ''; |
|
$open_tags[count($open_tags) - 2]['after'] = '</li></ul>'; |
|
} |
|
|
|
continue; |
|
} |
|
|
|
// Implicitly close lists and tables if something other than what's required is in them. This is needed for itemcode. |
|
if ($tag === null && $inside !== null && !empty($inside['require_children'])) |
|
{ |
|
array_pop($open_tags); |
|
|
|
$message = substr_replace($message, "\n" . $inside['after'] . "\n", $pos, 0); |
|
|
|
$pos += strlen($inside['after']) - 1 + 2; |
|
} |
|
|
|
// No tag? Keep looking, then. Silly people using brackets without actual tags. |
|
if ($tag === null) |
|
continue; |
|
|
|
// Propagate the list to the child (so wrapping the disallowed tag won't work either.) |
|
if (isset($inside['disallow_children'])) |
|
$tag['disallow_children'] = isset($tag['disallow_children']) ? array_unique(array_merge($tag['disallow_children'], $inside['disallow_children'])) : $inside['disallow_children']; |
|
|
|
// Is this tag disabled? |
|
if (isset($disabled[$tag['tag']])) |
|
{ |
|
if (!isset($tag['disabled_before']) && !isset($tag['disabled_after']) && !isset($tag['disabled_content'])) |
|
{ |
|
$tag['before'] = !empty($tag['block_level']) ? '<div>' : ''; |
|
$tag['after'] = !empty($tag['block_level']) ? '</div>' : ''; |
|
$tag['content'] = isset($tag['type']) && $tag['type'] === 'closed' ? '' : (!empty($tag['block_level']) ? '<div>$1</div>' : '$1'); |
|
} |
|
elseif (isset($tag['disabled_before']) || isset($tag['disabled_after'])) |
|
{ |
|
$tag['before'] = isset($tag['disabled_before']) ? $tag['disabled_before'] : (!empty($tag['block_level']) ? '<div>' : ''); |
|
$tag['after'] = isset($tag['disabled_after']) ? $tag['disabled_after'] : (!empty($tag['block_level']) ? '</div>' : ''); |
|
} |
|
else |
|
$tag['content'] = $tag['disabled_content']; |
|
} |
|
|
|
// We use this alot |
|
$tag_strlen = strlen($tag['tag']); |
|
|
|
// The only special case is 'html', which doesn't need to close things. |
|
if (!empty($tag['block_level']) && $tag['tag'] !== 'html' && empty($inside['block_level'])) |
|
{ |
|
$n = count($open_tags) - 1; |
|
while (empty($open_tags[$n]['block_level']) && $n >= 0) |
|
$n--; |
|
|
|
// Close all the non block level tags so this tag isn't surrounded by them. |
|
for ($i = count($open_tags) - 1; $i > $n; $i--) |
|
{ |
|
$message = substr_replace($message, "\n" . $open_tags[$i]['after'] . "\n", $pos, 0); |
|
$ot_strlen = strlen($open_tags[$i]['after']); |
|
$pos += $ot_strlen + 2; |
|
$pos1 += $ot_strlen + 2; |
|
|
|
// Trim or eat trailing stuff... see comment at the end of the big loop. |
|
if (!empty($open_tags[$i]['block_level']) && substr_compare($message, '<br />', $pos, 6) === 0) |
|
$message = substr_replace($message, '', $pos, 6); |
|
if (!empty($open_tags[$i]['trim']) && $tag['trim'] !== 'inside' && preg_match('~(<br />| |\s)*~', substr($message, $pos), $matches) !== 0) |
|
$message = substr($message, 0, $pos) . substr($message, $pos + strlen($matches[0])); |
|
|
|
array_pop($open_tags); |
|
} |
|
} |
|
|
|
// No type means 'parsed_content'. |
|
if (!isset($tag['type'])) |
|
{ |
|
// @todo Check for end tag first, so people can say "I like that [i] tag"? |
|
$open_tags[] = $tag; |
|
$message = substr($message, 0, $pos) . "\n" . $tag['before'] . "\n" . substr($message, $pos1); |
|
$pos += strlen($tag['before']) - 1 + 2; |
|
} |
|
// Don't parse the content, just skip it. |
|
elseif ($tag['type'] === 'unparsed_content') |
|
{ |
|
$pos2 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos1); |
|
if ($pos2 === false) |
|
continue; |
|
|
|
$data = substr($message, $pos1, $pos2 - $pos1); |
|
|
|
if (!empty($tag['block_level']) && substr($data, 0, 6) === '<br />') |
|
{ |
|
$data = substr($data, 6); |
|
} |
|
|
|
if (isset($tag['validate'])) |
|
$tag['validate']($tag, $data, $disabled); |
|
|
|
$code = strtr($tag['content'], array('$1' => $data)); |
|
$message = substr_replace($message, "\n" . $code . "\n", $pos, $pos2 + 3 + $tag_strlen - $pos); |
|
|
|
$pos += strlen($code) - 1 + 2; |
|
$last_pos = $pos + 1; |
|
|
|
} |
|
// Don't parse the content, just skip it. |
|
elseif ($tag['type'] === 'unparsed_equals_content') |
|
{ |
|
// The value may be quoted for some tags - check. |
|
if (isset($tag['quoted'])) |
|
{ |
|
$quoted = substr($message, $pos1, 6) === '"'; |
|
if ($tag['quoted'] !== 'optional' && !$quoted) |
|
continue; |
|
|
|
if ($quoted) |
|
$pos1 += 6; |
|
} |
|
else |
|
$quoted = false; |
|
|
|
$pos2 = strpos($message, $quoted === false ? ']' : '"]', $pos1); |
|
if ($pos2 === false) |
|
continue; |
|
|
|
$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2); |
|
if ($pos3 === false) |
|
continue; |
|
|
|
$data = array( |
|
substr($message, $pos2 + ($quoted === false ? 1 : 7), $pos3 - ($pos2 + ($quoted === false ? 1 : 7))), |
|
substr($message, $pos1, $pos2 - $pos1) |
|
); |
|
|
|
if (!empty($tag['block_level']) && substr($data[0], 0, 6) === '<br />') |
|
$data[0] = substr($data[0], 6); |
|
|
|
// Validation for my parking, please! |
|
if (isset($tag['validate'])) |
|
$tag['validate']($tag, $data, $disabled); |
|
|
|
$code = strtr($tag['content'], array('$1' => $data[0], '$2' => $data[1])); |
|
$message = substr_replace($message, "\n" . $code . "\n", $pos, $pos3 + 3 + $tag_strlen - $pos); |
|
$pos += strlen($code) - 1 + 2; |
|
} |
|
// A closed tag, with no content or value. |
|
elseif ($tag['type'] === 'closed') |
|
{ |
|
$pos2 = strpos($message, ']', $pos); |
|
$message = substr($message, 0, $pos) . "\n" . $tag['content'] . "\n" . substr($message, $pos2 + 1); |
|
$pos += strlen($tag['content']) - 1 + 2; |
|
} |
|
// This one is sorta ugly... :/ |
|
elseif ($tag['type'] === 'unparsed_commas_content') |
|
{ |
|
$pos2 = strpos($message, ']', $pos1); |
|
if ($pos2 === false) |
|
continue; |
|
|
|
$pos3 = stripos($message, '[/' . substr($message, $pos + 1, $tag_strlen) . ']', $pos2); |
|
if ($pos3 === false) |
|
continue; |
|
|
|
// We want $1 to be the content, and the rest to be csv. |
|
$data = explode(',', ',' . substr($message, $pos1, $pos2 - $pos1)); |
|
$data[0] = substr($message, $pos2 + 1, $pos3 - $pos2 - 1); |
|
|
|
if (isset($tag['validate'])) |
|
$tag['validate']($tag, $data, $disabled); |
|
|
|
$code = $tag['content']; |
|
foreach ($data as $k => $d) |
|
$code = strtr($code, array('$' . ($k + 1) => trim($d))); |
|
$message = substr($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen); |
|
$pos += strlen($code) - 1 + 2; |
|
} |
|
// This has parsed content, and a csv value which is unparsed. |
|
elseif ($tag['type'] === 'unparsed_commas') |
|
{ |
|
$pos2 = strpos($message, ']', $pos1); |
|
if ($pos2 === false) |
|
continue; |
|
|
|
$data = explode(',', substr($message, $pos1, $pos2 - $pos1)); |
|
|
|
if (isset($tag['validate'])) |
|
$tag['validate']($tag, $data, $disabled); |
|
|
|
// Fix after, for disabled code mainly. |
|
foreach ($data as $k => $d) |
|
$tag['after'] = strtr($tag['after'], array('$' . ($k + 1) => trim($d))); |
|
|
|
$open_tags[] = $tag; |
|
|
|
// Replace them out, $1, $2, $3, $4, etc. |
|
$code = $tag['before']; |
|
foreach ($data as $k => $d) |
|
$code = strtr($code, array('$' . ($k + 1) => trim($d))); |
|
$message = substr_replace($message, "\n" . $code . "\n", $pos, $pos2 + 1 - $pos); |
|
$pos += strlen($code) - 1 + 2; |
|
} |
|
// A tag set to a value, parsed or not. |
|
elseif ($tag['type'] === 'unparsed_equals' || $tag['type'] === 'parsed_equals') |
|
{ |
|
// The value may be quoted for some tags - check. |
|
if (isset($tag['quoted'])) |
|
{ |
|
$quoted = substr($message, $pos1, 6) === '"'; |
|
if ($tag['quoted'] !== 'optional' && !$quoted) |
|
continue; |
|
|
|
if ($quoted) |
|
$pos1 += 6; |
|
} |
|
else |
|
$quoted = false; |
|
|
|
$pos2 = strpos($message, $quoted === false ? ']' : '"]', $pos1); |
|
if ($pos2 === false) |
|
continue; |
|
|
|
$data = substr($message, $pos1, $pos2 - $pos1); |
|
|
|
// Validation for my parking, please! |
|
if (isset($tag['validate'])) |
|
$tag['validate']($tag, $data, $disabled); |
|
|
|
// For parsed content, we must recurse to avoid security problems. |
|
if ($tag['type'] !== 'unparsed_equals') |
|
{ |
|
$data = parse_bbc($data, !empty($tag['parsed_tags_allowed']) ? false : true, '', !empty($tag['parsed_tags_allowed']) ? $tag['parsed_tags_allowed'] : array()); |
|
|
|
// Unfortunately after we recurse, we must manually reset the static disabled tags to what they were |
|
parse_bbc('dummy'); |
|
} |
|
|
|
$tag['after'] = strtr($tag['after'], array('$1' => $data)); |
|
|
|
$open_tags[] = $tag; |
|
|
|
$code = strtr($tag['before'], array('$1' => $data)); |
|
$message = substr_replace($message, "\n" . $code . "\n", $pos, $pos2 + ($quoted === false ? 1 : 7) - $pos); |
|
|
|
$pos += strlen($code) - 1 + 2; |
|
} |
|
|
|
// If this is block level, eat any breaks after it. |
|
if (!empty($tag['block_level']) && isset($message[$pos + 1]) && substr_compare($message, '<br />', $pos + 1, 6) === 0) |
|
$message = substr_replace($message, '', $pos + 1, 6); |
|
|
|
// Are we trimming outside this tag? |
|
if (!empty($tag['trim']) && $tag['trim'] !== 'outside' && preg_match('~(<br />| |\s)*~', substr($message, $pos + 1), $matches) !== 0) |
|
$message = substr($message, 0, $pos + 1) . substr($message, $pos + 1 + strlen($matches[0])); |
|
} |
|
|
|
// Close any remaining tags. |
|
while ($tag = array_pop($open_tags)) |
|
$message .= "\n" . $tag['after'] . "\n"; |
|
|
|
// Parse the smileys within the parts where it can be done safely. |
|
if ($smileys === true) |
|
{ |
|
$message_parts = explode("\n", $message); |
|
for ($i = 0, $n = count($message_parts); $i < $n; $i += 2) |
|
parsesmileys($message_parts[$i]); |
|
|
|
$message = implode('', $message_parts); |
|
} |
|
|
|
// No smileys, just get rid of the markers. |
|
else |
|
$message = strtr($message, array("\n" => '')); |
|
|
|
if (isset($message[0]) && $message[0] === ' ') |
|
$message = ' ' . substr($message, 1); |
|
|
|
// Cleanup whitespace. |
|
$message = strtr($message, array(' ' => ' ', "\r" => '', "\n" => '<br />', '<br /> ' => '<br /> ', ' ' => "\n")); |
|
|
|
// Finish footnotes if we have any. |
|
if (strpos($message, '<sup class="bbc_footnotes">') !== false) |
|
{ |
|
global $fn_num, $fn_content, $fn_count; |
|
static $fn_total; |
|
|
|
// @todo temporary until we have nesting |
|
$message = str_replace(array('[footnote]', '[/footnote]'), '', $message); |
|
|
|
$fn_num = 0; |
|
$fn_content = array(); |
|
$fn_count = isset($fn_total) ? $fn_total : 0; |
|
|
|
// Replace our footnote text with a [1] link, save the text for use at the end of the message |
|
$message = preg_replace_callback('~(%fn%(.*?)%fn%)~is', 'footnote_callback', $message); |
|
$fn_total += $fn_num; |
|
|
|
// If we have footnotes, add them in at the end of the message |
|
if (!empty($fn_num)) |
|
$message .= '<div class="bbc_footnotes">' . implode('', $fn_content) . '</div>'; |
|
} |
|
|
|
// Allow addons access to what parse_bbc created |
|
call_integration_hook('integrate_post_parsebbc', array(&$message, &$smileys, &$cache_id, &$parse_tags)); |
|
|
|
// Cache the output if it took some time... |
|
if (isset($cache_key, $cache_t) && microtime(true) - $cache_t > 0.05) |
|
cache_put_data($cache_key, $message, 240); |
|
|
|
// If this was a force parse revert if needed. |
|
if (!empty($parse_tags)) |
|
{ |
|
if (empty($temp_bbc)) |
|
$bbc_codes = array(); |
|
else |
|
{ |
|
$bbc_codes = $temp_bbc; |
|
unset($temp_bbc); |
|
} |
|
} |
|
|
|
return $message; |
|
} |