Version 4.1.2

This commit is contained in:
Neo committed 2025-12-19 05:32:19 -08:00
1 parent 3ba79295ee
commit 28bd025b35
1131 files changed
+63658 -20579

No files matched your search

+237 -139
View File
@@ -55,12 +55,13 @@ class _Parser
* @param bool $filterProfanity Remove profanity?
* @param bool $cleanHtml If TRUE, HTML will be cleaned through HTMLPurifier
* @param callback $htmlPurifierConfig A function which will be passed the HTMLPurifier_Config object to customise it - see example
* @param int $requestTimeout NULL uses \IPS\DEFAULT_REQUEST_TIMEOUT. Can be set manually to avoid large timeouts on rebuild processes
* @return string
* @see __construct
*/
public static function parseStatic( $value, $bbcode=FALSE, $attachIds=NULL, $member=NULL, $area=FALSE, $filterProfanity=TRUE, $cleanHtml=TRUE, $htmlPurifierConfig=NULL )
public static function parseStatic( $value, $bbcode=FALSE, $attachIds=NULL, $member=NULL, $area=FALSE, $filterProfanity=TRUE, $cleanHtml=TRUE, $htmlPurifierConfig=NULL, $requestTimeout=NULL )
{
$obj = new self( $bbcode, $attachIds, $member, $area, $filterProfanity, $cleanHtml, $htmlPurifierConfig );
$obj = new self( $bbcode, $attachIds, $member, $area, $filterProfanity, $cleanHtml, $htmlPurifierConfig, $requestTimeout );
return $obj->parse( $value );
}
@@ -503,8 +504,13 @@ class _Parser
*/
protected static function allowedIFrameBases()
{
return array(
str_replace( array( 'http://', 'https://' ), '', \IPS\Settings::i()->base_url ),
$return = array();
/* The community itself (for internal embeds) */
$return[] = str_replace( array( 'http://', 'https://' ), '', \IPS\Settings::i()->base_url );
/* Our default embed options */
$return = array_merge( $return, array(
'www.youtube.com/embed/',
'player.vimeo.com/video/',
'www.hulu.com/embed.html',
@@ -513,7 +519,15 @@ class _Parser
'vine.co/',
'gfycat.com/',
'embed.spotify.com/'
);
) );
/* Extra admin-defined options */
if ( \IPS\Settings::i()->editor_allowed_iframe_bases )
{
$return = array_merge( $return, explode( ',', \IPS\Settings::i()->editor_allowed_iframe_bases ) );
}
return $return;
}
/**
@@ -548,7 +562,7 @@ class _Parser
try
{
$oembedUrl = \IPS\Http\Url::external( $oembedServices[ $domain ] )->setQueryString( array( 'format' => 'json', 'url' => $url, 'scheme' => ( $parsedUrl['scheme'] === 'https' or \IPS\Request::i()->isSecure() ) ? 'https' : null ) );
$response = $oembedUrl->request()->get()->decodeJson();
$response = $oembedUrl->request( static::$requestTimeout )->get()->decodeJson();
/* Ignore errors - this will be caught by the catch statement below */
if( !isset( $response['type'] ) )
@@ -607,40 +621,47 @@ class _Parser
return NULL;
}
/* Gfycat needs no iframe because it is just an iframe we're showing */
if ( $domain == 'gfycat.com' )
/* Show the iframe... */
if ( !$iframe )
{
try
if ( $domain == 'gfycat.com' )
{
$data = \IPS\Http\Url::external( 'https://gfycat.com/cajax/get/' . $matches[1] )->request()->get()->decodeJson();
if( isset( $data['gfyItem'] ) )
try
{
return \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->gfycat( $data['gfyItem']['gfyName'], $data['gfyItem']['width'], $data['gfyItem']['height'] );
$data = \IPS\Http\Url::external( 'https://gfycat.com/cajax/get/' . $matches[1] )->request()->get()->decodeJson();
if( isset( $data['gfyItem'] ) )
{
return \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->iframe( (string) \IPS\Http\Url::internal('app=core&module=system&controller=embed')->setQueryString( array( 'url' => $url, 'key' => $data['gfyItem']['gfyName'] ) ) );
}
else
{
return NULL;
}
}
catch ( \IPS\Http\Request\Exception $e )
{
return NULL;
}
}
catch ( \RuntimeException $e ) {}
}
/* But the others need to be in an iframe because they include <script> tags... */
else
{
/* Show the iframe... */
if ( !$iframe )
else
{
return \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->iframe( (string) \IPS\Http\Url::internal('app=core&module=system&controller=embed')->setQueryString( 'url', $url ) );
}
/* Or show the content... */
else
}
/* Or show the content... */
else
{
switch ( $domain )
{
switch ( $domain )
{
case 'facebook.com':
return \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->blankTemplate( \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->facebook( $url ) );
case 'plus.google.com':
return \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->blankTemplate( \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->google( $url ) );
}
case 'gfycat.com':
return \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->blankTemplate( \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->gfycat( \IPS\Request::i()->key ) );
case 'facebook.com':
return \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->blankTemplate( \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->facebook( $url ) );
case 'plus.google.com':
return \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->blankTemplate( \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->google( $url ) );
}
}
}
@@ -728,15 +749,28 @@ class _Parser
if ( in_array( mb_strtolower( $extension ), \IPS\Image::$imageExtensions ) )
{
$extra = '';
if ( !\IPS\Settings::i()->allow_remote_images )
{
throw new \DomainException;
}
/* Try getimagesize first, as it is more efficient when available */
try
{
$url = \IPS\Http\Url::external( $url );
try
{
$dims = @getimagesize( (string) $url );
/* It isn't possible to set a timeout for getimagesize() so if a URL takes a long time too load, or times out, then that can cause parsing to fail. Create a temporary file from the remote one and use that instead. We will request the file here, which will throw an exception if it times out */
$image = \IPS\Http\Url::external( $url )->request()->get();
$temporary = tempnam( \IPS\TEMP_DIRECTORY, 'image' );
\file_put_contents( $temporary, $image );
$dims = @getimagesize( $temporary );
if ( @is_file( $temporary ) )
{
@unlink( $temporary );
}
if( $dims === FALSE OR !isset( $dims[0] ) OR !$dims[0] )
{
@@ -807,12 +841,7 @@ class _Parser
* @brief All open inline nodes, needed by the BBCode parser
*/
protected $inlineNodes = array();
/**
* @brief Quote depth
*/
protected $quoteDepth = 0;
/**
* @brief Parsing BBCode flag, needed by the BBCode parser for no-parse tags
*/
@@ -831,7 +860,7 @@ class _Parser
/**
* @brief Existing attachments
*/
protected $existingAttachments = array();
public $existingAttachments = array();
/**
* @brief Member
@@ -862,6 +891,11 @@ class _Parser
* @brief Mapped attachments
*/
public $mappedAttachments = array();
/**
* @brief External link request timeout (NULL uses \IPS\DEFAULT_REQUEST_TIMEOUT)
*/
protected static $requestTimeout = NULL;
/**
* Constructor
@@ -873,6 +907,7 @@ class _Parser
* @param bool $filterProfanity Remove profanity?
* @param bool $cleanHtml If TRUE, HTML will be cleaned through HTMLPurifier
* @param callback $htmlPurifierConfig A function which will be passed the HTMLPurifier_Config object to customise it - see example
* @param int $requestTimeout NULL uses \IPS\DEFAULT_REQUEST_TIMEOUT. Can be set manually to avoid large timeouts on rebuild processes
* @return void
* @code
$parser = new \IPS\Text\Parser(
@@ -888,7 +923,7 @@ class _Parser
$value = $parser->parse( $value );
* @endcode
*/
public function __construct( $bbcode=FALSE, $attachIds=NULL, $member=NULL, $area=FALSE, $filterProfanity=TRUE, $cleanHtml=TRUE, $htmlPurifierConfig=NULL )
public function __construct( $bbcode=FALSE, $attachIds=NULL, $member=NULL, $area=FALSE, $filterProfanity=TRUE, $cleanHtml=TRUE, $htmlPurifierConfig=NULL, $requestTimeout=NULL )
{
/* Get member */
$this->member = $member === NULL ? \IPS\Member::loggedIn() : $member;
@@ -899,6 +934,9 @@ class _Parser
$this->bbcode = static::bbcodeTags( $this->member, $area );
}
/* Set timeout */
static::$requestTimeout = $requestTimeout;
/* Set area */
$this->area = $area;
@@ -994,14 +1032,17 @@ class _Parser
$def->addAttribute( 'blockquote', 'data-ipsquote-contentid', 'Text' );
$def->addAttribute( 'blockquote', 'data-ipsquote-contentcommentid', 'Text' );
$def->addAttribute( 'blockquote', 'data-cite', 'Text' );
$def->addAttribute( 'blockquote', 'data-ipsspoiler', 'Text' );
$def->addAttribute( 'div', 'data-ipsspoiler', 'Text' );
$def->addAttribute( 'div', 'data-role', 'Text' );
$def->addAttribute( 'div', 'data-controller', 'Text' );
$def->addAttribute( 'div', 'contenteditable', 'Text' );
$def->addAttribute( 'img', 'data-fileid', 'Text' );
$def->addAttribute( 'img', 'data-munge-src', 'Text' );
$def->addAttribute( 'img', 'data-extension', 'Text' );
$def->addAttribute( 'img', 'srcset', 'Text' );
$def->addAttribute( 'img', 'data-emoticon', 'Text' );
$def->addAttribute( 'a', 'data-pages-page', 'Number' );
$def->addAttribute( 'a', 'contenteditable', 'Text' );
$def->addAttribute( 'a', 'data-mentionid', 'Number' );
$def->addAttribute( 'a', 'data-controller', 'Text' );
$def->addAttribute( 'a', 'data-service', 'Text' );
@@ -1039,21 +1080,6 @@ class _Parser
/* BBCode, profanity, acronyms */
if ( !empty( $this->bbcode ) or !empty( $this->looseProfanity ) or !empty( $this->exactProfanity ) or !empty( $this->acronyms ) or $this->attachIds !== NULL )
{
/* Little hacky way of fixing lists that aren't spaced how we like */
$value = str_ireplace( "[list][*]", "[list]</p><p>[*]", $value );
$value = str_ireplace( "[list]<br>", "</p><p>[list]", $value );
/* Little hacky way of fixing newlines inside center tags */
$value = preg_replace( "/\[center\](.*?)<br>(.*?)\[\/center\]/i", "[center]$1</p><p>$2[/center]", $value );
/* Little hacky way of fixing newlines inside code tags */
while( preg_match( "/\[code\](.*?)<(br|p)>(.*?)\[\/code\]/ims", $value ) )
{
$value = preg_replace_callback( "/\[code\](.*?)<(?:br|p)>(.*?)\[\/code\]/ims", function( $matches ) {
return "[code]" . str_replace( '</p>', '', "{$matches[1]}\n{$matches[2]}[/code]" );
}, $value );
}
/* @link http://community.invisionpower.com/resources/bugs.html/_/4-0-0/no-js-br-added-in-the-middle-of-pre-blocks-r46414
If this causes issues (I couldn't find any in my testing with and without js enabled) we can remove this */
while( preg_match( "/\<pre class=['\"]ipsCode[\"']\>(.*?)(<br><br>)(.*?)\<\/pre\>/ims", $value, $matches ) )
@@ -1098,7 +1124,7 @@ class _Parser
/* And create a new */
$document = new \DOMDocument( '1.0', 'UTF-8' );
@$document->loadHTML( "<head><meta http-equiv='Content-Type' content='text/html; charset=utf-8'/></head>" );
/* Loop */
foreach ( $source->childNodes as $node )
{
@@ -1113,7 +1139,7 @@ class _Parser
$value = preg_replace( '/(<iframe\s+?(?:[^>])*)\/>/i', '$1></iframe>', $value );
$value = preg_replace( '/<meta http-equiv(?:[^>]+?)>/i', '', $value );
}
/* We need to wrap IP.Content custom 'pages' bbcode in a special div */
if( isset( $this->bbcode['page'] ) AND ( ( mb_strpos( $value, 'data-role' ) !== FALSE AND mb_strpos( $value, 'contentPage' ) !== FALSE ) OR mb_stripos( $value, '[page]' ) !== FALSE ) AND mb_strpos( $value, 'core.front.core.articlePages' ) === FALSE )
{
@@ -1123,16 +1149,17 @@ class _Parser
/* Little hack for lists */
if ( array_key_exists( 'list', $this->bbcode ) )
{
$value = preg_replace_callback( "/<[u|o]l data-ipsBBCode-list=\"true\"(?:.+?)?>.+?<\/[u|o]l>/im", function( $matches )
$value = preg_replace_callback( "/<[u|o]l data-ipsBBCode-list=\"true\"(?:.+?)?>.+?<\/[u|o]l>/is", function( $matches )
{
$v = str_replace( '</p><p>', '<br>', $matches[0] );
$v = str_replace( ' data-ipsBBCode-list="true"', '', $matches[0] );
$v = str_replace( '</p><p>', '<br>', $v );
$v = str_replace( array( '<p>', '</p>' ), '', $v );
$v = str_replace( '[*]', '</li><li>', $v );
$v = str_replace( '<ul></li>', '<ul>', $v );
$v = str_replace( '<ol></li>', '<ol>', $v );
$v = preg_replace( '#<ul>(\s+?)?</li>#s', '<ul>', $v );
$v = preg_replace( '#<ol>(\s+?)?</li>#s', '<ol>', $v );
$v = str_replace( '</ul>', '</li></ul>', $v );
$v = str_replace( '</ol>', '</li></ol>', $v );
$v = str_replace( ' data-ipsBBCode-list="true"', '', $v );
return $v;
}, $value );
}
@@ -1142,15 +1169,16 @@ class _Parser
{
$value = $this->htmlPurifier->purify( $value );
}
/* A few more replacements for dynamic file object URLs */
$value = preg_replace( '#<([^>]+?)(srcset)=(\'|")%7BfileStore\.([\d\w\_]+?)%7D/#i', '<\1\2=\3<fileStore.\4>/', $value );
$value = preg_replace( '#<([^>]+?)(href|src)=(\'|")%7BfileStore\.([\d\w\_]+?)%7D/#i', '<\1\2=\3<fileStore.\4>/', $value );
$value = preg_replace( '#<([^>]+?)(href|src)=(\'|")%7B___base_url___%7D/#i', '<\1\2=\3<___base_url___>/', $value );
$value = preg_replace( '#<([^>]+?)(href|src|srcset)=(\'|")%7B___base_url___%7D/#i', '<\1\2=\3<___base_url___>/', $value );
$value = preg_replace( '#<([^>]+?)(data-fileid)=(\'|"){___base_url___}/#i', '<\1\2=\3<___base_url___>/', $value );
/* iFrame has to be handled here */
$value = preg_replace( '#<iframe(.+?)src=(\'|")' . preg_quote( rtrim( \IPS\Settings::i()->base_url, '/' ), '#' ) . '/#i', '<iframe\1src=\2<___base_url___>/', $value );
/* Return */
return $value;
}
@@ -1168,9 +1196,17 @@ class _Parser
/* If we have no parent, we'll insert straight into the document */
$parent = $parent ?: $document;
/* If this is a code box, don't try to parse inside it */
if ( $node instanceof \DOMElement and mb_strpos( $node->getAttribute( 'class' ), 'ipsCode' ) !== false )
{
$newNode = $document->importNode( $node, TRUE );
$parent->appendChild( $newNode );
return true;
}
/* Is this a text node? */
if ( $node instanceof \DOMText )
{
{
/* If we have an open block node, change the parent to that */
if ( $this->openBlock and !$this->openBlockDepth )
{
@@ -1180,26 +1216,21 @@ class _Parser
/* We only need to do this if we have any text */
$text = $node->wholeText;
/* If this is a code box, don't try to parse inside it */
try
{
if( $parent instanceof \DOMElement and mb_strpos( $parent->getAttribute( 'class' ), 'ipsCode' ) !== false )
{
$parent->appendChild( $document->createTextNode( $text ) );
return true;
}
}
catch( \ErrorException $e )
{
/* 'DOMNode::appendChild(): Couldn't fetch DOMElement' */
if( mb_strpos( $e->getMessage(), "fetch DOMElement" ) )
{
throw new \InvalidArgumentException( $e->getMessage(), 103014 );
}
throw $e;
}
/* Little hacky way of fixing lists that aren't spaced how we like */
$text = str_ireplace( "[list][*]", "[list]</p><p>[*]", $text );
$text = str_ireplace( "[list]<br>", "</p><p>[list]", $text );
/* Little hacky way of fixing newlines inside center tags */
$text = preg_replace( "/\[center\](.*?)<br>(.*?)\[\/center\]/i", "[center]$1</p><p>$2[/center]", $text );
/* Little hacky way of fixing newlines inside code tags */
while( preg_match( "/\[code\](.*?)<(br|p)>(.*?)\[\/code\]/ims", $text ) )
{
$text = preg_replace_callback( "/\[code\](.*?)<(?:br|p)>(.*?)\[\/code\]/ims", function( $matches ) {
return "[code]" . str_replace( '</p>', '', "{$matches[1]}\n{$matches[2]}[/code]" );
}, $text );
}
/* Little hacky way of supporting [tag]content[/tag] where our preference is [tag=content] */
if ( array_key_exists( 'img', $this->bbcode ) )
{
@@ -1211,7 +1242,7 @@ class _Parser
}
/* If this is raw text immediately inside a quote, we need to fix that */
if( $parent instanceof \DOMElement and $parent->tagName === 'blockquote' and mb_strpos( $parent->getAttribute('class'), 'ipsStyle_spoiler' ) === FALSE )
if( trim( $text ) and $parent instanceof \DOMElement and $parent->tagName === 'blockquote' and mb_strpos( $parent->getAttribute('class'), 'ipsStyle_spoiler' ) === FALSE )
{
$wrapper = $document->createElement( 'p', $text );
$parent->appendChild( $wrapper );
@@ -1263,7 +1294,7 @@ class _Parser
{
if ( ( !$this->acronyms[ mb_strtolower( $section ) ]['a_casesensitive'] or $this->acronyms[ mb_strtolower( $section ) ]['a_short'] == $section ) and $parent->tagName !== 'abbr' )
{
$acronymNode = $document->createElement( 'abbr', htmlentities( $section, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) );
$acronymNode = $document->createElement( 'abbr', htmlspecialchars( $section, \IPS\HTMLENTITIES, 'UTF-8', FALSE ) );
$acronymNode->setAttribute( 'title', $this->acronyms[ mb_strtolower($section) ]['a_long'] );
}
else
@@ -1620,14 +1651,14 @@ class _Parser
{
return $added;
}
/* If this is the head of the document, just skip as we added that to force UTF-8 mode */
if( $node->tagName == 'head' )
{
return $added;
}
/* Remove Negative Margins */
/* Remove Negative Margins or nowraps */
if( !( $node instanceof \DOMComment ) and $node->nodeType == XML_ELEMENT_NODE and $node->hasAttribute('style') )
{
$style = $node->getAttribute('style');
@@ -1645,6 +1676,11 @@ class _Parser
continue;
}
}
if( mb_strpos( trim( $_inlineStyle[0] ), 'white-space' ) === 0 and mb_strpos( trim( $_inlineStyle[1] ), 'nowrap' ) === 0 )
{
continue;
}
$_saved[] = $_style;
}
@@ -1695,14 +1731,7 @@ class _Parser
/* Is it a quote? */
if ( $node->tagName === 'blockquote' && mb_strpos( $node->getAttribute('class'), 'ipsStyle_spoiler' ) === FALSE )
{
/* Are we preventing quote embedding? */
if ( $this->quoteDepth and \IPS\Settings::i()->strip_quotes )
{
return;
}
$this->quoteDepth++;
{
/* Do we need to change the relative time? */
if ( $node->getAttribute('data-cite') and $node->getAttribute('data-ipsquote-username') )
{
@@ -1736,7 +1765,7 @@ class _Parser
$rels[] = 'external';
/* Do we also want to add nofollow? */
if( \IPS\Settings::i()->posts_add_nofollow and ( !\IPS\Settings::i()->posts_add_nofollow_exclude or !in_array( preg_replace( '/^www\./', '', $link->data['host'] ), json_decode( \IPS\Settings::i()->posts_add_nofollow_exclude, TRUE ) ) ) )
if( \IPS\Settings::i()->posts_add_nofollow and ( !\IPS\Settings::i()->posts_add_nofollow_exclude or ( isset( $link->data['host'] ) and !in_array( preg_replace( '/^www\./', '', $link->data['host'] ), json_decode( \IPS\Settings::i()->posts_add_nofollow_exclude ) ) ) ) )
{
$rels[] = 'nofollow';
}
@@ -1832,9 +1861,27 @@ class _Parser
$added = TRUE;
}
if( $node->tagName === 'img' and !$this->isAllowedUrl( $node->getAttribute('src') ) )
if( $node->tagName === 'img' )
{
return;
$imageSrc = $node->getAttribute('src');
if ( !$this->isAllowedUrl( $imageSrc ) )
{
return;
}
if ( \IPS\Settings::i()->remote_image_proxy )
{
$imageSrc = new \IPS\Http\Url( $imageSrc );
if ( !$imageSrc->isInternal )
{
$newUrl = new \IPS\Http\Url( \IPS\Settings::i()->base_url . "applications/core/interface/imageproxy/imageproxy.php" );
$newUrl = $newUrl->setQueryString( array(
'img' => (string) $imageSrc,
'key' => hash_hmac( "sha256", (string) $imageSrc, md5( \IPS\Settings::i()->sql_pass . \IPS\Settings::i()->board_url . \IPS\Settings::i()->sql_database ) )
) );
$node->setAttribute( 'src', (string) $newUrl );
}
}
}
/* If we have an open block node, change the parent to that */
@@ -1858,13 +1905,9 @@ class _Parser
if ( static::$emoticons === NULL )
{
static::$emoticons = array();
foreach ( \IPS\Db::i()->select( 'image', 'core_emoticons' ) as $row )
{
static::$emoticons[] = $row;
}
static::$emoticons = static::getEmoticons();
}
$nodeChecked = FALSE;
if ( ( $node->tagName === 'a' or $node->tagName === 'img' ) and $node->getAttribute('data-extension') )
{
@@ -1879,7 +1922,7 @@ class _Parser
catch ( \Exception $e ) { }
}
$url = ( $node->tagName === 'a' ) ? $node->getAttribute('href') : $node->getAttribute('src');
if ( isset( static::$fileObjectClasses[ $extension ] ) and preg_match( '#^(' . preg_quote( rtrim( static::$fileObjectClasses[ $extension ]->baseUrl(), '/' ), '#' ) . ')/(.+?)$#', $url, $matches ) )
{
$nodeChecked = TRUE;
@@ -1891,7 +1934,7 @@ class _Parser
$nodeChecked = FALSE;
}
}
/* A fall back for earlier posts */
if ( ! $nodeChecked and ( ( $node->tagName === 'a' and preg_match( '#^(' . preg_quote( rtrim( static::$fileObjectClasses['core_Attachment']->baseUrl(), '/' ), '#' ) . ')/(.+?)$#', $node->getAttribute('href'), $matches ) ) ) )
{
@@ -1921,26 +1964,34 @@ class _Parser
$node->setAttribute( 'src', '{fileStore.core_Attachment}/' . $matches[2] );
$nodeChecked = TRUE;
}
/* Emoticons */
if ( ! $nodeChecked and ( ( $node->tagName === 'img' and preg_match( '#^(' . preg_quote( rtrim( static::$fileObjectClasses['core_Emoticons']->baseUrl(), '/' ), '#' ) . ')/(.+?)$#', $node->getAttribute('src'), $matches ) ) ) )
{
foreach( static::$emoticons as $emo )
{
if ( $emo == $matches[2] )
if ( $emo['image'] == $matches[2] )
{
$added = TRUE;
$node->setAttribute( 'src', '{fileStore.core_Emoticons}/' . $matches[2] );
if( $emo['image_2x'] && $emo['width'] && $emo['height'] )
{
$node->setAttribute( 'srcset', '%7BfileStore.core_Emoticons%7D/' . $emo['image_2x'] . ' 2x' );
/* Retina emoticons require a width/height for proper scaling */
$node->setAttribute( 'width', $emo['width'] );
$node->setAttribute( 'height', $emo['height'] );
}
$nodeChecked = TRUE;
}
}
}
/* Anything else that has a URL */
if ( ! $nodeChecked and ( $node->tagName === 'a' or $node->tagName === 'img' ) )
{
$url = ( $node->tagName === 'a' ) ? $node->getAttribute('href') : $node->getAttribute('src');
if ( preg_match( '#^(' . preg_quote( rtrim( \IPS\Settings::i()->base_url, '/' ), '#' ) . ')/(.+?)$#', $url, $matches ) )
{
$added = TRUE;
@@ -1974,7 +2025,7 @@ class _Parser
throw $e;
}
/* Loop children */
if ( $node->hasChildNodes() )
{
@@ -1992,13 +2043,7 @@ class _Parser
{
$this->openBlockDepth--;
}
/* End quote */
if ( $node->tagName === 'blockquote' )
{
$this->quoteDepth--;
}
/* Return */
return $added;
}
@@ -2215,7 +2260,7 @@ class _Parser
* @return bool If the node contains any contents
*/
protected static function mungeResourcesNode( $document, $node, $parent=NULL )
{
{
/* If we have no parent, we'll insert straight into the document */
$parent = $parent ?: $document;
@@ -2279,7 +2324,7 @@ class _Parser
}
}
}
/* Return */
return $added;
}
@@ -2331,20 +2376,24 @@ class _Parser
if ( static::$emoticons === NULL )
{
static::$emoticons = array();
foreach ( \IPS\Db::i()->select( 'image', 'core_emoticons' ) as $row )
{
static::$emoticons[] = $row;
}
static::$emoticons = static::getEmoticons();
}
if ( ( $image->tagName === 'img' and preg_match( '#^(' . preg_quote( rtrim( static::$fileObjectClasses['core_Emoticons']->baseUrl(), '/' ), '#' ) . ')/(.+?)$#', $image->getAttribute('src'), $matches ) ) )
{
foreach( static::$emoticons as $emo )
{
if ( $emo == $matches[2] )
if ( $emo['image'] == $matches[2] )
{
$image->setAttribute( 'src', '{fileStore.core_Emoticons}/' . $matches[2] );
if( $emo['image_2x'] && $emo['width'] && $emo['height'] )
{
/* Retina emoticons require a width and height for proper scaling */
$image->setAttribute( 'srcset', '%7BfileStore.core_Emoticons%7D/' . $emo['image_2x'] . ' 2x' );
$image->setAttribute( 'width', $emo['width'] );
$image->setAttribute( 'height', $emo['height'] );
}
$rebuilt = TRUE;
}
}
@@ -2359,8 +2408,9 @@ class _Parser
$value = preg_replace( '/<meta http-equiv(?:[^>]+?)>/i', '', preg_replace( '/^<!DOCTYPE.+?>/', '', str_replace( array( '<html>', '</html>', '<body>', '</body>', '<head>', '</head>' ), '', $value ) ) );
/* A few more replacements for dynamic file object URLs */
$value = preg_replace( '#<([^>]+?)(href|src)=(\'|")%7BfileStore\.([\d\w\_]+?)%7D/#i', '<\1\2=\3<fileStore.\4>/', $value );
$value = preg_replace( '#<([^>]+?)(href|src)=(\'|")%7B___base_url___%7D/#i', '<\1\2=\3<___base_url___>/', $value );
$value = preg_replace( '#<([^>]+?)(href|src|srcset)=(\'|")%7BfileStore\.([\d\w\_]+?)%7D/#i', '<\1\2=\3<fileStore.\4>/', $value );
$value = preg_replace( '#<([^>]+?)(srcset)=(\'|")%7BfileStore\.([\d\w\_]+?)%7D/#i', '<\1\2=\3<fileStore.\4>/', $value );
$value = preg_replace( '#<([^>]+?)(href|src|srcset)=(\'|")%7B___base_url___%7D/#i', '<\1\2=\3<___base_url___>/', $value );
$value = preg_replace( '#<([^>]+?)(data-fileid)=(\'|")%7B___base_url___%7D/#i', '<\1\2=\3<___base_url___>/', $value );
return $value;
@@ -2368,6 +2418,34 @@ class _Parser
return FALSE;
}
/**
* Fetch emoticons.
*
* @return array
*/
protected static function getEmoticons()
{
$emoticons = array();
try
{
foreach ( \IPS\Db::i()->select( 'image, image_2x, width, height', 'core_emoticons' ) as $row )
{
$emoticons[] = $row;
}
}
catch( \IPS\Db\Exception $ex )
{
/* The image_2x column was added in 4.1 so may not exist if Parser is used in previous upgrade modules */
foreach ( \IPS\Db::i()->select( 'image, NULL as image_2x, 0 as width, 0 as height', 'core_emoticons' ) as $row )
{
$emoticons[] = $row;
}
}
return $emoticons;
}
/**
* Return the default allowed CSS classes
@@ -2378,11 +2456,11 @@ class _Parser
protected function getAllowedCssClasses()
{
return array(
'ipsStyle', 'ipsStyle_underline', 'ipsStyle_center', 'ipsStyle_left', 'ipsStyle_right', 'ipsStyle_indent', 'ipsStyle_hr', 'ipsStyle_spoiler',
'ipsStyle_spoilerFancy', 'ipsStyle_spoilerFancy_inner', 'ipsStyle_spoilerFancy_text', 'ipsStyle_spoilerFancy_open',
'ipsStyle', 'ipsStyle_underline', 'ipsStyle_center', 'ipsStyle_left', 'ipsStyle_right', 'ipsStyle_indent', 'ipsStyle_hr',
'ipsSpoiler', 'ipsSpoiler_header', 'ipsSpoiler_contents',
'ipsButton',
'ipsCode', 'prettyprint', 'lang-auto', 'lang-javascript', 'lang-php', 'lang-css', 'lang-html',
'ipsQuote','ipsQuote_citation', 'ipsQuote_closed', 'ipsQuote_open',
'ipsCode', 'prettyprint', 'prettyprinted', 'lang-auto', 'lang-javascript', 'lang-php', 'lang-css', 'lang-html', 'lang-xml', 'linenums',
'ipsQuote','ipsQuote_citation', 'ipsQuote_contents',
'fa',
'ipsImage', 'ipsImage_thumbnailed', 'ipsAttachLink', 'ipsAttachLink_image',
'ipsEmbedded_image', 'ipsEmbedded_withImage', 'ipsEmbedded_headerArea', 'ipsEmbedded_type', 'ipsEmbedded', 'ipsEmbedded_error', 'ipsEmbeddedVideo', 'ipsEmbeddedVideo_limited', 'ipsEmbeddedOther',
@@ -2390,4 +2468,24 @@ class _Parser
'ipsUserPhoto_tiny',
);
}
/**
* Perform a safe html_entity_decode if you are not using UTF-8 MB4
*
* @param string $value Value to html entity decode
* @return string
*/
public static function utf8mb4SafeDecode( $value )
{
$value = html_entity_decode( $value, ENT_QUOTES, 'UTF-8' );
if ( \IPS\Settings::i()->getFromConfGlobal('sql_utf8mb4') !== TRUE )
{
$value = preg_replace_callback( '/[\x{10000}-\x{10FFFF}]/u', function( $mb4Character ) {
return mb_convert_encoding( $mb4Character[0], 'HTML-ENTITIES', 'UTF-8' );
}, $value );
}
return $value;
}
}