* Invision Power Services * IP.Board v3.4.8 * BBCode parsing core - common methods * Last Updated: $Date: 2012-06-08 09:28:02 +0100 (Fri, 08 Jun 2012) $ * * * @author $Author: mmecham $ * @copyright (c) 2001 - 2009 Invision Power Services, Inc. * @license http://www.invisionpower.com/company/standards.php#license * @package IP.Board * @link http://www.invisionpower.com * @since 9th March 2005 11:03 * @version $Revision: 10894 $ * * Revised in 3.4 by Matt Mecham * Refactored into a text parsing module * */ if ( ! defined( 'IN_IPB' ) ) { print "

Incorrect access

You cannot access this file directly. If you have recently upgraded, make sure you upgraded all the relevant files."; exit(); } /** * @author matt * */ class class_text_parser_bbcode extends classes_text_parser { /** * Strip quotes? * Strips quotes from the resulting text * * @access public * @var boolean */ public $strip_quotes = false; /** * Auto convert newlines to html line breaks * * @access public * @var boolean */ public $parse_nl2br = true; /** * Section keyword for parsing area * * @access public * @var string */ public $parseType = 'post'; /** * Allow unicode (parses escaped entities to actual entities) * * @access public * @var boolean */ public $allow_unicode = false; /** * Maximum number of embeded quotes * * @access public * @var integer */ public $max_embed_quotes = 15; /** * Error code stored * * @access public * @var string */ public $error = ''; /** * Warning code stored (warning is like an error but will not stop parsing execution) * * @access public * @var string */ public $warning = ''; /** * Number of images we've parsed so far * * @access protected * @var integer */ protected $image_count = 0; /** * Number of emoticons we've parsed so far * * @access protected * @var integer */ protected $emoticon_count = 0; /** * Array of emoticon alt tags * * @access protected * @var array */ protected $emoticon_alts = array(); /** * Registry object * * @access protected * @var object */ protected $registry; /** * Database object * * @access protected * @var object */ protected $DB; /** * Settings object * * @access protected * @var object */ protected $settings; /** * Request object * * @access protected * @var object */ protected $request; /** * Language object * * @access protected * @var object */ protected $lang; /** * Member object * * @access protected * @var object */ protected $member; protected $memberData; /** * Cache object * * @access protected * @var object */ protected $cache; protected $caches; /** * BBCodes we should parse. * Takes into account what section we are in, and our group. * * @access protected * @var array */ protected $_bbcodes = array(); /** * Plugin objects * * @access protected * @var array Holds plugin objects */ protected $plugins = array(); /** * Current position in the text document * * @access protected * @var integer */ protected $cur_pos = 0; /** * Multi-dimensional array of bbcodes not being parsed inside * * @access protected * @var array */ protected $noParseStorage = array(); /** * Identifier for replacement * * @access protected * @var integer */ protected $_storedNoParsing = 0; /** * Emoticon code * * @access protected * @var string */ protected $_emoCode = ''; /** * Emoticon image * * @access protected * @var string */ protected $_emoImage = ''; protected $_mediaUrlConverted = array(); protected $_sortedSmilies = array(); protected $_isConvertingForEditor = null; protected $_urlsEnabled = true; /** * Build in BBCode we can convert from CKEditor safely * @param array */ private $_builtInBBCode = array( 'b', 'i', 'u', 's', 'strike', 'font', 'size', 'color', 'background', 'sup', 'sub', 'list', 'url', 'img', 'center', 'left', 'right', 'indent', 'email', 'code', 'quote'); /** * Block level BBCodes * CKEditor wraps [tag] in P tags so we need to strip these * @param array */ private $_blockBBcode = array( 'spoiler' ); /** * Constructor * * @access public * @param object Registry object * @return @e void */ public function __construct() { /* Make object */ $this->registry = ipsRegistry::instance(); $this->DB = $this->registry->DB(); $this->settings =& $this->registry->fetchSettings(); $this->request =& $this->registry->fetchRequest(); $this->lang = $this->registry->getClass('class_localization'); $this->member = $this->registry->member(); $this->memberData =& $this->registry->member()->fetchMemberData(); $this->cache = $this->registry->cache(); $this->caches =& $this->registry->cache()->fetchCaches(); /* Load and init BBCodes */ foreach( $this->cache->getCache('bbcode') as $bbcode ) { /* Allowed this BBCode? */ if ( $bbcode['bbcode_sections'] != 'all' && parent::$Perms['parseArea'] != 'global' ) { $pass = false; $sections = explode( ',', $bbcode['bbcode_sections'] ); foreach( $sections as $section ) { if( $section == parent::$Perms['parseArea'] ) { $pass = true; break; } } if ( ! $pass ) { continue; } } /* Cheat a bit */ if ( in_array( $bbcode['bbcode_tag'], array( 'code', 'acronym', 'img' ) ) ) { $bbcode['bbcode_no_auto_url_parse'] = 1; } /* Store */ $this->_bbcodes[ $bbcode['bbcode_tag'] ] = $bbcode; } /* Can we parse URLs */ if ( isset( $this->_bbcodes['url'] ) ) { /* Allowed to use this? */ if ( $this->_bbcodes['url']['bbcode_groups'] != 'all' and parent::$Perms['memberData']['member_group_id'] ) { $pass = false; $groups = array_diff( explode( ',', $this->_bbcodes['url']['bbcode_groups'] ), array( '' ) ); $mygroups = array( parent::$Perms['memberData']['member_group_id'] ); if ( parent::$Perms['memberData']['mgroup_others'] ) { $mygroups = array_diff( array_merge( $mygroups, explode( ',', IPSText::cleanPermString( parent::$Perms['memberData']['mgroup_others'] ) ) ), array( '' ) ); } foreach( $groups as $g_id ) { if ( in_array( $g_id, $mygroups ) ) { $pass = true; break; } } if ( ! $pass ) { $this->_urlsEnabled = false; } } } /* Check for emoticons */ if ( ! $this->cache->exists('emoticons') ) { $emoticons = $this->cache->getCache('emoticons'); /* Fallback on recache */ if ( ! is_array($emoticons) OR ! count( $emoticons ) ) { $this->cache->rebuildCache( 'emoticons', 'global' ); } } } /** * Takes plain unparsed BBCode (not HTML) and converts it to HTML for storing in the DB * (Common codes parsed, URLs parsed, QUOTE/CODE/App specific not parsed) * * @access public * @param string Raw text * @return string Converted text */ public function toHtml( $txt="" ) { /* Reset */ $this->_resetPointers(); $this->cache->updateCacheWithoutSaving( '_tmp_bbcode_media', 0 ); $this->cache->updateCacheWithoutSaving( '_tmp_bbcode_images', 0 ); /* Fix no parsable */ $txt = $this->_processNoParseCodes( $txt ); $txt = str_replace( array( "\r\n", "\r" ), "\n", $txt ); /* Remove session IDs */ $txt = preg_replace_callback( '#(\?|&|;|&)s=([0-9a-zA-Z]){32}(&|;|&|$)?#', array( $this, '_bashSession' ), $txt ); /* Preserve newlines in codeboxes */ $txt = $this->_preserveCodeBoxes( $txt ); /* Fix newlines */ if ( ! strstr( $txt, "\n" ) && ( stristr( $txt, '
' ) || stristr( $txt, '
' ) ) ) { $txt = str_ireplace( array( '
', '
' ), "\n", $txt ); } else { if ( stristr( $txt, '
' ) || stristr( $txt, '
' ) ) { $txt = str_replace( "\n", "", $txt ); } } /* Stop direction swapping */ $txt = str_replace( "‪", '', $txt ); $txt = str_replace( "‫", '', $txt ); $txt = str_replace( "‬", '', $txt ); $txt = str_replace( "‭", '', $txt ); $txt = str_replace( "‮", '', $txt ); /* Fix old expression "protection" */ $txt = str_replace( "expression", "expression", $txt ); /* Convert to BR, but only if we're not working towards an editor */ $txt = nl2br( $txt ); /* Restore preserved newlines */ $txt = str_replace( "", "\n", $txt ); /* BBCodes to parse */ $bbcodes = $this->_builtInBBCode; /* Parse BBCode */ if ( parent::$Perms['parseBBCode'] ) { $txt = $this->_parseBBCode( $txt, 'html', $bbcodes ); } /* Tidy up */ $txt = str_replace( '


', '

', $txt ); $txt = str_replace( '
', '', $txt ); $txt = str_replace( '
', '', $txt ); $txt = str_replace( '
', '', $txt ); $txt = str_replace( '


', '

', $txt ); // Don't auto-link media. parent::$NoMediaAutoLinkify = true; $txt = $this->_autoLinkUrls( $txt ); // Reset parent::$NoMediaAutoLinkify = false; return $txt; } /** * Finish parsing BBCode for display * * @param string $txt * @return string */ public function toDisplay( $txt ) { /* Init */ $_storedLinks = array(); $_counter = 0; $_bbcodes = array(); /* Reset */ $this->cache->updateCacheWithoutSaving( '_tmp_bbcode_media', 0 ); /* Fix no parsable, second parse */ $txt = $this->_processNoParseCodes( $txt, 2 ); /* Get BBCode to parse.. and then do it! */ foreach( $this->_bbcodes as $tag => $data ) { if ( in_array( $tag, $this->_builtInBBCode ) ) { continue; } else { $_bbcodes[] = $tag; } } /* Clean up block level BBCode */ $txt = $this->_cleanBlockBBCode( $txt ); /* Parse BBCode */ if ( parent::$Perms['parseBBCode'] ) { $txt = $this->_parseBBCode( $txt, 'display', $_bbcodes ); } /* Finish URLS */ $txt = $this->_finishUrlsForDisplay( $txt ); /* Fix up URLs made safe previously */ $txt = $this->_processNoParseCodes( $txt, 3 ); return $txt; } /** * Loop over the bbcode and make replacements as necessary * * @access public * @param string Current text * @return string text */ protected function _parseBBCode( $txt, $mode='html', $bbcodes=array() ) { // ----------------------------------------- // We want preDbParse method called for shared // media for permission checking, so force it for now.. // ----------------------------------------- //$this->_bbcodes[$cur_method]['sharedmedia'] = $this->_bbcodes['display']['sharedmedia']; /* Replace them */ if ( count( $this->_bbcodes ) ) { foreach( $this->_bbcodes as $_tag => $_bbcode ) { /* Check to see if we can parse these */ if ( is_array( $bbcodes ) and count( $bbcodes ) ) { if ( ! in_array( $_bbcode['bbcode_tag'], $bbcodes ) ) { continue; } } /* Allowed to use this? */ if ( $_bbcode['bbcode_groups'] != 'all' and parent::$Perms['memberData']['member_group_id'] ) { $pass = false; $groups = array_diff( explode( ',', $_bbcode['bbcode_groups'] ), array( '' ) ); $mygroups = array( parent::$Perms['memberData']['member_group_id'] ); if ( parent::$Perms['memberData']['mgroup_others'] ) { $mygroups = array_diff( array_merge( $mygroups, explode( ',', IPSText::cleanPermString( parent::$Perms['memberData']['mgroup_others'] ) ) ), array( '' ) ); } foreach( $groups as $g_id ) { if ( in_array( $g_id, $mygroups ) ) { $pass = true; break; } } if ( ! $pass ) { continue; } } // ----------------------------------------- // Reset our current position // ----------------------------------------- $this->cur_pos = 0; // ----------------------------------------- // Store teh tags // ----------------------------------------- $_tags = array( $_bbcode['bbcode_tag'] ); // ----------------------------------------- // We'll also need to check for any aliases // ----------------------------------------- if ( $_bbcode['bbcode_aliases'] ) { $aliases = explode( ',', trim( $_bbcode['bbcode_aliases'] ) ); if ( is_array( $aliases ) and count( $aliases ) ) { foreach( $aliases as $alias ) { $_tags[] = trim( $alias ); } } } // ----------------------------------------- // If we have a plugin, just pass off // ----------------------------------------- if ( $_bbcode['bbcode_php_plugin'] ) { $file = IPS_ROOT_PATH . 'sources/classes/text/parser/bbcode/' . $_bbcode['bbcode_php_plugin']; $_key = md5( $_bbcode['bbcode_tag'] ); /* The legacy 'list' bbcode tag replaces \n out of necessity, however this breaks legacy code boxes upon upgrade. Ticket 853240 */ if( $_bbcode['bbcode_tag'] == 'list' ) { /* Preserve newlines in codeboxes */ $txt = $this->_preserveCodeBoxes( $txt ); } // ----------------------------------------- // Do we already have this plugin in our registry? // ----------------------------------------- if ( isset( $this->plugins[$_key] ) ) { // ----------------------------------------- // Run the method if it exists // ----------------------------------------- if ( method_exists( $this->plugins[$_key], 'run' ) ) { $_original = $txt; $txt = $this->plugins[$_key]->run( $txt, $mode ); if ( ! $txt ) { $txt = $_original; } else if ( $this->plugins[$_key]->error ) { $this->_addParsingError( $this->plugins[$_key]->error ); if( $_bbcode['bbcode_tag'] == 'list' ) { /* Restore preserved newlines */ $txt = str_replace( "", "\n", $txt ); } continue; } else if ( $this->plugins[$_key]->warning ) { $this->warning = $this->plugins[$_key]->warning; } } } // ----------------------------------------- // First time we've called this plugin // ----------------------------------------- elseif ( is_file( $file ) ) { $_classname = IPSLib::loadLibrary( $file, 'bbcode_plugin_' . IPSText::alphanumericalClean( $_bbcode['bbcode_tag'] ) ); // ----------------------------------------- // Class we need exists // ----------------------------------------- if ( class_exists( $_classname ) ) { // ----------------------------------------- // New instance of class, store in plugin registry // for use next time // ----------------------------------------- $plugin = new $_classname( $this->registry, $this ); $this->plugins[md5( $_bbcode['bbcode_tag'] )] = $plugin; // ----------------------------------------- // Method we need exists // ----------------------------------------- if ( method_exists( $plugin, 'run' ) ) { $_original = $txt; $txt = $plugin->run( $txt, $mode ); if ( ! $txt ) { $txt = $_original; } else if ( $plugin->error ) { $this->_addParsingError( $plugin->error ); if( $_bbcode['bbcode_tag'] == 'list' ) { /* Restore preserved newlines */ $txt = str_replace( "", "\n", $txt ); } continue; } else if ( $plugin->warning ) { $this->warning = $plugin->warning; } } } } // ----------------------------------------- // When we run a plugin, we don't do any other processing // "automatically". // Plugin is capable of doing what it wants that way. // ----------------------------------------- if( $_bbcode['bbcode_tag'] == 'list' ) { /* Restore preserved newlines */ $txt = str_replace( "", "\n", $txt ); } continue; } // ----------------------------------------- // Loop over this bbcode's tags // ----------------------------------------- foreach( $_tags as $_tag ) { // ----------------------------------------- // Infinite loop catcher // ----------------------------------------- $_iteration = 0; // ----------------------------------------- // Start building open tag // ----------------------------------------- $open_tag = '[' . $_tag; // ----------------------------------------- // Doz I can haz opin tag? Loopy loo // ----------------------------------------- while ( ( $this->cur_pos = stripos( $txt, $open_tag, $this->cur_pos ) ) !== false ) { // ----------------------------------------- // Stop infinite loops // ----------------------------------------- if ( $_iteration > $this->settings['max_bbcodes_per_post'] ) { break; } $open_length = strlen( $open_tag ); // ----------------------------------------- // Grab the new position to jump to // ----------------------------------------- $new_pos = strpos( $txt, ']', $this->cur_pos ) ? strpos( $txt, ']', $this->cur_pos ) : $this->cur_pos + 1; // ----------------------------------------- // Extract the option (like surgery) // ----------------------------------------- $_option = ''; if ( $_bbcode['bbcode_useoption'] ) { // ----------------------------------------- // Is option optional? // ----------------------------------------- if ( $_bbcode['bbcode_optional_option'] ) { // ----------------------------------------- // Does we haz it? // ----------------------------------------- if ( substr( $txt, $this->cur_pos + strlen( $open_tag ), 1 ) == '=' ) { $open_length += 1; $_option = substr( $txt, $this->cur_pos + $open_length, ( strpos( $txt, ']', $this->cur_pos ) - ( $this->cur_pos + $open_length ) ) ); } // ----------------------------------------- // If not, [u] != [url] (for example) // ----------------------------------------- else if ( ( strpos( $txt, ']', $this->cur_pos ) - ( $this->cur_pos + $open_length ) ) !== 0 ) { if ( strpos( $txt, ']', $this->cur_pos ) ) { $this->cur_pos = $new_pos; continue; } else { break; } } } // ----------------------------------------- // No? Then just grab it // ----------------------------------------- else { $open_length += 1; $_option = substr( $txt, $this->cur_pos + $open_length, ( strpos( $txt, ']', $this->cur_pos ) - ( $this->cur_pos + $open_length ) ) ); } } // ----------------------------------------- // [img] != [i] (for example) // ----------------------------------------- else if ( ( strpos( $txt, ']', $this->cur_pos ) - ( $this->cur_pos + $open_length ) ) !== 0 ) { if ( strpos( $txt, ']', $this->cur_pos ) ) { $this->cur_pos = $new_pos; continue; } } $_iteration ++; // ----------------------------------------- // Protect against XSS // ----------------------------------------- $_optionStrLen = IPSText::mbstrlen( $_option ); $_optionSlenstr = strlen( $_option ); $_option = $this->checkXss( $_option, false, $_tag ); if ( $_option !== FALSE ) { /* * Not parsing URls? - Needs to be AFTER the FALSE * check just above */ if ( ! empty( $_bbcode['bbcode_no_auto_url_parse'] ) ) { $_option = preg_replace( "#(http|https|news|ftp)://#i", "\\1://", $_option ); } // ----------------------------------------- // If this is a single tag, that's it // ----------------------------------------- if ( $_bbcode['bbcode_single_tag'] ) { $txt = substr_replace( $txt, $this->_parseBBCodeTag( $_bbcode, $_option, '' ), $this->cur_pos, ( $open_length + $_optionSlenstr + 1 ) ); } // ----------------------------------------- // Otherwise replace out the content too // ----------------------------------------- else { $close_tag = '[/' . $_tag . ']'; if ( stripos( $txt, $close_tag, $new_pos ) !== false ) { $_content = substr( $txt, ( $this->cur_pos + $open_length + $_optionSlenstr + 1 ), ( stripos( $txt, $close_tag, $this->cur_pos ) - ( $this->cur_pos + $open_length + $_optionSlenstr + 1 ) ) ); if ( $_bbcode['bbcode_useoption'] and $_bbcode['bbcode_optional_option'] and ! $_option and ! stristr( $_bbcode['bbcode_replace'], '{option}' ) ) { $_option = $_content; $_option = $this->checkXss( $_option, false, $_tag ); } /* Not parsing URls? */ if ( ! empty( $_bbcode['bbcode_no_auto_url_parse'] ) ) { $_content = preg_replace( "#(http|https|news|ftp)://#i", "\\1://", $_content ); } $txt = substr_replace( $txt, $this->_parseBBCodeTag( $_bbcode, $_option, $_content ), $this->cur_pos, ( stripos( $txt, $close_tag, $this->cur_pos ) + strlen( $close_tag ) - $this->cur_pos ) ); } else { // ----------------------------------------- // If there's no close tag, no need to // continue // ----------------------------------------- break; } } } // ----------------------------------------- // And reset current position to end of open tag // Bug 14744 - if we jump to $new_pos it can skip the // opening of the next bbcode tag // when the replacement HTML is shorter than the full // bbcode representation... // ----------------------------------------- $this->cur_pos = stripos( $txt, $open_tag ) ? stripos( $txt, $open_tag ) : $this->cur_pos + 1; // $new_pos; if ( $this->cur_pos > strlen( $txt ) ) { break; } } } } } return $txt; } /** * Does the actual bbcode replacement * * @access protected * @param string Current bbcode to parse * @param string [Optional] Option text * @param string [Optional for single tag bbcodes] Content text * @return string Converted text */ protected function _parseBBCodeTag( $_bbcode, $option='', $content='') { // ----------------------------------------- // Strip the optional quote delimiters // ----------------------------------------- $option = str_replace( '"', '"', $option ); $option = str_replace( '"', '"', $option ); $option = str_replace( ''', "'", $option ); $option = trim( $option, '"' . "'" ); // ----------------------------------------- // Stop CSS injection // ----------------------------------------- if ( $option ) { // ----------------------------------------- // Cut off for entities in option // @see // http://community.invisionpower.com/tracker/issue-19958-acronym/ // ----------------------------------------- $option = IPSText::UNhtmlspecialchars( $option ); $option = str_replace( '!', '!', $option ); /* http://community.invisionpower.com/resources/bugs.html/_/ip-board/error-in-accented-characters-in-the-option-tag-bbcode-r40739 */ $option = IPSText::convertNumericEntityToNamed( $option ); $option = IPSText::decodeNamedHtmlEntities( $option ); $option = IPSText::UNhtmlspecialchars( $option ); if ( strpos( $option, ';' ) !== false ) { $option = substr( $option, 0, strpos( $option, ';' ) ); } $charSet = ( IPS_DOC_CHAR_SET == 'ISO-8859-1' ) ? 'ISO-8859-15' : IPS_DOC_CHAR_SET; $option = @htmlentities( $option, ENT_NOQUOTES, $charSet ); $option = str_replace( '!', '!', $option ); } $option = str_replace( '"', '"', $option ); $option = str_replace( "'", ''', $option ); // ----------------------------------------- // Swapping option/content? // ----------------------------------------- if ( $_bbcode['bbcode_switch_option'] ) { $_tmp = $content; $content = $option; $option = $_tmp; } // ----------------------------------------- // Replace // ----------------------------------------- $replaceCode = $_bbcode['bbcode_replace']; $replaceCode = str_replace( '{base_url}', $this->settings['board_url'] . '/index.php?', $replaceCode ); $replaceCode = str_replace( '{image_url}', $this->settings['img_url'], $replaceCode ); preg_match( '/\{text\.(.+?)\}/i', $replaceCode, $matches ); if ( is_array( $matches ) and count( $matches ) ) { $replaceCode = str_replace( $matches[0], $this->lang->words[$matches[1]], $replaceCode ); } $replaceCode = str_replace( '{option}', $option, $replaceCode ); $replaceCode = str_replace( '{content}', $content, $replaceCode ); // ----------------------------------------- // Fix linebreaks in textareas // ----------------------------------------- if ( stripos( $replaceCode, "', "", $replaceCode ); $replaceCode = str_replace( "\r", "", $replaceCode ); $replaceCode = str_replace( "\n", "
", $replaceCode ); } return $replaceCode; } /** * Reset function to call to trigger "new post" for non-parsed content * * @access public * @return @e void */ public function resetPerPost() { $this->cache->updateCacheWithoutSaving( '_tmp_bbcode_images', 0 ); } /** * Check against XSS * * NOTE: When this function is updated, please also update classIncomingEmail::cleanMessage() * * @access public * @param string Original string * @param boolean Fix script HTML tags * @return string "Cleaned" text */ public function checkXss( $txt='', $fixScript=false, $tag='' ) { //----------------------------------------- // Opening script tags... // Check for spaces and new lines... //----------------------------------------- if ( $fixScript ) { $txt = preg_replace( '#<(\s+?)?s(\s+?)?c(\s+?)?r(\s+?)?i(\s+?)?p(\s+?)?t#is' , "<script" , $txt ); $txt = preg_replace( '#<(\s+?)?/(\s+?)?s(\s+?)?c(\s+?)?r(\s+?)?i(\s+?)?p(\s+?)?t#is', "</script", $txt ); } /* got a tag? */ if ( $tag ) { $tag = strip_tags( $tag, '
' ); switch ($tag) { case 'entry': case 'blog': case 'topic': case 'post': $test = str_replace( array( '"', "'", '"', ''' ), "", $txt ); if ( ! is_numeric( $test ) ) { $txt = false; } break; case 'acronym': $test = str_replace( array( '"', "'", '"', ''' ), "", $txt ); $test1 = str_replace( array( '<', ">", '[', ']' ), "", $test );//IPSText::alphanumericalClean( $test, '.+&#; ' ); if ( $test != $test1 ) { $txt = false; } break; case 'email': $test = str_replace( array( '"', "'", '"', ''' ), "", $txt ); $test = ( IPSText::checkEmailAddress( $test ) ) ? $txt : FALSE; break; case 'font': /* Make sure it's clean */ $test = str_replace( array( '&quot;', '"', "'", '"', ''' ), "", $txt ); $test1 = IPSText::alphanumericalClean( $test, '#.+, ' ); if ( $test != $test1 ) { $txt = false; } break; case 'background': case 'color': /* Make sure it's clean */ $test = str_replace( array( '&quot;', '"', "'", '"', ''' ), "", $txt ); /* Make rgb() safe */ $test = preg_replace( '#rgb(a)?\(([^\)]+?)\)#i', '', $test ); $test1 = IPSText::alphanumericalClean( $test, '#.+, ' ); if ( $test != $test1 ) { $txt = false; } break; default: $_regex = null; $_bbcodes = $this->cache->getCache('bbcode'); if ( !$txt and $_bbcodes[ $tag ]['bbcode_optional_option'] ) { continue; } $_regex = $_bbcodes[ $tag ]['bbcode_custom_regex']; if( $_regex ) { $test = str_replace( array( '"', "'", '"', ''' ), "", $txt ); if( !preg_match( $_regex, $test ) ) { $txt = false; } } break; } /* If we didn't actually get any option data, then return false */ $test = str_replace( array( '"', "'", '"', ''' ), "", $txt ); if ( strlen($txt) AND strlen( $test ) < 1 ) { $txt = false; } if ( $txt === false ) { return false; } /* Still here? Safety, then */ $txt = strip_tags( $txt, '
' ); if( strpos( $txt, '[' ) !== false OR strpos( $txt, ']' ) !== false ) { $txt = str_replace( array( '[', ']' ), array( '[', ']' ), $txt ); } } /* Attempt to make JS safe */ $txt = IPSText::xssMakeJavascriptSafe( $txt ); return $txt; } /** * Check against blacklisted URLs * * @access public * @param string Raw posted text * @return bool False if blacklisted url present, otherwise true */ public function checkBlacklistUrls( $t ) { if( !$t ) { return true; } if ( $this->settings['ipb_use_url_filter'] ) { $list_type = $this->settings['ipb_url_filter_option'] == "black" ? "blacklist" : "whitelist"; if( $this->settings['ipb_url_' . $list_type ] ) { $list_values = array(); $list_values = explode( "\n", str_replace( "\r", "", $this->settings['ipb_url_' . $list_type ] ) ); if( $list_type == 'whitelist' ) { $list_values[] = "http://{$_SERVER['HTTP_HOST']}/*"; } if ( count( $list_values ) ) { $good_url = 0; foreach( $list_values as $my_url ) { if( !trim($my_url) ) { continue; } $my_url = preg_quote( $my_url, '/' ); $my_url = str_replace( '\*', "(.*?)", $my_url ); if ( $list_type == "blacklist" ) { if( preg_match( '/' . $my_url . '/i', $t ) ) { return false; } } else { if ( preg_match( '/' . $my_url . '/i', $t ) ) { $good_url = 1; } } } if ( ! $good_url AND $list_type == "whitelist" ) { return false; } } } } return true; } /** * Makes data for quote strings "safe" * * @access public * @param string Raw text * @return string Converted text */ public function makeQuoteSafe( $txt='' ) { //----------------------------------------- // INIT //----------------------------------------- $begin = ''; $end = ''; //----------------------------------------- // Come via preg_replace_callback? //----------------------------------------- if ( is_array( $txt ) ) { $begin = $txt[1]; $end = $txt[3]; $txt = $txt[2]; } //----------------------------------------- // Sort name //----------------------------------------- $txt = str_replace( "+", "+" , $txt ); $txt = str_replace( "-", "-" , $txt ); $txt = str_replace( ":", ":" , $txt ); $txt = str_replace( "[", "[" , $txt ); $txt = str_replace( "]", "]" , $txt ); $txt = str_replace( ")", ")" , $txt ); $txt = str_replace( "(", "(" , $txt ); $txt = str_replace( "'", "'" , $txt ); return $begin . $txt . $end; } /** * Fix block level bbcodes */ protected function _cleanBlockBBCode( $txt ) { foreach( $this->_blockBBcode as $tag ) { /*

[spoiler]

*/ $txt = preg_replace( '#<(?:p|div)(?:[^>]+?)?>\[' . $tag . '([^\]]+?)?\]#i', '[' . $tag . '$1]', $txt ); /*

[/spoiler]

*/ $txt = preg_replace( '#<(?:p|div)(?:[^>]+?)?>\[/' . $tag . '\]#i', '[/' . $tag . ']', $txt ); } return $txt; } /** * Finish URLs for display * Truncates them, applies white/black lists, adds rel / targets * @param string In * @return string Out */ protected function _finishUrlsForDisplay( $txt ) { /* If HTML mode, don't clean links */ if ( parent::$Perms['parseHtml'] ) { return $txt; } /* Reset counter */ $this->cache->updateCacheWithoutSaving( '_tmp_bbcode_media', 0 ); /* Parse media URLs that are NOT linked */ $txt = preg_replace_callback( '#(^|\s|\)|\(|\{|\}|>|\]|\[|;|href=\S)((http|https|news|ftp)://(?:[^<>\)\[\"\s]+|[a-zA-Z0-9/\._\-!&\#;,%\+\?:=]+))()?#is', array( $this, '_parseMediaUrls_CallBack' ), $txt ); /* LEGACY stuffs - a post from < 3.4 may not be HTMLised properly */ if ( $this->_urlsEnabled === true && preg_match( '#(http|https)://#', $txt ) && ! stristr( $txt, '_autoLinkUrls( $txt ); } preg_match_all( '#]*?)href=["\']([^"\']+?)?["\']([^>]*?)?>(.+?)#is', $txt, $urlMatches ); /* Finish up URLs and such */ for( $i = 0 ; $i < count( $urlMatches[0] ) ; $i++ ) { $raw = $urlMatches[0][ $i ]; $url = $urlMatches[1][ $i ]; $attr = $urlMatches[2][ $i ]; $text = $urlMatches[3][ $i ]; $done = false; $pm = true; preg_match( '#data-ipb=["\']([^"\']+?)?["\']#i', $raw, $matches ); if ( $matches[1] && stristr( $matches[1], 'noparse' ) ) { continue; } else if( $matches[1] && stristr( $matches[1], 'nomediaparse' ) ) { $pm = false; } preg_match( '#rel=["\']([^"\']+?)?["\']#i', $raw, $matches ); if ( $matches[1] && stristr( $matches[1], 'lightbox' ) ) { continue; } /* Urls disabled? */ if ( $this->_urlsEnabled !== true ) { $txt = str_replace( $raw, $url, $txt ); continue; } /* Restored 1st March, Matt @link http://community.invisionpower.com/resources/bugs.html/_/ip-board/some-previously-embedded-content-youtube-etc-now-showing-as-links-after-upgrade-r41411 */ /* Is this a media URL? */ /* Updated 14 May, http://community.invisionpower.com/resources/bugs.html/_/ip-board/url-tags-get-changed-to-media-tags-automatically-r40467 - Editor now sets "noparsemedia" data-ipb attribute, which we can skip here for automatic parsing */ if ( $pm AND $this->settings['bbcode_automatic_media'] and isset( $this->_bbcodes['media'] ) and ( $this->_bbcodes['media']['bbcode_sections'] == 'all' or in_array( parent::$Perms['parseArea'], explode( ',', $this->_bbcodes['media']['bbcode_sections'] ) ) ) ) { $media = $this->cache->getCache( 'mediatag' ); if ( $url == $text && is_array( $media ) AND count( $media ) ) { foreach( $media as $type => $r ) { if ( preg_match( "#^" . $r['match'] . "$#is", $url ) ) { $this->cache->updateCacheWithoutSaving( '_tmp_autoparse_media', 1 ); $_result = $this->_parseBBCode( '[media]' . $url . '[/media]', 'display', array( 'media' ) ); $this->cache->updateCacheWithoutSaving( '_tmp_autoparse_media', 0 ); $txt = str_replace( $raw, $_result, $txt ); $done = true; } } } } /* Format the URL */ if ( $done !== true ) { // ----------------------------------------- // URL filtering? // ----------------------------------------- if ( ! $this->isAllowedUrl( $url ) ) { /* Unlink */ $url = $this->parseBadWords( $url, FALSE ); $txt = str_replace( $raw, $url, $txt ); } // ----------------------------------------- // Let's remove any nested links.. // ----------------------------------------- $text = preg_replace( '/(.+?)<\/a>/is', "\\3", $text ); // ----------------------------------------- // Need to "truncate" the "content" to ~35 // EDIT: but only if it's the same as content // ----------------------------------------- /* * Changes here @link * http://community.invisionpower.com/tracker/issue-36082-long-links-on-mobile-extend-width/ # V V Don't split if URL has entities V V # */ if ( ( empty( $this->settings['__noTruncateUrl'] ) ) and IPSText::mbstrlen( $text ) > 38 && ( ! preg_match( '#&\#([0-9]{2,4})#i', $text ) ) and ( substr( $text, 0, 7 ) == 'http://' or substr( $text, 0, 8 ) == 'https://' ) ) { $text = htmlspecialchars( IPSText::mbsubstr( html_entity_decode( urldecode( $text ) ), 0, 20 ) ) . '...' . htmlspecialchars( IPSText::mbsubstr( html_entity_decode( urldecode( $text ) ), - 15 ) ); } // ----------------------------------------- // Adding rel='nofollow'? // ----------------------------------------- $rels = array(); $rel = ''; $_title = ''; /* Skipping VigLink? */ if ( $this->settings['viglink_norewrite'] and IPSMember::isInGroup( parent::$Perms['memberData'], explode( ',', $this->settings['viglink_norewrite'] ) ) ) { $rels[] = 'norewrite'; } /* Fetch actual host for better matching */ $data = @parse_url( $url ); if ( $this->settings['posts_add_nofollow'] ) { if ( ! stristr( $data['host'], $_SERVER['HTTP_HOST'] ) ) { $rels[] = "nofollow"; } } if ( $this->settings['links_external'] ) { if ( ! stristr( $data['host'], $_SERVER['HTTP_HOST'] ) ) { /* Look a little closer */ $rels[] = "external"; $_title = $this->lang->words['bbc_external_link']; } } if ( count( $rels ) ) { $rel = " rel='" . implode( ' ', $rels ) . "'"; } $replace = "{$text}"; $txt = str_replace( $raw, $replace, $txt ); } } return $txt; } /** * HTML Auto link URLs * @param string $html */ protected function _autoLinkUrls( $html ) { if ( parent::$NoBBCodeAutoLinkify === true ) { return $html; } /* * Capture 'href="' and '' as [URL] is now parsed first, we * discard these in _autoParseUrls */ $html = preg_replace_callback( '#(^|\s|\)|\(|\{|\}|/>|>|\]|\[|;|href=\S)((http|https|news|ftp)://(?:[^<>\)\[\"\s]+|[a-zA-Z0-9/\._\-!&\#;,%\+\?:=]+))()?#is', array( $this, '_autoLinkUrls_CallBack' ), $html ); return $html; } /** * Callback to auto-parse media urls * * @access protected * @param array Matches from the regular expression * @return string Converted text */ protected function _parseMediaUrls_CallBack( $matches ) { $_extra = ''; /* Basic checking */ if ( stristr( $matches[1], 'href' ) ) { return $matches[0]; } if( strlen( $matches[2] ) < 12 ) { return $matches[0]; } if ( isset( $matches[4] ) AND stristr( $matches[4], '' ) ) { return $matches[0]; } /* Check for XSS */ if ( ! IPSText::xssCheckUrl( $matches[2] ) ) { return $matches[0]; } if( substr( $matches[2], -1 ) == ',' ) { $matches[2] = rtrim( $matches[2], ',' ); $_extra = ','; } /* Check for ! which is &#xx; at this point */ if( preg_match( '/&#\d+?;$/', $matches[2], $_m ) ) { $matches[2] = str_replace( $_m[0], '', $matches[2] ); $_extra = $_m[0]; } /* Is this a media URL? */ if( $this->settings['bbcode_automatic_media'] and isset( $this->_bbcodes['media'] ) and ( $this->_bbcodes['media']['bbcode_sections'] == 'all' or in_array( parent::$Perms['parseArea'], explode( ',', $this->_bbcodes['media']['bbcode_sections'] ) ) ) ) { $media = $this->cache->getCache( 'mediatag' ); /* Already converted? */ if ( in_array( $matches[2], $this->_mediaUrlConverted ) ) { return $matches[0]; } if ( is_array( $media ) AND count( $media ) ) { foreach( $media as $type => $r ) { if ( preg_match( "#^" . $r['match'] . "$#is", $matches[2] ) ) { $this->cache->updateCacheWithoutSaving( '_tmp_autoparse_media', 1 ); $_result = $this->_parseBBCode( $matches[1] . '[media]' . $matches[2] . '[/media]' . $_extra, 'html', array( 'media' ) ); $this->cache->updateCacheWithoutSaving( '_tmp_autoparse_media', 0 ); return $_result; } } } } return $matches[1] . $matches[2] . $_extra; } /** * Callback to auto-parse urls * * @access protected * @param array Matches from the regular expression * @return string Converted text */ protected function _autoLinkUrls_CallBack( $matches ) { $_extra = ''; /* Basic checking */ if ( stristr( $matches[1], 'href' ) || $matches[1] == '>' ) { return $matches[0]; } if( strlen( $matches[2] ) < 12 ) { return $matches[0]; } if ( $matches[1] == ']' ) { return $matches[0]; } if ( isset( $matches[4] ) AND stristr( $matches[4], '' ) ) { return $matches[0]; } /* Check for XSS */ if ( ! IPSText::xssCheckUrl( $matches[2] ) ) { return $matches[0]; } if( substr( $matches[2], -1 ) == ',' ) { $matches[2] = rtrim( $matches[2], ',' ); $_extra = ','; } if( substr( $matches[2], -15 ) == '~~~~~_____~~~~~' ) { $matches[2] = substr( $matches[2], 0, -15 ); $_extra = '~~~~~_____~~~~~'; } /* Check for ! which is &#xx; at this point */ if( preg_match( '/&#\d+?;$/', $matches[2], $_m ) ) { $matches[2] = str_replace( $_m[0], '', $matches[2] ); $_extra = $_m[0]; } /* Is this a media URL? */ if( ! parent::$NoMediaAutoLinkify and $this->settings['bbcode_automatic_media'] and isset( $this->_bbcodes['media'] ) and ( $this->_bbcodes['media']['bbcode_sections'] == 'all' or in_array( parent::$Perms['parseArea'], explode( ',', $this->_bbcodes['media']['bbcode_sections'] ) ) ) ) { $media = $this->cache->getCache( 'mediatag' ); /* Already converted? */ if ( in_array( $matches[2], $this->_mediaUrlConverted ) ) { return $matches[0]; } if ( is_array( $media ) AND count( $media ) ) { foreach( $media as $type => $r ) { if ( preg_match( "#^" . $r['match'] . "$#is", $matches[2] ) ) { $this->cache->updateCacheWithoutSaving( '_tmp_autoparse_media', 1 ); $_result = $this->_parseBBCode( $matches[1] . '[media]' . $matches[2] . '[/media]' . $_extra, 'html', array( 'media' ) ); $this->cache->updateCacheWithoutSaving( '_tmp_autoparse_media', 0 ); return $_result; } } } } /* It's not media - so we'll use [url] - check we're allowed first */ if ( ! isset( $this->_bbcodes['url'] ) or ( $this->_bbcodes['url']['bbcode_sections'] != 'all' and ! in_array( parent::$Perms['parseArea'], explode( ',', $this->_bbcodes['url']['bbcode_sections'] ) ) ) ) { // We're not allowed to use [url] here return $matches[0]; } /* Ensure bbcode is stripped for the actual URL */ /* @link http://community.invisionpower.com/tracker/issue-22580-bbcode-breaks-link-add-bold-formatting-to-part-of-link/ */ if ( preg_match( '#\[\w#', $matches[2] ) ) { $wFormatting = $matches[2]; $matches[2] = $this->stripAllTags( $matches[2] ); return $this->_parseBBCode( $matches[1] . '[url="' . $matches[2] . '"]' . $wFormatting . '[/url]' . $_extra, 'html', array( 'url' ) ); } else { /* Is option enforced? */ if ( empty( $this->_bbcodes['url']['bbcode_optional_option'] ) ) { return $this->_parseBBCode( $matches[1] . '[url="' . $matches[2] . '"]' . $matches[2] . '[/url]' . $_extra, 'html', array( 'url' ) ); } else { return $this->_parseBBCode( $matches[1] . '[url]' . $matches[2] . '[/url]' . $_extra, 'html', array( 'url' ) ); } } } /** * Reset bbcode internal pointers * * @access protected * @return @e void */ protected function _resetPointers() { $this->error = ''; $this->image_count = 0; $this->emoticon_count = 0; } /** * Remove session keys from URLs * * @access protected * @param array Array of matches * @return string Converted text */ protected function _bashSession( $matches=array() ) { $start_tok = str_replace( '&', '&', $matches[1] ); $end_tok = str_replace( '&', '&', $matches[3] ); if ( ( $start_tok == '?' OR $start_tok == '&' ) and $end_tok == '') { return ''; } else if ( $start_tok == '?' and $end_tok == '&' ) { return '?'; } else if ( $start_tok == '&' and $end_tok == '&' ) { return "&"; } else { return $start_tok . $end_tok; } } }