Invision Power Services, Inc. * @copyright (c) 2001 - SVN_YYYY Invision Power Services, Inc. * @license http://www.invisionpower.com/legal/standards/ * @package IPS Social Suite * @since 12 Jun 2013 * @version SVN_VERSION_NUMBER */ namespace IPS\Text; /* To prevent PHP errors (extending class does not exist) revealing path */ if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) ) { header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' ); exit; } /** * Legacy Text Parser */ class _LegacyParser { /** * Maximum number of embeds we will try. * @note Attempting to pull too many during the rebuild process can easily time out */ public $maxEmbeds = 10; /** * Parse statically * * @param string $value The value to parse * @param \IPS\Member|null $member The member posting. NULL will use currently logged in member. * @param bool $allowHtml Allow HTML * @param string $attachClass Key to use for attachments * @param int $id1 ID1 to use for attachments * @param int $id2 ID2 to use for attachments * @param int|NULL $id3 ID3 to use for attachments * @param string $itemClass The *item* classname (e.g. IPS\forums\Topic) * @return string * @see __construct */ public static function parseStatic( $value, $member=NULL, $allowHtml=FALSE, $attachClass=null, $id1=0, $id2=0, $id3=NULL, $itemClass=NULL ) { $obj = new self( $member, $allowHtml, $attachClass, $id1, $id2, $id3, $itemClass ); return $obj->parse( $value ); } /** * @brief BBcodes */ public $bbcodes = NULL; /** * @brief Emoticons */ public $emoticons = NULL; /** * @brief Media tags */ public $media = NULL; /** * Can use plugin? * * @param \IPS\Member $member The member * @param string $bbcode BBCode * @return bool */ public static function canUse( \IPS\Member $member, $bbcode ) { foreach( static::$bbcodes as $code ) { if( $code['bbcode_tag'] == $bbcode OR ( $code['bbcode_aliases'] AND in_array( $bbcode, explode( ',', $code['bbcode_aliases'] ) ) ) ) { if( $code['bbcode_groups'] == 'all' ) { return true; } if( $member->inGroup( explode( ',', $bbcode['bbcode_groups'] ) ) ) { return true; } } } return false; } /** * @brief Member */ protected $member = NULL; /** * @brief Allow HTML */ protected $allowHtml = FALSE; /** * @brief Parser object */ protected $parser = NULL; /** * @brief Key for attachments */ protected $attachClass = NULL; /** * @brief Item classname */ protected $itemClass = NULL; /** * @brief ID1 to use for attachments */ protected $idOne = 0; /** * @brief ID2 to use for attachments */ protected $idTwo = 0; /** * @brief ID3 to use for attachments */ protected $idThree = 0; /** * Constructor * * @param \IPS\Member|null $member The member posting. NULL will use guest. * @param bool $allowHtml Allow HTML * @param string $attachClass Key to use for attachments * @param int $id1 ID1 to use for attachments * @param int $id2 ID2 to use for attachments * @param int|NULL $id3 ID3 to use for attachments * @param string $itemClass The *item* classname (e.g. IPS\forums\Topic) * @return void */ public function __construct( $member=NULL, $allowHtml=FALSE, $attachClass=null, $id1=0, $id2=0, $id3=NULL, $itemClass=NULL ) { /* Get member */ $this->member = $member === NULL ? \IPS\Member::load(0) : $member; /* Remember if we allow HTML */ $this->allowHtml = $allowHtml; /* Set attachment data */ $this->attachClass = $attachClass; $this->idOne = $id1; $this->idTwo = $id2; $this->idThree = $id3; /* And item class */ $this->itemClass = $itemClass; /* Grab legacy custom bbcodes */ try { $this->bbcodes = iterator_to_array( \IPS\Db::i()->select( '*', 'custom_bbcode' )->setKeyField( 'bbcode_tag' ) ); } catch( \IPS\Db\Exception $e ) { $this->bbcodes = array(); } /* Grab emoticons */ try { $this->emoticons = iterator_to_array( \IPS\Db::i()->select( '*', 'core_emoticons' )->setKeyField( 'typed' ) ); } catch( \IPS\Db\Exception $e ) { $this->emoticons = array(); } /* Grab a new parser object */ $this->parser = new \IPS\Text\Parser( TRUE, $this->idOne ? array( $this->idOne, $this->idTwo, $this->idThree ) : NULL, $this->member, $this->attachClass, TRUE, ( $this->allowHtml ? FALSE : TRUE ), NULL, 1 ); $updatedBbcodeTags = array_keys( $this->parser->bbcodeTags( $this->member, TRUE ) ); foreach( $updatedBbcodeTags as $tag ) { if( isset( $this->bbcodes[ $tag ] ) AND $tag != 'media' ) { unset( $this->bbcodes[ $tag ] ); } } if( isset( $this->bbcodes['media'] ) ) { $this->bbcodes['media']['bbcode_replace'] = '

{content}

'; } if( $this->media === null ) { try { $this->media = iterator_to_array( \IPS\Db::i()->select( '*', 'bbcode_mediatag' ) ); } catch( \IPS\Db\Exception $e ) { $this->media = array(); } } } /** * Parse * * @param string $value Text to parse * @return string */ public function parse( $value ) { /* We have to fix up newlines a little bit */ $value = str_replace( array( "\r\n", "\r" ), "\n", $value ); if ( ! \strstr( $value, "\n" ) && ( \stristr( $value, '
' ) || \stristr( $value, '
' ) ) ) { $value = \str_ireplace( array( '
', '
' ), "\n", $value ); } else { if ( \stristr( $value, '
' ) || \stristr( $value, '
' ) ) { $value = \str_replace( "\n", "", $value ); } } $value = nl2br( $value ); /* In case the content rebuild has ran or partially ran on this content... */ $value = preg_replace( '#<([^>]+?)(href|src)=(\'|")/#i', '<\1\2=\3%7BfileStore.\4%7D/', $value ); $value = preg_replace( '#<([^>]+?)(href|src)=(\'|")<___base_url___>/#i', '<\1\2=\3%7B___base_url___%7D/', $value ); $value = preg_replace( '#<([^>]+?)(data-fileid)=(\'|")<___base_url___>/#i', '<\1\2=\3%7B___base_url___%7D/', $value ); /* Start with legacy emoticon parsing */ $value = str_replace( "<#EMO_DIR#>", "<#EMO_DIR>", $value ); $value = preg_replace( "#(\s)?<([^>]+?)emoid=\"(.+?)\"([^>]*?)".">(\s)?#is", "\\1\\3\\5", $value ); preg_match_all( "#(]+?)class=['\"]bbc_emoticon[\"'](?:[^>]+?)alt=['\"](.+?)[\"'](?:[^>]+?)?>)#is", $value, $matches ); if( is_array($matches[1]) AND count($matches[1]) ) { foreach( $matches[1] as $index => $match ) { $value = str_replace( $match, $matches[2][ $index ], $value ); } } /* Parse emoticons */ usort( $this->emoticons, function( $a, $b ){ if ( mb_strlen( $a['typed'] ) == mb_strlen( $b['typed'] ) ) { return 0; } return ( mb_strlen( $a['typed'] ) > mb_strlen( $b['typed'] ) ) ? -1 : 1; }); foreach( $this->emoticons as $emoticon ) { $code = str_replace( '<', '<', str_replace( '>', '>', $emoticon['typed'] ) ); if( !$code or !\stristr( $value, $code ) ) { continue; } if ( ! \IPS\File::isFullyQualifiedUrl( $emoticon['image'] ) ) { $emoticon['image'] = \IPS\File::getClass('core_Emoticons')->baseUrl() . '/' . $emoticon['image']; } $quotedCode = preg_quote( $code, '/' ); $value = preg_replace( "/(^|[^a-zA-Z0-9\'\"\/]){$quotedCode}($|[^a-zA-Z0-9\'\"\/])/i", "$1" . $code . "$2", $value ); } /* Old char conversions */ $value = str_replace( " ", " ", $value ); $value = str_replace( "'", "'", $value ); $value = str_replace( "&;", "&", $value ); $value = str_replace( "<#EMO_DIR>", "<#EMO_DIR#>", $value ); /* Unconvert code */ $value = preg_replace_callback( "#(.+?)(.+?)(.+?)#is", array( $this, '_parseOldCode'), $value ); $value = preg_replace_callback( "#(.+?)(.+?)(.+?)#is", array( $this, '_parseOldCode'), $value ); $value = preg_replace_callback( "#.+?#", array( $this, '_parseOldFlash'), $value ); $value = preg_replace( "#(.+?)#", '[code]', $value ); $value = preg_replace( "#(.+?)#", '[/code]', $value ); $value = preg_replace( "#
(.+?)
(.+?)
#is", "[code]\\2[/code]", $value ); /* Preserve code data */ $codeboxes = array(); preg_match_all( "/\[(code|codebox|sql|php|xml|html)(.*?)\](.+?)\[\/(code|codebox|sql|php|xml|html)\]/ims", $value, $matches ); foreach( $matches[0] as $k => $m ) { $c = count( $codeboxes ); $codeboxes[ $c ] = $m; $replacement = ''; $value = str_replace( $m, $replacement, $value ); } /* Just remove this - our new parser doesn't need (or like) it */ $value = str_replace( "[/*]", '', $value ); /* Convert some old HTML back into bbcode to be properly parsed */ $value = preg_replace( "#(.+?)#is", "[entry=\"\\2\"]\\3[/entry]", $value ); $value = preg_replace( "#(.+?)#is", "[blog=\"\\2\"]\\3[/blog]", $value ); $value = preg_replace( "#(.+?)#is", "[post=\"\\2\"]\\3[/post]", $value ); $value = preg_replace( "#(.+?)#is", "[topic=\"\\1\"]\\2[/topic]", $value ); $value = preg_replace( "#<\{POST_SNAPBACK\}>#is", "[snapback]\\3[/snapback]", $value ); $value = preg_replace( "#(.+?)#is", "[extract]\\1[/extract]", $value ); $value = preg_replace( "#(.+?)#is", "[spoiler]\\1[/spoiler]", $value ); /* Unconvert quotes */ $value = preg_replace( "#(.+?)#" , '[quote]' , $value ); $value = preg_replace( "#(.+?)#" , "[quote name='\\1' date='\\2']" , $value ); $value = preg_replace( "#(.+?)#" , "[quote name='\\1']" , $value ); $value = preg_replace( "#(.+?)#" , '[/quote]' , $value ); $value = preg_replace( "#\[quote=(.+?),(.+?)\]#i" , "[quote name='\\1' date='\\2']" , $value ); $value = preg_replace( "#\[quote=(.*?)\[url(.*?)\](.+?)\[\/url\]\]#i" , "[quote=\\1\\3]" , $value ); $value = preg_replace_callback( "#(.+?)#si" , array( $this, '_parseOldQuote' ) , $value ); /* Unconvert indent tag */ while( preg_match( "#
(.+?)
#is", $value ) ) { $value = preg_replace( "#
(.+?)
#is" , "[indent]\\1[/indent]", $value ); } /* Capital inconsistency */ foreach( array_keys( $this->parser->bbcodeTags( $this->member, TRUE ) ) as $bbcode ) { $value = str_replace( '[' . mb_strtoupper( $bbcode ), '[' . $bbcode, $value ); $value = str_replace( '[/' . mb_strtoupper( $bbcode ), '[/' . $bbcode, $value ); } /* Convert quote tag */ $value = preg_replace_callback( "#]*?)>#si", array( $this, '_parseOldBlockquote' ) , $value ); $value = preg_replace_callback( "#\[quote([^>]*?)\]#si" , array( $this, '_parseOldQuoteBbcode' ) , $value ); $value = str_replace( "", "", $value ); $value = str_replace( "[/quote]", "", $value ); /* Fix download manager embedded screenshot URLs */ $value = preg_replace_callback( "#base_url, '/' ), '#' ) . "/index\.php\?app=downloads(?:&|&)module=display(?:&|&)section=screenshot(?:&|&)id=(\d+?)['\"]([^>]*?)>#si", array( $this, '_fixDownloadsScreenshots' ), $value ); /* These were auto-replacements previously */ $value = str_ireplace( "(c)" , "©" , $value ); $value = str_ireplace( "(tm)" , "™" , $value ); $value = str_ireplace( "(r)" , "®" , $value ); /* Convert PHP tags if necessary */ $value = str_ireplace( "" , "?>" , $value ); /* Fix bbcode attributes */ $value = preg_replace_callback( "/\[([^\]]+?)=(\"|'|'|'|")([^\]]+?)(\"|'|'|'|")\]/", function( $matches ) { /* We strip the enclosing quotes and we strip any trailing ';' */ return "[" . $matches[1] . "=" . rtrim( $matches[3], ';' ) . "]"; }, $value ); /* Convert bbcode tags, but only those our new parser doesn't handle */ foreach( $this->bbcodes as $code ) { /* If we don't have a regex replacement, probably because we used to use a plugin, we can't do this automatically */ if( !$code['bbcode_replace'] ) { continue; } /* Build the regex */ $regex = "/\[(?:{$code['bbcode_tag']}" . ( $code['bbcode_aliases'] ? '|' . str_replace( ',', '|', $code['bbcode_aliases'] ) : '' ) . ")" . ( $code['bbcode_useoption'] ? "=(.+?)" : '' ) . "\]" . ( $code['bbcode_single_tag'] ? '' : ( "(.+?)\[\/(?:{$code['bbcode_tag']}" . ( $code['bbcode_aliases'] ? '|' . str_replace( ',', '|', $code['bbcode_aliases'] ) : '' ) . ")\]" ) ) . "/ims"; /* Now actually perform the replacement */ $depth = 0; while( preg_match( $regex, $value ) ) { /* We want to allow nested BBCodes, but we don't want an infinite loop */ $depth++; if ( $depth > 20 ) { break; } $value = preg_replace_callback( $regex, function( $matches ) use ( $code ) { $replacement = $code['bbcode_replace']; if( $code['bbcode_single_tag'] ) { if( $code['bbcode_useoption'] ) { $replacement = str_replace( '{option}', $matches[1], $replacement ); } } else { if( $code['bbcode_useoption'] ) { $replacement = str_replace( '{option}', $matches[1], $replacement ); $replacement = str_replace( '{content}', $matches[2], $replacement ); } else { $replacement = str_replace( '{content}', $matches[1], $replacement ); } } return $replacement; }, $value ); } } /* Figure out what attachments are missing so we can add them at the end */ if( $this->attachClass !== NULL AND $this->idOne !== 0 ) { $embeddedAttachments = array(); preg_match_all( "/\[attachment=(.+?):(.+?)\]/ims", $value, $matches ); if( count( $matches[1] ) ) { foreach( $matches[1] as $id ) { $embeddedAttachments[ $id ] = $id; } } $where = array( array( 'location_key=?', $this->attachClass ), array( 'id1=?', $this->idOne ) ); if( $this->idTwo !== 0 ) { $where[] = array( 'id2=?', $this->idTwo ); } $mappedAttachments = iterator_to_array( \IPS\Db::i()->select( '*', 'core_attachments_map', $where )->setKeyField( 'attachment_id' ) ); foreach( $mappedAttachments as $attachmentId => $map ) { if( !in_array( $attachmentId, $embeddedAttachments ) ) { $value .= '

[attachment=' . $attachmentId . ':name]

'; } } } /* Now fix shared media */ $value = preg_replace_callback( "/\[sharedmedia=(.+?):(.+?):(.+?)\]/ims", function( $matches ) { switch( $matches[1] ) { case 'calendar': try { $event = \IPS\Db::i()->select( '*', 'calendar_events', array( 'event_id=?', (int) $matches[3] ) )->first(); $url = \IPS\Http\Url::internal( "app=calendar&module=events&controller=event&id={$event['event_id']}", 'front', 'calendar_event', $event['event_title_seo'] ); return "

{$url}

"; } catch( \Exception $e ) { return ""; } break; case 'gallery': if( $matches[2] == 'images' ) { try { $image = \IPS\Db::i()->select( '*', 'gallery_images', array( 'image_id=?', (int) $matches[3] ) )->first(); $url = \IPS\Http\Url::internal( "app=gallery&module=gallery&controller=view&id={$image['image_id']}", 'front', 'gallery_image', $image['image_caption_seo'] ); return "

{$url}

"; } catch( \Exception $e ) { return ""; } } else { try { $album = \IPS\Db::i()->select( '*', 'gallery_albums', array( 'album_id=?', (int) $matches[3] ) )->first(); $url = \IPS\Http\Url::internal( "app=gallery&module=gallery&controller=browse&album={$album['album_id']}", 'front', 'gallery_album', $album['album_name_seo'] ); return "

{$url}

"; } catch( \Exception $e ) { return ""; } } break; case 'downloads': try { $file = \IPS\Db::i()->select( '*', 'downloads_files', array( 'file_id=?', (int) $matches[3] ) )->first(); $url = \IPS\Http\Url::internal( "app=downloads&module=downloads&controller=view&id={$file['file_id']}", 'front', 'downloads_file', $file['file_name_furl'] ); return "

{$url}

"; } catch( \Exception $e ) { return ""; } break; case 'core': /* We'll just let the attachment parsing next take care of this */ return "[attachment={$matches[3]}:string]"; break; case 'blog': try { $entry = \IPS\Db::i()->select( '*', 'blog_entries', array( 'entry_id=?', (int) $matches[3] ) )->first(); $url = \IPS\Http\Url::internal( "app=blog&module=blogs&controller=entry&id={$entry['entry_id']}", 'front', 'blog_entry', $entry['entry_name_seo'] ); return "

{$url}

"; } catch( \Exception $e ) { return ""; } break; } /* Could be a third party app - just return original embed code if we are still here */ return $matches[0]; }, $value ); /* Some people may have tried to embed the direct youtube video...tsk tsk */ preg_match_all( '#youtube.com/v/([^ \[]+?)#is', $value, $matches ); foreach( $matches[0] as $idx => $m ) { $value = str_replace( $m, "youtube.com/watch?v=" . $matches[1][ $idx ], $value ); } /* Media */ $urls = array(); /* Parse [URL]url[/URL] bbcode */ preg_match_all( '#\[url]([^\[]+?)\[\/url\]#is', $value, $matches ); foreach( $matches[0] as $idx => $m ) { $value = str_replace( $m, "" . $matches[1][ $idx ] . "", $value ); } /* Parse [URL=url]text[/URL] bbcode */ preg_match_all( '#\[url=(?:\'|"|&\#039;)?(.+?)(?:\'|"|&\#039;)?\](.+?)\[\/url\]#is', $value, $matches ); foreach( $matches[0] as $idx => $m ) { $value = str_replace( $m, "" . $matches[2][ $idx ] . "", $value ); } /* Parse img bbcode */ preg_match_all( '#\[img\](.+?)\[\/img\]#is', $value, $matches ); foreach( $matches[0] as $idx => $m ) { $image = basename( $matches[1][ $idx ] ); $value = str_replace( $m, "{$image}", $value ); } /* Get rid of empty hyperlinks, which can be present from bugs in older versions of IPB */ preg_match_all( '#(.+?)\<\/a\>#is', $value, $matches ); foreach( $matches[0] as $k => $v ) { $value = str_replace( $v, $matches[1][ $k ], $value ); } /* Get rid of nested iframes if they exist */ preg_match_all( "/