Version 4.6.0

This commit is contained in:
Neo committed 2025-12-19 05:55:31 -08:00
1 parent f79dcf067a
commit 517a5e1f70
2036 files changed
+110041 -26162

No files matched your search

+1 -1
View File
@@ -35,7 +35,7 @@ class _Encrypt
/**
* @brief Cipher
*/
protected $cipher;
public $cipher;
/**
* @brief IV
+2 -2
View File
@@ -33,8 +33,8 @@ class _HtmlPurifierHttpsImages extends \HTMLPurifier_URIFilter
public function filter( &$uri, $config, $context )
{
/* We only care about embedded resources for this */
if ( !$context->get( 'EmbeddedURI', true ) )
/* We only care about embedded resources for this, skip src attr so our own parser can handle that. */
if ( !$context->get( 'EmbeddedURI', true ) OR $context->get( 'CurrentAttr', true ) == 'src' )
{
return true;
}
+3 -3
View File
@@ -62,7 +62,7 @@ class _HtmlPurifierInternalLinkDef extends \HTMLPurifier_AttrDef_URI
if ( $this->allowedBases )
{
$isOkay = FALSE;
$queryString = $url->isFriendly ? ( $url->hiddenQueryString + $url->queryString ) : $url->queryString;
$queryString = ( $url instanceof \IPS\Http\Url\Friendly ) ? ( $url->hiddenQueryString + $url->queryString ) : $url->queryString;
foreach ( $this->allowedBases as $requiredQueryString )
{
@@ -126,7 +126,7 @@ class _HtmlPurifierInternalLinkDef extends \HTMLPurifier_AttrDef_URI
static::$uploadUrls = array();
foreach( \IPS\File::getConfigurations() as $configuration )
foreach( \IPS\File::getStore() as $configuration )
{
$class = \IPS\File::getClass( $configuration['id'] );
@@ -134,7 +134,7 @@ class _HtmlPurifierInternalLinkDef extends \HTMLPurifier_AttrDef_URI
{
$url = new \IPS\Http\Url( $class->baseUrl() );
if( $url->isInternal )
if( $url instanceof \IPS\Http\Url\Internal )
{
static::$uploadUrls[ $class->baseUrl() ] = $class->baseUrl();
}
+12 -4
View File
@@ -99,7 +99,7 @@ class _LegacyParser
/**
* @brief Parser object
*/
protected $parser = NULL;
public $parser = NULL;
/**
* @brief Key for attachments
@@ -126,6 +126,11 @@ class _LegacyParser
*/
protected $idThree = 0;
/**
* @brief Are Acronyms Supported?
*/
protected static $_newAcronyms = null;
/**
* Constructor
*
@@ -176,14 +181,17 @@ class _LegacyParser
}
/* The `a_type` column is added in 4.5, so we need to account for this if running the legacy parser during an older upgrade */
$newAcronyms = \IPS\Db::i()->checkForColumn( 'core_acronyms', 'a_type' );
if( static::$_newAcronyms === NULL )
{
static::$_newAcronyms = \IPS\Db::i()->checkForColumn( 'core_acronyms', 'a_type' );
}
/* Grab a new parser object */
\IPS\Text\Parser::$requestTimeout = 1;
$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, $newAcronyms );
$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, static::$_newAcronyms );
/* If we're in the upgrader and the new acronym support hasn't been added yet, populate acronyms manually */
if( !$newAcronyms )
if( !static::$_newAcronyms )
{
$this->parser->caseSensitiveAcronyms = iterator_to_array( \IPS\Db::i()->select( array( 'a_short', 'a_long', "'acronym' as `a_type`" ), 'core_acronyms', array( 'a_casesensitive=1' ) )->setKeyField( 'a_short' ) );
+337 -141
View File
@@ -26,7 +26,7 @@ class _Parser
* @brief Regex for detecting an Emoji character
* @note This string is automatically generated by the Emoji Data Builder. Do not modify it manually.
*/
const EMOJI_REGEX = '\x{1F468}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F468}|\x{1F469}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F468}|\x{1F469}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F469}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F91D}\x{200D}\x{1F9D1}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0065}\x{E006E}\x{E0067}\x{E007F}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0073}\x{E0063}\x{E0074}\x{E007F}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0077}\x{E006C}\x{E0073}\x{E007F}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F93D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F93D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F575}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F477}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2695}\x{FE0F}|\x{1F477}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F939}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F938}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F938}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F939}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9B8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F93E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F93E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2708}\x{FE0F}|\x{1F937}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9B8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9B9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9B9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9CD}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F646}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F473}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F481}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F926}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F937}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F647}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F647}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64B}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64B}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F645}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F645}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6A3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6A3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F481}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B5}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B5}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F575}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F487}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F487}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F486}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F486}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F926}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CE}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F482}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F482}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CE}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CD}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F473}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3CB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9CF}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D7}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3CA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3CA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9DA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CC}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9DA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9DB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640Line truncated
const EMOJI_REGEX = '\x{1F468}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F468}|\x{1F469}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F468}|\x{1F469}\x{200D}\x{2764}\x{FE0F}\x{200D}\x{1F48B}\x{200D}\x{1F469}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{1F91D}\x{200D}\x{1F9D1}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0065}\x{E006E}\x{E0067}\x{E007F}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0077}\x{E006C}\x{E0073}\x{E007F}|\x{1F3F4}\x{E0067}\x{E0062}\x{E0073}\x{E0063}\x{E0074}\x{E007F}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F468}\x{200D}\x{1F468}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F466}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F466}|\x{1F469}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F468}\x{200D}\x{1F469}\x{200D}\x{1F467}\x{200D}\x{1F467}|\x{1F3CA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696}\x{FE0F}|\x{1F64D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64B}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64B}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F647}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F647}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F646}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F646}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2695}\x{FE0F}|\x{1F645}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F645}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9B9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3C3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3C3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F64E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D1}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2708}\x{FE0F}|\x{1F9D6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D7}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D7}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9D8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F575}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F575}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9B9}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F93D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F93D}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F93E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F93E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9B8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F64E}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6A3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CD}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9CF}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3CC}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CC}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F939}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F938}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F938}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CE}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F937}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F937}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CE}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F935}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F935}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9CF}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6A3}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F926}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F926}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F3CA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F939}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9CD}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B6}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2695}\x{FE0F}|\x{1F6B5}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3C4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F3C4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B5}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F6B4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F6B4}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9B8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F9D8}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F468}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696}\x{FE0F}|\x{1F9DB}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2642}\x{FE0F}|\x{1F9DA}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2640}\x{FE0F}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2708}\x{FE0F}|\x{1F469}[\x{1F3FB}-\x{1F3FF}]?\x{200D}\x{2696Line truncated
/* !Parser: Bootstrap */
@@ -40,6 +40,11 @@ class _Parser
*/
protected $attachIds = NULL;
/**
* @brief Attachment Lang
*/
protected $attachIdsLang = NULL;
/**
* @brief Rows from core_attachments_map containing attachments which belong to the content being edited - as they are found by the parser, they will be removed so we are left with attachments that have been removed
*/
@@ -96,9 +101,10 @@ class _Parser
* @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 bool $parseAcronyms Parse acronyms?
* @param ?int $attachIdsLang Language ID number if this Editor is part of a Translatable field.
* @return void
*/
public function __construct( $bbcode=FALSE, $attachIds=NULL, \IPS\Member $member=NULL, $area=FALSE, $filterProfanity=TRUE, $cleanHtml=TRUE, $htmlPurifierConfig=NULL, $parseAcronyms=TRUE )
public function __construct( $bbcode=FALSE, $attachIds=NULL, \IPS\Member $member=NULL, $area=FALSE, $filterProfanity=TRUE, $cleanHtml=TRUE, $htmlPurifierConfig=NULL, $parseAcronyms=TRUE, $attachIdsLang=NULL )
{
/* Set the Member */
$this->member = $member ?: \IPS\Member::loggedIn();
@@ -117,6 +123,7 @@ class _Parser
/* Get attachments */
$this->attachIds = $attachIds;
$this->attachIdsLang = $attachIdsLang;
if( $attachIds !== NULL )
{
$where = array( array( 'location_key=?', $area ) );
@@ -211,6 +218,33 @@ class _Parser
* @return string
*/
public function parse( $value )
{
/* Clean HTML */
$value = $this->purify( $value );
/* BBCode, Profanity, etc. */
if ( $value )
{
$value = $this->_parseContent( $value );
}
/* Clean HTML */
$value = $this->purify( $value );
/* Replace any {fileStore.whatever} tags with <fileStore.whatever> */
$value = static::replaceFileStoreTags( $value );
/* Return */
return $value;
}
/**
* Parse
*
* @param string $value HTML to run through HTML purifier
* @return string
*/
public function purify( $value )
{
/* CKEditor sometimes includes these for markers. HTMLPurifier will remove the style attribute so we need to strip them first */
$value = str_replace( '<span style="display: none;">&nbsp;</span>', '', $value );
@@ -221,22 +255,6 @@ class _Parser
$value = $this->htmlPurifier->purify( $value );
}
/* BBCode, Profanity, etc. */
if ( $value )
{
$value = $this->_parseContent( $value );
}
/* Clean HTML */
if ( $value and $this->htmlPurifier )
{
$value = $this->htmlPurifier->purify( $value );
}
/* Replace any {fileStore.whatever} tags with <fileStore.whatever> */
$value = static::replaceFileStoreTags( $value );
/* Return */
return $value;
}
@@ -351,7 +369,7 @@ class _Parser
$element->setAttribute( 'width', $width );
}
$element->setAttribute( 'data-ratio', (float) round( ( $height / $width ) * 100, 2 ) );
$element->setAttribute( 'data-ratio', number_format( (float) round( ( $height / $width ) * 100, 2 ), 2, '.', '' ) );
}
}
}
@@ -557,7 +575,7 @@ class _Parser
/* CSS Definition */
$cssDefinition = $config->getCSSDefinition();
$this->_htmlPurifierModifyCssDefinition( $cssDefinition );
$this->_htmlPurifierModifyCssDefinition( $cssDefinition, $config );
if( \IPS\Settings::i()->allow_remote_images AND \IPS\Settings::i()->allow_only_https_remote_images )
{
@@ -663,14 +681,25 @@ class _Parser
/* Picture tag - we don't use it, but RSS imports might */
$def->addElement( 'picture', 'Inline', 'Optional: (source, Flow) | (Flow, source) | Flow', 'Common', array() );
}
/**
* @brief Maximum allowed width/height px values (to prevent causing page layout oddities)
*/
protected $cssMaxWidthHeight = 1000;
/**
* @brief Maximum allowed border width px value (to prevent causing page layout oddities)
*/
protected $cssMaxBorderWidth = 50;
/**
* Customize HTML Purifier CSS Definition
*
* @param HTMLPurifier_HTMLDefinition $def The definition
* @param HTMLPurifier_Config $config HTML Purifier configuration object
* @return void
*/
protected function _htmlPurifierModifyCssDefinition( \HTMLPurifier_CSSDefinition $def )
protected function _htmlPurifierModifyCssDefinition( \HTMLPurifier_CSSDefinition $def, \HTMLPurifier_Config $config )
{
/* Do not allow negative margins */
$margin = $def->info['margin-right'] = $def->info['margin-left'] = $def->info['margin-bottom'] = $def->info['margin-top'] = new \HTMLPurifier_AttrDef_CSS_Composite(
@@ -686,6 +715,28 @@ class _Parser
$def->info['white-space'] = new \HTMLPurifier_AttrDef_Enum(
array( 'normal', 'pre', 'pre-wrap', 'pre-line')
);
/* Limit the maximum width and height allowed */
$def->info['width'] = $def->info['height'] = new \HTMLPurifier_AttrDef_CSS_Composite( array(
new \HTMLPurifier_AttrDef_CSS_Length( '0px', $this->cssMaxWidthHeight . 'px' ),
new \HTMLPurifier_AttrDef_CSS_Percentage( true ),
new \HTMLPurifier_AttrDef_Enum( array( 'auto' ) )
) );
/* Limit the maximum border width allowed */
$border_width =
$def->info['border-top-width'] =
$def->info['border-bottom-width'] =
$def->info['border-left-width'] =
$def->info['border-right-width'] = new \HTMLPurifier_AttrDef_CSS_Composite(array(
new \HTMLPurifier_AttrDef_Enum( array( 'thin', 'medium', 'thick' ) ),
new \HTMLPurifier_AttrDef_CSS_Length( '0px', $this->cssMaxBorderWidth . 'px' )
) );
$def->info['border-width'] = new \HTMLPurifier_AttrDef_CSS_Multiple( $border_width );
/* We have to reset this so the constructor picks up the new values we just specified */
$def->info['border'] = $def->info['border-bottom'] = $def->info['border-top'] = $def->info['border-left'] = $def->info['border-right'] = new \HTMLPurifier_AttrDef_CSS_Border( $config );
}
/**
@@ -758,8 +809,6 @@ class _Parser
'www.screencast.com/users/',
'fast.wistia.net/embed/',
'www.screencast.com/users/',
'clips.twitch.tv/embed',
'player.twitch.tv/'
) );
/* Extra admin-defined options */
@@ -1943,7 +1992,7 @@ class _Parser
// removing the src attrib causes a FOUC in firefox, so we need to set it to an empty page instead (see #2178)
$element->setAttribute( 'src', static::blankPage() );
}
else if ( $src->isInternal )
else if ( $src instanceof \IPS\Http\Url\Internal )
{
/* If this is an internal embed, replace src and controller for lazy loading */
if ( \IPS\Settings::i()->lazy_load_enabled )
@@ -1988,14 +2037,34 @@ class _Parser
if( mb_stristr( $imageSrc, (string) $imageProxyUrl ) )
{
$srcUrl = \IPS\Http\Url::createFromString( str_replace( '<___base_url___>', rtrim( \IPS\Settings::i()->base_url, '/' ), $imageSrc ) );
$element->setAttribute( $attributeName, static::_getProxiedImageUrl( $srcUrl->queryString['img'], $useProxyUrl ) );
try
{
$srcUrl = \IPS\Http\Url::createFromString( str_replace( array( '<___base_url___>', '%7B___base_url___%7D' ), rtrim( \IPS\Settings::i()->base_url, '/' ), $imageSrc ) )->queryString['img'];
}
catch( \IPS\Http\Url\Exception $e )
{
parse_str( parse_url( str_replace( array( '<___base_url___>', '%7B___base_url___%7D' ), rtrim( \IPS\Settings::i()->base_url, '/' ), $imageSrc ), PHP_URL_QUERY ), $queryString );
$srcUrl = static::_getProxiedImageUrl( $queryString['img'], $useProxyUrl );
}
$element->setAttribute( $attributeName, static::_getProxiedImageUrl( $srcUrl, $useProxyUrl ) );
/* We also need to remove image proxy from the parent A tag if it exists */
if( $element->parentNode->tagName === 'a' AND mb_stristr( $element->parentNode->getAttribute( 'href' ), (string) $imageProxyUrl ) )
{
$hrefSrcUrl = \IPS\Http\Url::createFromString( str_replace( '<___base_url___>', rtrim( \IPS\Settings::i()->base_url, '/' ), $element->parentNode->getAttribute( 'href' ) ) );
$element->parentNode->setAttribute( 'href', static::_getProxiedImageUrl( $hrefSrcUrl->queryString['img'], $useProxyUrl ) );
try
{
$hrefSrcUrl = \IPS\Http\Url::createFromString( str_replace( array( '<___base_url___>', '%7B___base_url___%7D' ), rtrim( \IPS\Settings::i()->base_url, '/' ), $element->parentNode->getAttribute( 'href' ) ) )->queryString['img'];
}
catch( \IPS\Http\Url\Exception $e )
{
parse_str( parse_url( str_replace( array( '<___base_url___>', '%7B___base_url___%7D' ), rtrim( \IPS\Settings::i()->base_url, '/' ), $element->parentNode->getAttribute( 'href' ) ), PHP_URL_QUERY ), $queryString );
$srcUrl = static::_getProxiedImageUrl( $queryString['img'], $useProxyUrl );
}
$element->parentNode->setAttribute( 'href', static::_getProxiedImageUrl( $hrefSrcUrl, $useProxyUrl ) );
}
}
@@ -2017,28 +2086,26 @@ class _Parser
/* If for some reason we're processing an existing imageproxy URL, set the full URL. */
try
{
$imageSrcUrl = \IPS\Http\Url::createFromString( str_replace( '<___base_url___>', rtrim( \IPS\Settings::i()->base_url, '/' ), $data[0] ) );
$imageSrcUrl = \IPS\Http\Url::createFromString( str_replace( array( '<___base_url___>', '%7B___base_url___%7D' ), rtrim( \IPS\Settings::i()->base_url, '/' ), $data[0] ) );
if( isset( $imageSrcUrl->queryString['img'] ) )
{
$imageSrcUrl = $imageSrcUrl->queryString['img'];
}
else
{
$imageSrcUrl = $data[0];
}
}
catch( \IPS\Http\Url\Exception $e )
{
/* Make sure the URL doesn't start with a local placeholder. See comment above. */
if ( $e->getMessage() === 'INVALID_URL' AND preg_match( '#^({|%7B|\<|%3C)fileStore\.#', $data[0] ) )
{
/* If it does, it's fine. Above we simply return, however here we are dealing with multiple URL's, so add it to the fixed list to be safe. */
$fixedUrls[] = $data[0] . ( !empty( $data[1] ) ? ' ' . $data[1] : '' );
continue;
}
/* Still here? Something else is wrong, so throw */
throw $e;
/* If we're here just return the original URL. Legacy data can result in many different problems that just can't be accounted for in every case. */
parse_str( parse_url( str_replace( array( '<___base_url___>', '%7B___base_url___%7D' ), rtrim( \IPS\Settings::i()->base_url, '/' ), $data[0] ), PHP_URL_QUERY ), $queryString );
$imageSrcUrl = ( isset( $queryString['img'] ) ) ? $queryString['img'] : $data[0];
}
if( !isset( $imageSrcUrl->queryString['img'] ) )
{
continue;
}
$fixedUrls[] = static::_getProxiedImageUrl( $imageSrcUrl->queryString['img'], $useProxyUrl ) . ( ! empty( $data[1] ) ? ' ' . $data[1] : '' );
$fixedUrls[] = static::_getProxiedImageUrl( $imageSrcUrl, $useProxyUrl ) . ( ! empty( $data[1] ) ? ' ' . $data[1] : '' );
}
}
@@ -2064,6 +2131,12 @@ class _Parser
return $url;
}
/* If the table no longer exists because it was dropped but we are re-running a remove image proxy step, just use original URL */
if( !\IPS\Db::i()->checkForTable( 'core_image_proxy' ) )
{
return $url;
}
try
{
$cacheEntry = \IPS\Db::i()->select( '*', 'core_image_proxy', array( 'md5_url=?', md5( $url ) ) )->first();
@@ -2114,7 +2187,7 @@ class _Parser
}
/* We add external/nofollow rel attributes for non-internal non-local links */
if ( !$url->isInternal )
if ( !( $url instanceof \IPS\Http\Url\Internal ) )
{
$rels[] = 'external';
@@ -2328,7 +2401,7 @@ class _Parser
}
/* If it's URL to applications/core/interface/file/attachment.php, it's definitely an attachment */
if ( preg_match( '#^(?:http:|https:)?' . preg_quote( rtrim( str_replace( array( 'http://', 'https://' ), '//', \IPS\Settings::i()->base_url ), '/' ), '#' ) . '/applications/core/interface/file/attachment\.php\?id=(\d+)$#', $url, $matches ) )
if ( preg_match( '#^(?:http:|https:)?' . preg_quote( rtrim( str_replace( array( 'http://', 'https://' ), '//', \IPS\Settings::i()->base_url ), '/' ), '#' ) . '/applications/core/interface/file/attachment\.php\?id=(\d+)(?:&key=[a-f0-9]{32})?$#', $url, $matches ) )
{
try
{
@@ -2375,15 +2448,16 @@ class _Parser
}
elseif ( $attachment['attach_member_id'] === (int) $this->member->member_id and !\in_array( $attachment['attach_id'], $this->mappedAttachments ) )
{
if( $this->area )
{
if( $this->area and !isset( \IPS\Request::i()->_previewField ) )
{
\IPS\Db::i()->replace( 'core_attachments_map', array(
'attachment_id' => $attachment['attach_id'],
'location_key' => $this->area,
'id1' => ( \is_array( $this->attachIds ) and isset( $this->attachIds[0] ) ) ? $this->attachIds[0] : NULL,
'id2' => ( \is_array( $this->attachIds ) and isset( $this->attachIds[1] ) ) ? $this->attachIds[1] : NULL,
'id3' => ( \is_array( $this->attachIds ) and isset( $this->attachIds[2] ) ) ? $this->attachIds[2] : NULL,
'temp' => \is_string( $this->attachIds ) ? $this->attachIds : NULL
'temp' => \is_string( $this->attachIds ) ? $this->attachIds : NULL,
'lang' => $this->attachIdsLang
) );
}
@@ -3066,7 +3140,7 @@ class _Parser
*/
protected static function oembedServices()
{
return array(
$services = array(
'youtube.com' => array( 'https://www.youtube.com/oembed', static::EMBED_VIDEO ),
'm.youtube.com' => array( 'https://www.youtube.com/oembed', static::EMBED_VIDEO ),
'youtu.be' => array( 'https://www.youtube.com/oembed', static::EMBED_VIDEO ),
@@ -3077,17 +3151,11 @@ class _Parser
'collegehumor.com' => array( 'http://www.collegehumor.com/oembed.json', static::EMBED_VIDEO ),
'twitter.com' => array( 'https://publish.twitter.com/oembed', static::EMBED_TWEET ),
'mobile.twitter.com' => array( 'https://publish.twitter.com/oembed', static::EMBED_TWEET ),
'instagr.am' => array( 'https://api.instagram.com/oembed', static::EMBED_IMAGE ),
'instagram.com' => array( 'https://api.instagram.com/oembed', static::EMBED_IMAGE ),
'soundcloud.com' => array( 'https://soundcloud.com/oembed', static::EMBED_VIDEO ),
'open.spotify.com' => array( 'https://embed.spotify.com/oembed', static::EMBED_VIDEO ),
'play.spotify.com' => array( 'https://embed.spotify.com/oembed', static::EMBED_VIDEO ),
'ted.com' => array( 'https://www.ted.com/services/v1/oembed', static::EMBED_VIDEO ),
'vine.co' => array( 'https://vine.co/oembed.json', static::EMBED_VIDEO ),
'facebook.com' => array(
'(\/.+?\/videos\/|video.php)' => array( 'https://www.facebook.com/plugins/video/oembed.json', static::EMBED_VIDEO ),
0 => array( 'https://www.facebook.com/plugins/post/oembed.json', static::EMBED_STATUS ),
),
'dailymotion.com' => array( 'https://www.dailymotion.com/services/oembed', static::EMBED_VIDEO ),
'coub.com' => array( 'http://coub.com/api/oembed.json' . ( \IPS\Settings::i()->max_video_width ? ( '?maxwidth=' . \IPS\Settings::i()->max_video_width ) : '' ), static::EMBED_VIDEO ),
'*.deviantart.com' => array( 'https://backend.deviantart.com/oembed', static::EMBED_IMAGE ),
@@ -3106,10 +3174,60 @@ class _Parser
'*.smugmug.com' => array( 'http://api.smugmug.com/services/oembed', static::EMBED_IMAGE ),
'ustream.tv' => array( 'https://www.ustream.tv/oembed', static::EMBED_VIDEO ),
'*.wistia.com' => array( 'http://fast.wistia.com/oembed', static::EMBED_VIDEO ),
'*.wi.st' => array( 'http://fast.wistia.com/oembed', static::EMBED_VIDEO ),
'clips.twitch.tv' => array( 'https://api.twitch.tv/v5/oembed', static::EMBED_VIDEO ),
'*.wi.st' => array( 'http://fast.wistia.com/oembed', static::EMBED_VIDEO ),
'tiktok.com' => array( 'https://www.tiktok.com/oembed', static::EMBED_VIDEO ),
'm.tiktok.com' => array( 'https://www.tiktok.com/oembed', static::EMBED_VIDEO ),
'vm.tiktok.com' => array( 'https://www.tiktok.com/oembed', static::EMBED_VIDEO ),
);
/* Can we support Facebook and Instagram oembeds? */
if( $token = static::getFacebookToken() )
{
$services['instagram.com'] = array(
array( 'https://graph.facebook.com/v9.0/instagram_oembed?access_token=' . $token, static::EMBED_IMAGE )
);
$services['instagr.am'] = $services['instagram.com'];
$services['facebook.com'] = array(
'(\/.+?\/videos\/|video.php)' => array( 'https://graph.facebook.com/v9.0/oembed_video?access_token=' . $token, static::EMBED_VIDEO ),
'(\/.+?\/posts\/|\/.+?\/activity\/|\/.+?\/photos?\/|photo.php|\/.+?\/media\/|permalink.php|\/.+?\/questions\/|\/.+?\/notes\/|\/.+?\/media\/)' => array( 'https://graph.facebook.com/v9.0/oembed_post?access_token=' . $token, static::EMBED_STATUS ),
0 => array( 'https://graph.facebook.com/v9.0/oembed_page?access_token=' . $token, static::EMBED_STATUS ),
);
}
return $services;
}
/**
* Get an application token from Facebook for oembed support
*
* @return NULL|string
*/
protected static function getFacebookToken()
{
try
{
if( !\IPS\Settings::i()->fb_ig_oembed_token AND \IPS\Settings::i()->fb_ig_oembed_appid AND \IPS\Settings::i()->fb_ig_oembed_appsecret )
{
$value = \IPS\Http\Url::external( "https://graph.facebook.com/oauth/access_token?client_id=" . \IPS\Settings::i()->fb_ig_oembed_appid . "&client_secret=" . \IPS\Settings::i()->fb_ig_oembed_appsecret . "&grant_type=client_credentials" )->request()->get()->decodeJson();
if( !empty( $value['access_token'] ) )
{
\IPS\Settings::i()->changeValues( array( 'fb_ig_oembed_token' => $value['access_token'] ) );
}
else
{
\IPS\Log::debug( "Facebook access token could not be retrieved for oembed:\n" . var_export( $value, true ), 'facebook_oembed' );
}
}
}
catch( \Exception $e )
{
\IPS\Log::debug( $e, 'facebook_oembed' );
}
return \IPS\Settings::i()->fb_ig_oembed_token ? \IPS\Settings::i()->fb_ig_oembed_token : NULL;
}
/**
@@ -3128,7 +3246,7 @@ class _Parser
public static function embeddableMedia( $url, $iframe=FALSE, $member=NULL )
{
/* Internal */
if ( $url->isInternal )
if ( $url instanceof \IPS\Http\Url\Internal )
{
/* Internal Embed */
if ( $embedCode = static::_internalEmbed( $url, $iframe, $member ) )
@@ -3169,11 +3287,12 @@ class _Parser
/**
* oEmbed Embed Code
*
* @param \IPS\Http\Url $url URL
* @param bool $iframe Some services need to be in iFrames so they cannot be edited in the editor. If TRUE, will return contents for iframe, if FALSE, return the iframe.
* @param \IPS\Http\Url $url URL
* @param bool $iframe Some services need to be in iFrames so they cannot be edited in the editor. If TRUE, will return contents for iframe, if FALSE, return the iframe.
* @param int $attemptNumber The attempt number, useful for automatically retrying a service
* @return string|null
*/
protected static function _oembedEmbed( \IPS\Http\Url $url, $iframe=FALSE )
protected static function _oembedEmbed( \IPS\Http\Url $url, $iframe=FALSE, $attemptNumber=1 )
{
/* Strip the "www." from the domain */
$domain = $url->data['host'];
@@ -3186,10 +3305,27 @@ class _Parser
return null;
}
/* Twitch clips will use www.twitch.tv, but support oembed - however other twitch URLs are treated special so we can't just manipulate the entire URL */
if( $domain == 'twitch.tv' AND mb_strpos( $url->data['path'], '/clip/' ) !== FALSE )
/* TikTok does not support oembed for their short links, but will redirect to the full URL which does */
if( $domain == 'vm.tiktok.com' )
{
$domain = 'clips.twitch.tv';
try
{
$response = $url->request( null, null, 0 )->get();
if( $response->httpResponseCode == 301 )
{
$url = \IPS\Http\Url::external( $response->httpHeaders['Location'] )->stripQueryString();
$domain = $url->data['host'];
}
else
{
throw new \IPS\Http\Request\Exception;
}
}
catch( \IPS\Http\Request\Exception $e )
{
throw new \UnexpectedValueException( 'embed__fail_500', (float) static::EMBED_VIDEO );
}
}
/* TikTok doesn't return logical response codes for invalid URLs */
@@ -3197,16 +3333,22 @@ class _Parser
{
return NULL;
}
elseif( $domain == 'm.tiktok.com' AND mb_strpos( $url->data['path'], '/v/' ) === FALSE )
{
return NULL;
}
/* Get oEmbed Services */
$oembedServices = static::oembedServices();
/* If the URL's domain is in the list... */
$entry = NULL;
$entry = NULL;
$entryKey = NULL;
if ( array_key_exists( $domain, $oembedServices ) )
{
$entry = $oembedServices[ $domain ];
$entry = $oembedServices[ $domain ];
$entryKey = $domain;
}
else
{
@@ -3216,7 +3358,8 @@ class _Parser
{
if( preg_match( "/^" . str_replace( array( '.', '*' ), array( '\\.', '.*?' ), $k ) . "$/i", $domain ) )
{
$entry = $v;
$entry = $v;
$entryKey = $k;
}
}
}
@@ -3251,7 +3394,14 @@ class _Parser
$endpoint = $entry[0];
$endtype = $entry[1];
}
/* Youtube shorts feature doesn't return a valid response in its own oembed API so we need to tweak it */
if ( ( $domain == 'youtube.com' or $domain == 'm.youtube.com' or $domain == 'youtu.be' ) and mb_strpos( $url->data['path'], '/shorts/' ) !== FALSE)
{
$url->queryString['v'] = preg_replace( '#/shorts/(\w+?)#', '$1', $url->data['path'] );
$url->data['path'] = '/watch';
}
/* Call oEmbed Service */
try
{
@@ -3269,6 +3419,13 @@ class _Parser
if( $response->httpResponseCode != '200' )
{
/* If this is FB/IG we can reattempt one more time in case the stored access token was incorrect or expired */
if( \in_array( $entryKey, array( 'instagram.com', 'instagr.am', 'facebook.com' ) ) AND $attemptNumber < 2 )
{
\IPS\Settings::i()->changeValues( array( 'fb_ig_oembed_token' => '' ) );
return static::_oembedEmbed( $url, $iframe, 2 );
}
switch( $response->httpResponseCode )
{
case '404':
@@ -3293,19 +3450,7 @@ class _Parser
{
throw new \UnexpectedValueException( 'embed__fail_500', (float) $endtype );
}
/* Facebook videos report as video but they're not HTML5 videos with the aspect ratio we need, so treat it as rich */
if ( $domain == 'facebook.com' and preg_match( '(\/.+?\/videos\/|video.php)', $url ) )
{
$response['type'] = 'rich';
}
/* For Facebook, we need to be a bit hacky to make it responsive */
if ( $domain == 'facebook.com' )
{
$response['html'] = preg_replace( '/data\-width="\d*"/', 'data-width="auto"', $response['html'] );
}
/* Flickr when used in the video template is a bit quirky. Requires rich. */
if ( $domain == 'flickr.com' OR $domain == 'flic.kr' )
{
@@ -3345,7 +3490,7 @@ class _Parser
}
/* Tiktok tries to embed a <script src="https://tiktok.com/embed.js"></script> tag which isn't triggered by CKEditor and doesn't survive HTMLPurifier, so force it to load in an iframe */
if ( $domain == 'tiktok.com' )
if ( $domain == 'tiktok.com' OR $domain == 'm.tiktok.com' )
{
$response['type'] = 'rich';
}
@@ -3668,24 +3813,6 @@ class _Parser
}
}
/* Twitch TV Live Streams*/
if ( preg_match( '/^https:\/\/www\.?twitch\.tv\/(.+)/i', (string) $url, $matches ) )
{
$qbits = explode( "/", $matches[1] );
/* Work out if we are embedding a collection, video or channel */
if( isset( $qbits[1] ) )
{
$type = ( $qbits[0] == 'collections' ) ? 'collection' : 'video';
return \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->twitch( $type, $qbits[1] );
}
else
{
return \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->twitch( 'channel', $qbits[0] );
}
}
/* So if it's not that, just return */
return NULL;
}
@@ -3808,25 +3935,6 @@ class _Parser
throw new \UnexpectedValueException( 'embed__fail_500', static::EMBED_LINK );
}
/* Strip the CSRF key if present - normally shouldnt't be, but just in case..*/
$preview = $preview->setQueryString( 'do', 'embed' )->stripQueryString( 'csrfKey' );
if( isset( $url->queryString['do'] ) and $url->queryString['do'] == 'findComment' )
{
$preview = $preview->setQueryString( 'embedComment', $url->queryString['comment'] );
}
elseif( isset( $url->queryString['do'] ) and $url->queryString['do'] == 'findReview' )
{
$preview = $preview->setQueryString( 'embedReview', $url->queryString['review'] );
}
if( isset( $url->queryString['do'] ) )
{
$preview = $preview->setQueryString( 'embedDo', $url->queryString['do'] );
}
if ( isset( $url->queryString['page'] ) AND $url->queryString['page'] > 1 )
{
$preview = $preview->setPage( 'page', $url->queryString['page'] );
}
/* Add the author ID for notifications */
try
{
@@ -3844,6 +3952,45 @@ class _Parser
$author = 0;
}
/* Strip the CSRF key if present - normally shouldnt't be, but just in case..*/
$preview = $preview->setQueryString( 'do', 'embed' )->stripQueryString( 'csrfKey' );
if( isset( $url->queryString['do'] ) and $url->queryString['do'] == 'findComment' )
{
$preview = $preview->setQueryString( 'embedComment', $url->queryString['comment'] );
/* Get the correct author ID */
$commentClass = $item::$commentClass;
try
{
$comment = $commentClass::loadAndCheckPerms( $url->queryString['comment'] );
$author = $comment->author()->member_id;
}
catch( \Exception $e ){}
}
elseif( isset( $url->queryString['do'] ) and $url->queryString['do'] == 'findReview' )
{
$preview = $preview->setQueryString( 'embedReview', $url->queryString['review'] );
/* Get the correct author ID */
$reviewClass = $item::$reviewClass;
try
{
$review = $reviewClass::loadAndCheckPerms( $url->queryString['review'] );
$author = $review->author()->member_id;
}
catch( \Exception $e ){}
}
if( isset( $url->queryString['do'] ) )
{
$preview = $preview->setQueryString( 'embedDo', $url->queryString['do'] );
}
if ( isset( $url->queryString['page'] ) AND $url->queryString['page'] > 1 )
{
$preview = $preview->setPage( 'page', $url->queryString['page'] );
}
/* And return */
return "<iframe src='{$preview}' data-embedContent data-controller='core.front.core.autosizeiframe' data-embedauthorid='{$author}' allowfullscreen=''></iframe>";
}
@@ -3877,27 +4024,31 @@ class _Parser
$widthToUse = $width;
$heightToUse = $height;
/* Adjust the width/height according to our maximum dimensions */
if ( $width > $maxImageDims[0] )
/* 0x0 means unlimited, so only do these calculations if a specific size has been set */
if( \intval( $maxImageDims[0] ) !== 0 || \intval( $maxImageDims[1] ) !== 0 )
{
$widthToUse = $maxImageDims[0];
$heightToUse = floor( $height / $width * $widthToUse );
if ( $heightToUse > $maxImageDims[1] )
/* Adjust the width/height according to our maximum dimensions */
if ( $width > $maxImageDims[0] )
{
$widthToUse = floor( $maxImageDims[1] * ( $widthToUse / $heightToUse ) );
$heightToUse = $maxImageDims[1];
$widthToUse = $maxImageDims[0];
$heightToUse = floor( $height / $width * $widthToUse );
if ( $heightToUse > $maxImageDims[1] )
{
$widthToUse = floor( $maxImageDims[1] * ( $widthToUse / $heightToUse ) );
$heightToUse = $maxImageDims[1];
}
}
}
elseif( $height > $maxImageDims[1] )
{
$heightToUse = $maxImageDims[1];
$widthToUse = floor( $width / $height * $heightToUse );
if ( $widthToUse > $maxImageDims[0] )
elseif( $height > $maxImageDims[1] )
{
$heightToUse = floor( $maxImageDims[0] * ( $heightToUse / $widthToUse ) );
$widthToUse = $maxImageDims[0];
$heightToUse = $maxImageDims[1];
$widthToUse = floor( $width / $height * $heightToUse );
if ( $widthToUse > $maxImageDims[0] )
{
$heightToUse = floor( $maxImageDims[0] * ( $heightToUse / $widthToUse ) );
$widthToUse = $maxImageDims[0];
}
}
}
@@ -4089,11 +4240,11 @@ class _Parser
{
/* It's not, so create one and add the image to it */
$parent = $image->parentNode;
$clonedImage = clone $image;
$anchor = $content->createElement( 'a' );
$anchor->setAttribute( 'href', '{fileStore.core_Attachment}/' . $attachment['attach_location'] );
$anchor->appendChild( $image );
$parent->appendChild( $anchor );
$parent->removeChild( $image );
$anchor->appendChild( $clonedImage );
$parent->replaceChild( $anchor, $image );
}
else
{
@@ -4168,6 +4319,51 @@ class _Parser
return FALSE;
}
/**
* Rebuild rel tag contents for posts
*
* @param string $textContent The content of the text, and they say comments aren't useful
* @param \IPS\Member $member The author of the content
* @return mixed FALSE or changed content
*/
public static function rebuildUrlRels( $textContent, $member )
{
$obj = new static( TRUE, NULL, $member );
$rebuilt = FALSE;
/* Create DOMDocument */
$content = new \IPS\Xml\DOMDocument( '1.0', 'UTF-8' );
@$content->loadHTML( \IPS\Xml\DOMDocument::wrapHtml( $textContent ) );
$xpath = new \DOMXpath( $content );
foreach( $xpath->query('//a') as $link )
{
if ( $link->getAttribute('href') )
{
try
{
$url = \IPS\Http\Url::createFromString( str_replace( array( '<___base_url___>', '%7B___base_url___%7D' ), rtrim( \IPS\Settings::i()->base_url, '/' ), $link->getAttribute('href') ) );
$rels = $obj->_getRelAttributes( $url );
$link->setAttribute( 'rel', implode( ' ', $rels ) );
$rebuilt = TRUE;
}
catch( \Exception $e ) { }
}
}
if ( $rebuilt )
{
$value = $content->saveHTML();
$value = preg_replace( '/<meta http-equiv(?:[^>]+?)>/i', '', preg_replace( '/^<!DOCTYPE.+?>/', '', str_replace( array( '<html>', '</html>', '<body>', '</body>', '<head>', '</head>' ), '', $value ) ) );
/* DOMDocument::saveHTML will encode the base_url brackets, so we need to make sure it's in the expected format. */
return str_replace( '&lt;___base_url___&gt;', '<___base_url___>', $value );
}
return FALSE;
}
/**
* Perform a safe html_entity_decode if you are not using UTF-8 MB4
*