public function parse($message) { // Don't waste cycles if ($message === '') { return ''; } // Clean up any cut/paste issues we may have $message = sanitizeMSCutPaste($message); // @todo change this to <br> (it will break tests) $message = str_replace("\n", '<br />', $message); // @todo test to see if searching for '[' here makes sense // if (strpos($this->message, '[') === false) // return $this->message; // I guess if we made it this far... $this->message = $message; //$msg_parts = $this->tokenize($message); return $this->parseTokens($this->tokenize($message)); }
/** * Parse the BBC in a string/message * * @param string $message * * @return string */ public function parse($message) { // The parser allows you to check some things about the message. // If you move this later, you might be talking about the last message. $this->resetParser(); $this->triggerEvent('pre_parsebbc', array(&$message, $this->bbc)); // Don't waste cycles if ($message === '') { return ''; } // Clean up any cut/paste issues we may have $message = sanitizeMSCutPaste($message); $this->message = $message; unset($message); // @todo change this to <br> (it will break tests) $this->message = str_replace("\n", '<br />', $this->message); // Check if the message might have a link or email to save a bunch of parsing in autolink() if ($this->autolink_enabled) { $this->autolinker->setPossibleAutolink($this->message); } $this->possible_html = $this->html_enabled && strpos($this->message, '<') !== false; // Don't load the HTML Parser unless we have to if ($this->possible_html && $this->html_parser === null) { $this->loadHtmlParser(); } // This handles pretty much all of the parsing. It is a separate method so it is easier to override and profile. $this->parse_loop(); // Close any remaining tags. while ($tag = $this->closeOpenedTag()) { $this->message .= $this->noSmileys($tag[Codes::ATTR_AFTER]); } if (isset($this->message[0]) && $this->message[0] === ' ') { $this->message = substr_replace($this->message, ' ', 0, 1); } // Cleanup whitespace. $this->message = str_replace(array(' ', '<br /> ', ' '), array(' ', '<br /> ', "\n"), $this->message); // Finish footnotes if we have any. if ($this->num_footnotes > 0) { $this->handleFootnotes(); } // Allow addons access to what the parser created $message = $this->message; $this->triggerEvent('post_parsebbc', array(&$message)); $this->message = $message; return $this->message; }
protected function cleanMessage() { $this->message = Util::htmlspecialchars($this->message, ENT_QUOTES, 'UTF-8', true); $this->message = strtr($this->message, array("\r" => '', '[]' => '[]', '['' => '['')); // Clean up any cut/paste issues we may have $this->message = sanitizeMSCutPaste($this->message); }
/** * 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 variables can't be touched by the benchmark script. // Instead of doing eval() (which I should do), I am just going to make them globals and access them like that. //static $bbc_codes = array(), $itemcodes = array(), $no_autolink_tags = array(); //static $disabled, $default_disabled, $parse_tag_cache; global $bbc_codes, $itemcodes, $no_autolink_tags; global $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(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', '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(''' => '\'', ' ' => " ", '"' => '>">', '"' => '<"<', '<' => '<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('\'' => ''', " " => ' ', '>">' => '"', '<"<' => '"', '<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($message, 0, $pos) . "\n" . $tag['after'] . "\n" . substr($message, $pos2 + 1); $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($message, 0, $pos) . substr($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 (strtolower(substr($message, $pos + 1, $pt_strlen)) != $possible['tag']) { 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; } elseif (!empty($possible['parameters'])) { if ($next_c != ' ') { continue; } } elseif (isset($possible['type'])) { // Do we need an equal sign? if (in_array($possible['type'], array('unparsed_equals', 'unparsed_commas', 'unparsed_commas_content', 'unparsed_equals_content', 'parsed_equals')) && $next_c != '=') { 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; } } 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; } elseif (isset($inside['disallow_children']) && in_array($possible['tag'], $inside['disallow_children'])) { continue; } 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']) ? $possible['disallow_before'] : $possible['before']; $possible['after'] = isset($possible['disallow_after']) ? $possible['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. $match = false; $orders = permute($preg); foreach ($orders as $p) { if (preg_match('~^' . implode('', $p) . '\\]~i', substr($message, $pos1 - 1), $matches) != 0) { $match = true; break; } } // 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">'; } 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($message, 0, $pos2) . (!empty($matches[0]) && substr($matches[0], -1) == '[' ? '[/li]' : '[/li][/list]') . substr($message, $pos2); $open_tags[count($open_tags) - 2]['after'] = '</ul>'; } 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($message, 0, $pos) . "\n" . $inside['after'] . "\n" . substr($message, $pos); $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($message, 0, $pos) . "\n" . $open_tags[$i]['after'] . "\n" . substr($message, $pos); $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($message, $pos, 6) == '<br />') { $message = substr($message, 0, $pos) . substr($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; } 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($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 3 + $tag_strlen); $pos += strlen($code) - 1 + 2; $last_pos = $pos + 1; } 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($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos3 + 3 + $tag_strlen); $pos += strlen($code) - 1 + 2; } 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; } 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; } 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($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + 1); $pos += strlen($code) - 1 + 2; } 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($message, 0, $pos) . "\n" . $code . "\n" . substr($message, $pos2 + ($quoted == false ? 1 : 7)); $pos += strlen($code) - 1 + 2; } // If this is block level, eat any breaks after it. if (!empty($tag['block_level']) && substr($message, $pos + 1, 6) === '<br />') { $message = substr($message, 0, $pos + 1) . substr($message, $pos + 7); } // 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); } 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; }