Invision Power Services, Inc.
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
* @license http://www.invisionpower.com/legal/standards/
* @package IPS Community 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;
}
/**
* Text Parser
*/
class _Parser
{
/* !Parser: Bootstrap */
/**
* @brief If parsing BBCode, the supported BBCode tags
*/
protected $bbcode = NULL;
/**
* @brief Attachment IDs
*/
protected $attachIds = 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
*/
public $existingAttachments = array();
/**
* @brief Attachment IDs
*/
public $mappedAttachments = array();
/**
* @brief If parsing BBCode or attachments, the member posting
*/
protected $member = NULL;
/**
* @brief If parsing BBCode or attachments, the Editor area we're parsing in. e.g. "core_Signatures". A boolean value will allow or disallow all BBCodes that are dependant on area.
*/
protected $area = NULL;
/**
* @brief Loose Profanity Filters
*/
protected $looseProfanity = array();
/**
* @brief Exact Profanity Filters
*/
protected $exactProfanity = array();
/**
* @brief Case-sensitive Acronyms
*/
protected $caseSensitiveAcronyms = array();
/**
* @brief Case-insensitive Acronyms
*/
protected $caseInsensitiveAcronyms = array();
/**
* @brief If cleaning HTML, the HTMLPurifier object
*/
protected $htmlPurifier = NULL;
/**
* Constructor
*
* @param bool $bbcode Parse BBCode?
* @param array|null $attachIds array of ID numbers to idenfity content for attachments if the content has been saved - the first two must be int or null, the third must be string or null. If content has not been saved yet, an MD5 hash used to claim attachments after saving.
* @param \IPS\Member|null $member The member posting, NULL will use currently logged in member.
* @param string|bool $area If parsing BBCode or attachments, the Editor area we're parsing in. e.g. "core_Signatures". A boolean value will allow or disallow all BBCodes that are dependant on area.
* @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 bool $parseAcronyms Parse acronyms?
* @return void
*/
public function __construct( $bbcode=FALSE, $attachIds=NULL, \IPS\Member $member=NULL, $area=FALSE, $filterProfanity=TRUE, $cleanHtml=TRUE, $htmlPurifierConfig=NULL, $parseAcronyms=TRUE )
{
/* Set the Member */
$this->member = $member ?: \IPS\Member::loggedIn();
/* Set the member and area */
if ( $bbcode or $attachIds )
{
$this->area = $area;
}
/* Get available BBCodes */
if ( $bbcode )
{
$this->bbcode = static::bbcodeTags( $this->member, $this->area );
}
/* Get attachments */
$this->attachIds = $attachIds;
if ( is_array( $attachIds ) )
{
$where = array( array( 'location_key=?', $area ) );
$i = 1;
foreach ( $attachIds as $id )
{
$where[] = array( "id{$i}=?", $id );
$i++;
}
$this->existingAttachments = iterator_to_array( \IPS\Db::i()->select( '*', 'core_attachments_map', $where )->setKeyField( 'attachment_id' ) );
$this->mappedAttachments = array_keys( $this->existingAttachments );
}
/* Get profanity filters */
if ( $filterProfanity )
{
foreach( \IPS\core\Profanity::getProfanity() AS $profanity )
{
if ( $profanity->action == 'swap' )
{
if ( $profanity->m_exact )
{
$this->exactProfanity[ $profanity->type ] = $profanity->swop;
}
else
{
$this->looseProfanity[ $profanity->type ] = $profanity->swop;
}
}
}
}
/* Get HTMLPurifier Configuration */
if ( $cleanHtml )
{
if ( !function_exists('idn_to_ascii') )
{
\IPS\IPS::$PSR0Namespaces['TrueBV'] = \IPS\ROOT_PATH . "/system/3rd_party/php-punycode";
require_once \IPS\ROOT_PATH . "/system/3rd_party/php-punycode/polyfill.php";
}
require_once \IPS\ROOT_PATH . "/system/3rd_party/HTMLPurifier/HTMLPurifier.auto.php";
$this->htmlPurifier = new \HTMLPurifier( $this->_htmlPurifierConfiguration( $htmlPurifierConfig ) );
}
/* Get acronyms */
if ( $parseAcronyms )
{
$this->caseSensitiveAcronyms = iterator_to_array( \IPS\Db::i()->select( array( 'a_short', 'a_long' ), 'core_acronyms', array( 'a_casesensitive=1' ) )->setKeyField( 'a_short' )->setValueField( 'a_long' ) );
$this->caseInsensitiveAcronyms = array();
foreach ( \IPS\Db::i()->select( array( 'a_short', 'a_long' ), 'core_acronyms', array( 'a_casesensitive=0' ) )->setKeyField( 'a_short' )->setValueField( 'a_long' ) as $k => $v )
{
$this->caseInsensitiveAcronyms[ mb_strtolower( $k ) ] = $v;
}
}
}
/**
* Parse
*
* @param string $value HTML to parse
* @return string
*/
public function parse( $value )
{
/* CKEditor sometimes includes these for markers. HTMLPurifier will remove the style attribute so we need to strip them first */
$value = str_replace( '', '', $value );
/* Clean HTML */
if ( $value and $this->htmlPurifier )
{
$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 [tag] Content [/tag]
mode where BBCode tags are wrapped in P tags like so:
tags inside block BBCode tags, so our example ends up parsing like so:
[tag]
Content
[/tag]
tags are converted to
to prevent parser confusion */
preg_match_all( '#\[(' . implode( '|', $blockTags ) . ')\](.+?)\[/\1\]#si', $value, $matches, PREG_SET_ORDER );
foreach( $matches as $id => $match )
{
$value = str_replace( $match[0], preg_replace( '#
]+?)?'.'>#i', '
', $match[0] ), $value );
}
}
}
/* Parse */
$parser = new DOMParser( array( $this, '_parseDomElement' ), array( $this, '_parseDomText' ) );
$document = $parser->parseValueIntoDocument( $value );
/* [page] tags need to be handled specially */
if ( $this->bbcode !== NULL and static::canUse( $this->member, 'Page', $this->area ) and $this->containsPageTags )
{
$body = DOMParser::getDocumentBody( $document );
$bodyWithPages = $this->_parseContentWithSeparationTag(
$body,
function ( \DOMDocument $document ) {
$mainDiv = $document->createElement('div');
$mainDiv->setAttribute( 'data-controller', 'core.front.core.articlePages' );
return $mainDiv;
},
function ( \DOMDocument $document ) {
$subDiv = $document->createElement('div');
$subDiv->setAttribute( 'data-role', 'contentPage' );
$hr = $document->createElement('hr');
$hr->setAttribute( 'data-role', 'contentPageBreak' );
$subDiv->appendChild( $hr );
return $subDiv;
},
'[page]'
);
$newBody = new \DOMElement('body');
$body->parentNode->replaceChild( $newBody, $body );
$newBody->appendChild( $bodyWithPages );
}
/* Return */
return DOMParser::getDocumentBodyContents( $document );
}
/**
* Parse HTML element (e.g. ,
, , etc.)
*
* @param \DOMElement $element The element from the source document to parse
* @param \DOMNode $parent The node from the new document which will be this node's parent
* @param \IPS\Text\DOMParser $parser DOMParser Object
* @return void
*/
public function _parseDomElement( \DOMElement $element, \DOMNode $parent, \IPS\Text\DOMParser $parser )
{
/* Adjust parent for block BBCode */
$this->_adjustParentForBlockBBCodeAtStartOfNode( $parent );
/* Start of an ? */
$okayToParse = TRUE;
if ( $element->tagName === 'abbr' and $element->hasAttribute('title') )
{
$title = $element->getAttribute('title');
if ( !in_array( $title, $this->openAbbrTags ) )
{
$this->openAbbrTags[] = $title;
}
else
{
$okayToParse = FALSE;
}
}
/* Import */
if ( $okayToParse )
{
/* Import the element as it is */
$ownerDocument = $parent->ownerDocument ?: $parent;
$newElement = $ownerDocument->importNode( $element );
/* Element-specific parsing */
if ( $okayToParse )
{
$this->_parseElement( $newElement );
}
/* Append */
$parent->appendChild( $newElement );
/* Swap out emoticons that should be plaintext (meaning we hit the maximum limit of emoticons per editor) */
foreach( $parent->getElementsByTagName( 'img' ) AS $img )
{
if ( $img->hasAttribute( 'data-ipsEmoticon-plain' ) )
{
$replace = $parent->appendChild( new \DOMText( $img->getAttribute( 'data-ipsEmoticon-plain' ) ) );
$parent->replaceChild( $replace, $img );
}
}
/* ) to go in the block-level element for any content left (e.g. " [center]This needs to be centered [/center]This should not be centered tags don't parse BBCode inside */
$resumeBBCodeAfterPre = FALSE;
if ( $newElement->tagName == 'pre' and $this->bbcodeParse )
{
$this->bbcodeParse = FALSE;
$resumeBBCodeAfterPre = TRUE;
}
}
else
{
$newElement = $parent;
}
/* Loop children */
$parser->_parseDomNodeList( $element->childNodes, $newElement );
/* Finish */
if ( $okayToParse )
{
/* tags don't parse BBCode inside */
if ( $newElement->tagName == 'pre' and $resumeBBCodeAfterPre )
{
$this->bbcodeParse = TRUE;
}
/* End of an ? */
if ( $okayToParse and $element->tagName === 'abbr' and $element->hasAttribute('title') )
{
$k = array_search( $element->getAttribute('title'), $this->openAbbrTags );
if ( $k !== FALSE )
{
unset( $this->openAbbrTags[ $k ] );
}
}
/* If we did have children, but now we don't (for example, the entire content is a block-level BBCode), drop this element to avoid unintentional whitespace */
if ( $newElement->parentNode and $element->childNodes->length and !$newElement->childNodes->length )
{
$parent->removeChild( $newElement );
}
}
/* Adjust parent for block BBCode */
$this->_adjustParentForBlockBBCodeAtEndOfNode( $parent );
}
/**
* Parse Text
*
* @param \DOMText $textNode The text from the source document to parse
* @param \DOMNode $parent The node from the new document which will be this node's parent - passed by reference and may be modified for siblings
* @return void
*/
public function _parseDomText( \DOMText $textNode, \DOMNode &$parent, \IPS\Text\DOMParser $parser )
{
/* Adjust parent for block BBCode */
$this->_adjustParentForBlockBBCodeAtStartOfNode( $parent );
/* Init */
$text = $textNode->wholeText;
$breakPoints = array();
/* Contains [page] tags? */
if ( mb_strpos( $text, '[page]' ) !== FALSE )
{
$this->containsPageTags = TRUE;
}
/* If we are parsing BBCode, we will look for opening (e.g. "[foo=bar]") and closing (e.g. "[/foo]") tags */
if ( $this->bbcode !== NULL and count( $this->bbcode ) )
{
/* First, if we have any single-tag BBCodes (e.g. "[img=URL]") expressed as normal BBCodes (e.g. "[img]URL[/img]") - fix that */
foreach ( $this->bbcode as $tag => $bbcode )
{
if ( isset( $bbcode['single'] ) and $bbcode['single'] )
{
if ( isset( $bbcode['attributes'] ) and in_array( '{option}', $bbcode['attributes'] ) )
{
$text = preg_replace( '/\[(' . preg_quote( $tag, '/' ) . ')\](.+?)\[\/' . preg_quote( $tag, '/' ) . '\]/i', '[$1=$2]', $text );
}
else
{
$text = preg_replace( '/\[(' . preg_quote( $tag, '/' ) . ')\]\s*\[\/' . preg_quote( $tag, '/' ) . '\]/i', '[$1]', $text );
}
}
}
/* And add our regex to the breakpoints */
$breakPoints[] = '(\[\/?(?:' . implode( '|', array_map( function ( $value ) { return preg_quote( $value, '/' ); }, array_keys( $this->bbcode ) ) ) . ')(?:[=\s].+?)?\])';
}
/* If we have any acronyms, they also need to be breakpoints */
if ( count( $this->caseSensitiveAcronyms ) or count( $this->caseInsensitiveAcronyms ) )
{
$breakPoints[] = '((?=<^|\b)(?:' . implode( '|', array_merge( array_map( function ( $value ) { return preg_quote( $value, '/' ); }, array_keys( $this->caseSensitiveAcronyms ) ), array_map( function ( $value ) { return preg_quote( $value, '/' ); }, array_keys( $this->caseInsensitiveAcronyms ) ) ) ) . ')(?=\b|$))';
}
/* Loop through each section */
if ( count( $breakPoints ) )
{
$sections = array_values( array_filter( preg_split( '/' . implode( '|', $breakPoints ) . '/iu', $text, null, PREG_SPLIT_DELIM_CAPTURE ), function( $val ) { return $val !== ''; } ) );
foreach( $sections as $sectionId => $section )
{
$this->_parseTextSection( $section, $parent, ++$sectionId, count( $sections ) );
}
}
else
{
$this->_parseTextSection( $textNode->wholeText, $parent, 1, 1 );
}
/* Adjust parent for block BBCode */
$this->_adjustParentForBlockBBCodeAtEndOfNode( $parent );
}
/**
* Parse a section of text after it has been split into relevant sections
*
* @param string $section The text from the source document to parse
* @param \DOMNode $parent The node from the new document which will be this node's parent - passed by reference and may be modified for siblings
* @param int $sectionId The position of this section out of all the sections in the node - used to indicate if there's text before/after this section
* @param int $sectionCount The total number of sections in the node - used to indicate if there's text before/after this section
* @return void
*/
protected function _parseTextSection( $section, \DOMNode &$parent, $sectionId, $sectionCount )
{
/* If it's empty, skip it */
if ( $section === '' )
{
return;
}
/* If this restarts parsing, do that */
if ( $section == $this->resumeBBCodeParsingOn )
{
$this->bbcodeParse = TRUE;
$this->resumeBBCodeParsingOn = NULL;
}
/* Start of BBCode tag? */
if (
$this->bbcode !== NULL and $this->bbcodeParse and // BBCode is enabled
preg_match( '/^\[([a-z\*]+?)(?:([=\s])(.+?))?\]$/i', $section, $matches ) and // It looks like a BBCode tag
array_key_exists( mb_strtolower( $matches[1] ), $this->bbcode ) and // The tag is in the list
( !isset( $this->bbcode[ mb_strtolower( $matches[1] ) ]['allowOption'] ) or $this->bbcode[ mb_strtolower( $matches[1] ) ]['allowOption'] === TRUE or !isset( $matches[3] ) or !$matches[3] ) // If options aren't allowed for this tag, there isn't one
)
{
/* What was the option? */
$option = NULL;
if ( isset( $matches[3] ) )
{
$option = $matches[3];
/* If it's [foo="bar"] then we strip the quotes, (if it's [foo bar="baz"] then we don't) */
if ( !preg_match( '/^\s*$/', $matches[2] ) )
{
$option = trim( $option, '"\'' );
}
}
/* Send to _openBBcode */
$this->_openBBCode( mb_strtolower( $matches[1] ), $option, $parent, $sectionId, $sectionCount );
}
/* End of BBCode tag? */
elseif ( $this->bbcodeParse and array_key_exists( mb_strtolower( $section ), $this->closeTagsForOpenBBCode ) )
{
$this->_closeBBCode( mb_substr( mb_strtolower( $section ), 2, -1 ), $parent, $sectionId, $sectionCount );
}
/* Normal text */
else
{
/* HTMLPurifier will strip carrage returns, but if HTML posting is enabled this doesn't happen which
leaves blank spaces - so we need to strip here */
if ( !$this->htmlPurifier )
{
$section = str_replace( "\r", '', $section );
}
/* Profanity */
foreach ( $this->exactProfanity as $bad => $good )
{
$section = preg_replace( '/(^|\b|\s)' . preg_quote( $bad, '/' ) . '(\b|\s|!|\?|\.|,|$)/iu', "\\1" . $good . "\\2", $section );
}
$section = str_ireplace( array_keys( $this->looseProfanity ), array_values( $this->looseProfanity ), $section );
/* Note what $parent is */
$originalParent = $parent;
/* Acronym? */
if ( array_key_exists( $section, $this->caseSensitiveAcronyms ) and !in_array( $this->caseSensitiveAcronyms[ $section ], $this->openAbbrTags ) )
{
$parent = $parent->appendChild( new \DOMElement( 'abbr' ) );
$parent->setAttribute( 'title', $this->caseSensitiveAcronyms[ $section ] );
}
elseif ( array_key_exists( mb_strtolower( $section ), $this->caseInsensitiveAcronyms ) and !in_array( $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ], $this->openAbbrTags ) )
{
$parent = $parent->appendChild( new \DOMElement( 'abbr' ) );
$parent->setAttribute( 'title', $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ] );
}
/* Insert the text */
$this->_insertNodeApplyingInlineBBcode( new \DOMText( $section ), $parent );
/* Restore the parent */
$parent = $originalParent;
}
}
/**
* Open BBCode tag
*
* @param string $tag The tag (e.g. "b")
* @param string|NULL $option If an option was provided (e.g. "[foo=bar]"), it's value
* @param \DOMNode $parent The node from the new document which will be this node's parent - passed by reference and may be modified for siblings
* @param int $sectionId The position of this section out of all the sections in the node - used to indicate if there's text before/after this section
* @param int $sectionCount The total number of sections in the node - used to indicate if there's text before/after this section
* @return void
*/
protected function _openBBCode( $tag, $option, \DOMNode &$parent, $sectionId, $sectionCount )
{
/* Get definiton */
$bbcode = $this->bbcode[ $tag ];
/* Get the document */
$document = $parent->ownerDocument ?: $parent;
/* Create the element */
$bbcodeElement = $document->createElement( $bbcode['tag'] );
/* Add any attributes */
if ( isset( $bbcode['attributes'] ) )
{
foreach ( $bbcode['attributes'] as $k => $v )
{
$bbcodeElement->setAttribute( $k, str_replace( '{option}', ( $option ?: ( isset( $bbcode['defaultOption'] ) ? $bbcode['defaultOption'] : '' ) ), $v ) );
}
}
/* Callback */
if ( isset( $bbcode['callback'] ) )
{
$bbcodeElement = call_user_func( $bbcode['callback'], $bbcodeElement, array( 2 => $option ), $document );
}
/* Stop parsing? ([code] blocks make it so BBCode isn't parsed inside them) */
if ( isset( $bbcode['noParse'] ) and $bbcode['noParse'] )
{
$this->bbcodeParse = FALSE;
$this->resumeBBCodeParsingOn = "[/{$tag}]";
}
/* Parse it */
$this->_parseElement( $bbcodeElement );
/* Single only? */
if ( isset( $bbcode['single'] ) and $bbcode['single'] )
{
$this->_insertNodeApplyingInlineBBcode( $bbcodeElement, $parent );
}
/* Or with content? */
else
{
/* Block level? */
if ( isset( $bbcode['block'] ) and $bbcode['block'] )
{
/* Insert the block level element */
$lastOpennedBlockId = NULL;
if ( !empty( $this->openBlockBBCodeInOrder ) )
{
$openBBCodeBlocks = array_keys( $this->openBlockBBCodeInOrder );
$lastOpennedBlockId = array_pop( $openBBCodeBlocks );
}
if ( $lastOpennedBlockId and list( $id, $tagName ) = explode( '-', $lastOpennedBlockId ) and isset( $this->bbcode[ $tagName ]['noChildren'] ) and $this->bbcode[ $tagName ]['noChildren'] )
{
$parent->appendChild( $bbcodeElement );
}
else
{
$parent->parentNode->appendChild( $bbcodeElement );
}
/* Callback */
$blockElement = $bbcodeElement;
if ( isset( $bbcode['getBlockContentElement'] ) )
{
$blockElement = call_user_func( $bbcode['getBlockContentElement'], $bbcodeElement );
}
/* Create an element of the same type (normally element
*
* @param \DOMElement $element The element
* @return bool
*/
protected function _parseImgElement( \DOMElement $element )
{
/* When editing content in the AdminCP, images and iframes get the src munged. When we save, we need to put that back */
$this->_removeMunge( $element );
/* If it's not allowed, remove the src */
if ( !$this->isAllowedUrl( $element->getAttribute('src') ) )
{
$element->removeAttribute('src');
return;
}
/* Is it an emoticon? */
if ( $emoticon = $this->_getEmoticon( $element->getAttribute('src') ) )
{
if ( $this->_emoticons < 75 )
{
$element->setAttribute( 'src', str_replace( static::$fileObjectClasses['core_Emoticons']->baseUrl(), '{fileStore.core_Emoticons}', $element->getAttribute('src') ) );
$element->setAttribute( 'alt', $emoticon['typed'] );
if( $emoticon['image_2x'] and $emoticon['width'] and $emoticon['height'] )
{
$element->setAttribute( 'srcset', str_replace( static::$fileObjectClasses['core_Emoticons']->baseUrl(), '%7BfileStore.core_Emoticons%7D', $emoticon['image_2x'] ) );
$element->setAttribute( 'width', $emoticon['width'] );
$element->setAttribute( 'height', $emoticon['height'] );
}
$this->_emoticons++;
}
else
{
/* Set an attribute on the element - we'll need to know this later */
$element->setAttribute( 'data-ipsEmoticon-plain', $emoticon['typed'] );
}
}
/* Or an attachment? */
elseif ( $attachment = $this->_getAttachment( $element->getAttribute('src') ) )
{
$element->setAttribute( 'data-fileid', $attachment['attach_id'] );
$element->setAttribute( 'src', str_replace( static::$fileObjectClasses['core_Attachment']->baseUrl(), '{fileStore.core_Attachment}', $element->getAttribute('src') ) );
if ( !$element->getAttribute('alt') )
{
$element->setAttribute( 'alt', $attachment['attach_file'] );
}
$this->_logAttachment( $attachment );
}
/* Or some other media? */
elseif ( $element->getAttribute('data-extension') and $file = $this->_getFile( $element->getAttribute('data-extension'), $element->getAttribute('src') ) )
{
$element->setAttribute( 'src', '{fileStore.' . $file->storageExtension . '}/' . (string) $file );
if ( !$element->getAttribute('alt') )
{
$element->setAttribute( 'alt', $file->originalFilename );
}
}
/* Nope, regular image */
else
{
/* We need an alt (HTMLPurifier handles this normally, but it may not always run) */
if ( !$element->getAttribute('alt') )
{
$element->setAttribute( 'alt', mb_substr( basename( $element->getAttribute('src') ), 0, 40 ) );
}
/* Image proxy */
if ( \IPS\Settings::i()->remote_image_proxy or !\IPS\Settings::i()->allow_remote_images )
{
$imageSrc = $element->getAttribute('src');
try
{
$imageSrc = \IPS\Http\Url::createFromString( $imageSrc );
$useProxy = !$imageSrc->isInternal and !$imageSrc->isLocalhost(); // We don't use the image proxy for internal resources as it may cause protected resources to be exposed because the server thinks it's an internal request
}
catch ( \IPS\Http\Url\Exception $e )
{
$useProxy = TRUE;
}
if ( $useProxy )
{
if ( !\IPS\Settings::i()->allow_remote_images )
{
$element->setAttribute( 'src', '' );
}
else
{
$newUrl = \IPS\Http\Url::createFromString( \IPS\Settings::i()->base_url . "applications/core/interface/imageproxy/imageproxy.php" );
$newUrl = $newUrl->setQueryString( array(
'img' => (string) $imageSrc,
'key' => hash_hmac( "sha256", (string) $imageSrc, \IPS\Settings::i()->site_secret_key )
) );
$element->setAttribute( 'src', (string) $newUrl );
}
}
/* Replace srcset also */
if ( $element->getAttribute('srcset') )
{
$urls = explode( ',', $element->getAttribute('srcset') );
$fixedUrls = array();
foreach( $urls as $url )
{
/* Format is: http://url.com/img.png size */
$data = explode( ' ', trim( $url ) );
if ( count( $data ) <= 2 )
{
$imageSrc = $data[0];
try
{
$imageSrc = \IPS\Http\Url::createFromString( $data[0] );
$useProxy = !$imageSrc->isInternal and !$imageSrc->isLocalhost(); // We don't use the image proxy for internal resources as it may cause protected resources to be exposed because the server thinks it's an internal request
}
catch ( \IPS\Http\Url\Exception $e )
{
$useProxy = TRUE;
}
if ( $useProxy )
{
if ( !\IPS\Settings::i()->allow_remote_images )
{
break;
}
else
{
$newUrl = \IPS\Http\Url::createFromString( \IPS\Settings::i()->base_url . "applications/core/interface/imageproxy/imageproxy.php" );
$newUrl = $newUrl->setQueryString( array(
'img' => str_replace( ',', '%2C', (string) $imageSrc ), /* srcset URLs can have a param like resize=150,150 and the comma breaks the URLs */
'key' => hash_hmac( "sha256", (string) $data[0], \IPS\Settings::i()->site_secret_key )
) );
$fixedUrls[] = (string) $newUrl . ( ! empty( $data[1] ) ? ' ' . $data[1] : '' );
}
}
}
}
if ( count( $fixedUrls ) )
{
$element->setAttribute( 'srcset', implode( ', ', $fixedUrls ) );
}
else
{
$element->setAttribute( 'srcset', '' );
}
}
}
}
}
/**
* Parse
\t" so also strip any whitespace after a break CKEditor doesn't actually have a way to indent individual lines */ if ( ( $previousSibling = $textNode->previousSibling and $previousSibling instanceof \DOMElement and $previousSibling->tagName == 'br' ) or ( $textNode->parentNode instanceof \DOMElement and $textNode->parentNode->tagName == 'p' and $textNode->parentNode->lastChild and $textNode->parentNode->lastChild instanceof \DOMElement and $textNode->parentNode->lastChild->tagName == 'br' ) ) { $text = preg_replace( '/^\s/', '', $text ); } /* Insert */ $parent->appendChild( new \DOMText( $text ) ); } ), 21, -6 ); /* Create a new
with those contents */
$return = $originalNode->ownerDocument->createElement( 'pre' );
$return->appendChild( new \DOMText( html_entity_decode( $contents ) ) ); // We have to decode HTML entities otherwise they'll be double-encoded. Test with "[code]Test[/code]"
$return->setAttribute( 'class', 'ipsCode' );
return $return;
} );
$return['code'] = $code;
$return['codebox'] = $code;
$return['html'] = $code;
$return['php'] = $code;
$return['sql'] = $code;
$return['xml'] = $code;
}
/* Color */
if ( static::canUse( $member, 'TextColor', $area ) )
{
$return['color'] = array( 'tag' => 'span', 'attributes' => array( 'style' => 'color:{option}' ), 'allowOption' => TRUE );
}
/* Font */
if ( static::canUse( $member, 'Font', $area ) )
{
$return['font'] = array( 'tag' => 'span', 'attributes' => array( 'style' => 'font-family:{option}' ), 'allowOption' => TRUE );
}
/* HR */
$return['hr'] = array( 'tag' => 'hr', 'single' => TRUE, 'allowOption' => FALSE );
/* Image */
if ( static::canUse( $member, 'ipsImage', $area ) )
{
$return['img'] = array( 'tag' => 'img', 'attributes' => array( 'src' => '{option}', 'class' => 'ipsImage' ), 'single' => TRUE, 'allowOption' => TRUE );
}
/* Indent */
if ( static::canUse( $member, 'Indent', $area ) )
{
$return['indent'] = array( 'tag' => 'div', 'attributes' => array( 'style' => 'margin-left:{option}px' ), 'block' => FALSE, 'defaultOption' => 25, 'allowOption' => TRUE );
}
/* Italics */
if ( static::canUse( $member, 'Italic', $area ) )
{
$return['i'] = array( 'tag' => 'em', 'allowOption' => FALSE );
}
/* Justify */
if ( static::canUse( $member, 'JustifyLeft', $area ) )
{
$return['left'] = array( 'tag' => 'div', 'attributes' => array( 'style' => 'text-align:left' ), 'block' => TRUE, 'allowOption' => FALSE );
}
if ( static::canUse( $member, 'JustifyCenter', $area ) )
{
$return['center'] = array( 'tag' => 'div', 'attributes' => array( 'style' => 'text-align:center' ), 'block' => TRUE, 'allowOption' => FALSE );
}
if ( static::canUse( $member, 'JustifyRight', $area ) )
{
$return['right'] = array( 'tag' => 'div', 'attributes' => array( 'style' => 'text-align:right' ), 'block' => TRUE, 'allowOption' => FALSE );
}
/* Links */
if ( static::canUse( $member, 'ipsLink', $area ) )
{
/* Email */
$return['email'] = array( 'tag' => 'a', 'attributes' => array( 'href' => 'mailto:{option}' ), 'allowOption' => TRUE );
/* Member */
$return['member'] = array(
'tag' => 'a',
'attributes'=> array( 'contenteditable' => 'false', 'data-ipsHover' => '' ),
'callback' => function( \DOMElement $node, $matches, \DOMDocument $document )
{
try
{
$member = \IPS\Member::load( $matches[2], 'name' );
if ( $member->member_id != 0 )
{
$node->setAttribute( 'href', $member->url() );
$node->setAttribute( 'data-ipsHover-target', $member->url()->setQueryString( 'do', 'hovercard' ) );
$node->setAttribute( 'data-mentionid', $member->member_id );
$node->appendChild( $document->createTextNode( '@' . $member->name ) );
}
}
catch ( \Exception $e ) {}
return $node;
},
'single' => TRUE,
'allowOption' => TRUE
);
/* Links */
$return['url'] = array( 'tag' => 'a', 'attributes' => array( 'href' => '{option}' ), 'allowOption' => TRUE );
}
/* List */
if ( static::canUse( $member, 'BulletedList', $area ) or static::canUse( $member, 'NumberedList', $area ) )
{
$return['list'] = array(
'tag' => 'ul',
'callback' => function( $node, $matches, $document )
{
/* Set the main attributes for our element */ $node->setAttribute( 'class', 'ipsQuote' ); $node->setAttribute( 'data-ipsQuote', '' ); if ( isset( $options['name'] ) and $options['name'] ) { $node->setAttribute( 'data-ipsQuote-username', $options['name'] ); } if ( isset( $options['date'] ) and $options['date'] ) { $node->setAttribute( 'data-ipsQuote-timestamp', strtotime( $options['date'] ) ); } if ( \IPS\Application::appIsEnabled('forums') and isset( $options['post'] ) and $options['post'] ) { try { $post = \IPS\forums\Topic\Post::load( $options['post'] ); $node->setAttribute( 'data-ipsQuote-contentapp', 'forums' ); $node->setAttribute( 'data-ipsQuote-contenttype', 'forums' ); $node->setAttribute( 'data-ipsQuote-contentclass', 'forums_Topic' ); $node->setAttribute( 'data-ipsQuote-contentid', $post->item()->tid ); $node->setAttribute( 'data-ipsQuote-contentcommentid', $post->pid ); } catch ( \OutOfRangeException $e ) {} } /* Create the citation element */ $citation = $document->createElement('div'); $citation->setAttribute( 'class', 'ipsQuote_citation' ); $node->appendChild( $citation ); /* Create the content element */ $contents = $document->createElement('div'); $contents->setAttribute( 'class', 'ipsQuote_contents ipsClearfix' ); $node->appendChild( $contents ); return $node; }, 'getBlockContentElement' => function( \DOMElement $node ) { foreach ( $node->childNodes as $child ) { if ( $child instanceof \DOMElement and mb_strpos( $child->getAttribute('class'), 'ipsQuote_contents' ) !== FALSE ) { return $child; } } return $node; }, 'block' => TRUE, 'allowOption' => TRUE ); } /* Size */ if ( static::canUse( $member, 'FontSize', $area ) ) { $return['size'] = array( 'tag' => 'span', 'callback' => function( $node, $matches ) { switch ( $matches[2] ) { case 1: $node->setAttribute( 'style', 'font-size:8px' ); break; case 2: $node->setAttribute( 'style', 'font-size:10px' ); break; case 3: $node->setAttribute( 'style', 'font-size:12px' ); break; case 4: $node->setAttribute( 'style', 'font-size:14px' ); break; case 5: $node->setAttribute( 'style', 'font-size:18px' ); break; case 6: $node->setAttribute( 'style', 'font-size:24px' ); break; case 7: $node->setAttribute( 'style', 'font-size:36px' ); break; case 8: $node->setAttribute( 'style', 'font-size:48px' ); break; } return $node; }, 'allowOption' => TRUE ); } /* Spoiler */ if ( static::canUse( $member, 'ipsSpoiler', $area ) ) { $return['spoiler'] = array( 'tag' => 'div', 'callback' => function( \DOMElement $node, $matches, \DOMDocument $document ) { /* Set the main attributes for ourelement */ $node->setAttribute( 'class', 'ipsSpoiler' ); $node->setAttribute( 'data-ipsSpoiler', '' ); /* Create the citation element */ $header = $document->createElement('div'); $header->setAttribute( 'class', 'ipsSpoiler_header' ); $node->appendChild( $header ); $headerSpan = $document->createElement('span'); $header->appendChild( $headerSpan ); /* Create the content element */ $contents = $document->createElement('div'); $contents->setAttribute( 'class', 'ipsSpoiler_contents' ); $node->appendChild( $contents ); return $node; }, 'getBlockContentElement' => function( \DOMElement $node ) { foreach ( $node->childNodes as $child ) { if ( $child instanceof \DOMElement and $child->getAttribute('class') == 'ipsSpoiler_contents' ) { return $child; } } return $node; }, 'block' => TRUE, 'allowOption' => FALSE ); } /* Strike */ if ( static::canUse( $member, 'Strike', $area ) ) { $return['s'] = array( 'tag' => 'span', 'attributes' => array( 'style' => 'text-decoration:line-through' ), 'allowOption' => FALSE ); $return['strike'] = array( 'tag' => 'span', 'attributes' => array( 'style' => 'text-decoration:line-through' ), 'allowOption' => FALSE ); } /* Subscript */ if ( static::canUse( $member, 'Subscript', $area ) ) { $return['sub'] = array( 'tag' => 'sub', 'allowOption' => FALSE ); } /* Superscript */ if ( static::canUse( $member, 'Superscript', $area ) ) { $return['sup'] = array( 'tag' => 'sup', 'allowOption' => FALSE ); } /* Underline */ if ( static::canUse( $member, 'Underline', $area ) ) { $return['u'] = array( 'tag' => 'span', 'attributes' => array( 'style' => 'text-decoration:underline' ), 'allowOption' => FALSE ); } /* App-Specific */ foreach ( \IPS\Application::allExtensions( 'core', 'BBCode', $member ) as $key => $bbcode ) { if ( $bbcode->permissionCheck( $member, $area ) ) { list( $app, $tag ) = explode( '_', $key ); $return[ $tag ] = $bbcode->getConfiguration(); } } return $return; } /** * @brief Cached permissions */ protected static $permissions = array(); /** * Can use plugin? * * @param \IPS\Member $member The member * @param string $key Plugin key * @param string $area The Editor area * @return bool */ public static function canUse( \IPS\Member $member, $key, $area ) { $permissionSettings = json_decode( \IPS\Settings::i()->ckeditor_permissions, TRUE ); if ( !isset( static::$permissions[ $member->member_id ][ $key ] ) ) { if ( !isset( $permissionSettings[ $key ] ) ) { static::$permissions[ $member->member_id ][ $key ] = TRUE; } else { $val = TRUE; if ( $permissionSettings[ $key ]['groups'] !== '*' ) { if ( !$member->inGroup( $permissionSettings[ $key ]['groups'] ) ) { $val = FALSE; } } if ( $permissionSettings[ $key ]['areas'] !== '*' ) { if ( !in_array( $area, $permissionSettings[ $key ]['areas'] ) ) { $val = FALSE; } } static::$permissions[ $member->member_id ][ $key ] = $val; } } return static::$permissions[ $member->member_id ][ $key ]; } /** * Parse content looking for a separation tag * Used for lists where [*] breaks and whole content where [page] breaks * * @param \DOMNode $originalNode The node containing the content we want to examine * @param callback $mainElementCreator A callback which returns a \DOMElement to be the main element. Is passed \DOMDocument as a parameter * @param callback $subElementCreator A callback which returns a \DOMElement to be a new sub element. Is passed \DOMDocument as a parameter * @return \DOMElement */ protected function _parseContentWithSeparationTag( \DOMNode $originalNode, $mainElementCreator, $subElementCreator, $separator ) { /* Create a copy of the node */ $workingDocument = new \DOMDocument; $workingNode = $workingDocument->importNode( $originalNode, TRUE ); /* Create a freshwith a single
- inside */ $mainElement = call_user_func( $mainElementCreator, $originalNode->ownerDocument ); $currentSubElement = $mainElement->appendChild( call_user_func( $subElementCreator, $originalNode->ownerDocument ) ); /* Parse */ $ignoreNextSeparator = TRUE; foreach ( $workingNode->childNodes as $node ) { $this->_parseContentWithSeparationTagLoop( $mainElement, $currentSubElement, $separator, $subElementCreator, $node, $mainElement, $ignoreNextSeparator ); } /* Return */ return $mainElement; } /** * Loop for _parseContentWithSeparationTag * * @param \DOMElement $mainElement The main element all of our content is going into * @param \DOMElement $currentSubElement The current sub element that nodes go into. When a $separator is detected, a new one is created * @param string $separator The separator, e.g. "[*]" or "[page]" * @param callback $subElementCreator A callback which returns a \DOMElement to be a new sub element * @param \DOMNode $node The current node we're examining * @param \DOMNode $parent The parent of the current node we're examining * @param bool $ignoreNextSeparator If we have [list][*]Foo[/list] We want to ignore the first [*] so we don't end up with
- this keeps track of that * @return void */ protected function _parseContentWithSeparationTagLoop( \DOMElement $mainElement, \DOMElement &$currentSubElement, $separator, $subElementCreator, \DOMNode $node, \DOMNode &$parent, &$ignoreNextSeparator ) { /* If the node is an element... */ if ( $node->nodeType === XML_ELEMENT_NODE ) { /* Ignore any preceeding
- Foo
s */ if ( $ignoreNextSeparator and $node->tagName == 'br' ) { return; } /* Import and insert */ $newElement = $mainElement->ownerDocument->importNode( $node ); if ( $parent->isSameNode( $mainElement ) ) { $currentSubElement->appendChild( $newElement ); } else { $parent->appendChild( $newElement ); } /* Loop children */ foreach ( $node->childNodes as $child ) { $this->_parseContentWithSeparationTagLoop( $mainElement, $currentSubElement, $separator, $subElementCreator, $child, $newElement, $ignoreNextSeparator ); } } /* Or if it's text... */ elseif ( $node->nodeType === XML_TEXT_NODE ) { /* Ignore any closing tag */ $text = $node->wholeText; $text = str_replace( preg_replace( '/\[(.+?)\]/', '[/$1]', $separator ), '', $text ); /* Break it up where we find the separator... */ foreach ( array_filter( preg_split( '/(' . preg_quote( $separator, '/' ) . ')/', $text, null, PREG_SPLIT_DELIM_CAPTURE ), 'trim' ) as $textSection ) { /* If this section the separator... */ if ( $textSection === $separator ) { /* Unless we're ignoring it, create a new element... */ if ( !$ignoreNextSeparator ) { /* Strip any extrenous
from the last one */ if ( $currentSubElement->lastChild and $currentSubElement->lastChild->nodeType === XML_ELEMENT_NODE and $currentSubElement->lastChild->tagName == 'br' ) { $currentSubElement->removeChild( $currentSubElement->lastChild ); } /* Create a new one */ $currentSubElement = $mainElement->appendChild( call_user_func( $subElementCreator, $mainElement->ownerDocument ) ); if ( !$parent->isSameNode( $mainElement ) ) { $parent = $parent->cloneNode( FALSE ); $currentSubElement->appendChild( $parent ); } } /* Or if we are, then don't ignore the next one */ else { $ignoreNextSeparator = FALSE; } } else { /* Insert */ if ( $parent->isSameNode( $mainElement ) ) { $currentSubElement->appendChild( new \DOMText( $textSection ) ); } else { $parent->appendChild( new \DOMText( $textSection ) ); } /* If we're meant to be ignoring the next separator, but we have found content before it, remove that flag */ if ( $ignoreNextSeparator ) { $ignoreNextSeparator = FALSE; } } } } } /* !Embeddable Media */ /** * Get OEmbed Services * Implemented in this way so it's easy for hook authors to override if they wanted to * * @see oEmbed * @return array */ protected static function oembedServices() { return 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 ), 'flickr.com' => array( 'https://www.flickr.com/services/oembed/', static::EMBED_IMAGE ), 'flic.kr' => array( 'https://www.flickr.com/services/oembed/', static::EMBED_IMAGE ), 'hulu.com' => array( 'http://www.hulu.com/api/oembed.json', static::EMBED_VIDEO ), 'vimeo.com' => array( 'https://vimeo.com/api/oembed.json', static::EMBED_VIDEO ), 'collegehumor.com' => array( 'http://www.collegehumor.com/oembed.json', static::EMBED_VIDEO ), 'twitter.com' => array( 'https://api.twitter.com/1/statuses/oembed.json', 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 ), ), 'gfycat.com' => array( 'https://api.gfycat.com/v1/oembed', static::EMBED_IMAGE ), 'dailymotion.com' => array( 'https://www.dailymotion.com/services/oembed', static::EMBED_VIDEO ), 'codepen.io' => array( 'http://codepen.io/api/oembed', static::EMBED_LINK ), 'coub.com' => array( 'http://coub.com/api/oembed.json', static::EMBED_VIDEO ), '*.deviantart.com' => array( 'https://backend.deviantart.com/oembed', static::EMBED_IMAGE ), 'docs.com' => array( 'http://docs.com/api/oembed', static::EMBED_LINK ), 'funnyordie.com' => array( 'http://www.funnyordie.com/oembed.json', static::EMBED_VIDEO ), 'gettyimages.com' => array( 'http://embed.gettyimages.com/oembed', static::EMBED_IMAGE ), 'ifixit.com' => array( 'http://www.ifixit.com/Embed', static::EMBED_LINK ), 'imgur.com' => array( 'http://api.imgur.com/oembed', static::EMBED_IMAGE ), 'kickstarter.com' => array( 'http://www.kickstarter.com/services/oembed', static::EMBED_LINK ), 'meetup.com' => array( 'https://api.meetup.com/oembed', static::EMBED_LINK ), 'mixcloud.com' => array( 'http://www.mixcloud.com/oembed', static::EMBED_VIDEO ), 'mix.office.com' => array( 'https://mix.office.com/oembed', static::EMBED_VIDEO ), 'reddit.com' => array( 'http://www.reddit.com/oembed', static::EMBED_LINK ), 'reverbnation.com' => array( 'https://www.reverbnation.com/oembed', static::EMBED_VIDEO ), 'screencast.com' => array( 'https://api.screencast.com/external/oembed', static::EMBED_IMAGE ), 'slideshare.net' => array( 'http://www.slideshare.net/api/oembed/2', static::EMBED_VIDEO ), '*.smugmug.com' => array( 'http://api.smugmug.com/services/oembed', static::EMBED_IMAGE ), 'ustream.tv' => array( 'https://www.ustream.tv/oembed', static::EMBED_VIDEO ), ); } /** * @brief External link request timeout */ public static $requestTimeout = \IPS\DEFAULT_REQUEST_TIMEOUT; /** * Convert URL to embed HTML * * @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. * @return string|null HTML embded code, or NULL if URL is not embeddable */ public static function embeddableMedia( $url, $iframe=FALSE ) { /* Internal */ if ( $url->isInternal ) { /* Internal Embed */ if ( $embedCode = static::_internalEmbed( $url, $iframe ) ) { return $embedCode; } /* Image? */ if ( $embedCode = static::_imageEmbed( $url, $iframe ) ) { return $embedCode; } } /* External */ else { /* oEmbed? */ if ( $embedCode = static::_oembedEmbed( $url, $iframe ) ) { return $embedCode; } /* Other services */ if ( $embedCode = static::_customEmbed( $url, $iframe ) ) { return $embedCode; } /* Image? */ if ( \IPS\Settings::i()->allow_remote_images and $embedCode = static::_imageEmbed( $url, $iframe ) ) { return $embedCode; } } /* Still here? It's not embeddable */ return NULL; } /** * @brief Embed type constants */ const EMBED_IMAGE = 1; const EMBED_VIDEO = 2; const EMBED_STATUS = 3; const EMBED_TWEET = 4; const EMBED_LINK = 5; /** * 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. * @return string|null */ protected static function _oembedEmbed( \IPS\Http\Url $url, $iframe=FALSE ) { /* Strip the "www." from the domain */ $domain = $url->data['host']; if ( mb_substr( $domain, 0, 4 ) === 'www.' ) { $domain = mb_substr( $domain, 4 ); } if( !$domain ) { return null; } /* Get oEmbed Services */ $oembedServices = static::oembedServices(); /* If the URL's domain is in the list... */ $entry = NULL; if ( array_key_exists( $domain, $oembedServices ) ) { $entry = $oembedServices[ $domain ]; } else { foreach( $oembedServices as $k => $v ) { if( mb_strpos( $k, '*' ) !== FALSE ) { if( preg_match( "/^" . str_replace( array( '.', '*' ), array( '\\.', '.*?' ), $k ) . "$/i", $domain ) ) { $entry = $v; } } } } if( $entry ) { $endtype = static::EMBED_LINK; $endpoint = NULL; /* If we have multiple possible ones, find the best */ if ( is_array( $entry[0] ) ) { foreach ( $entry as $regex => $endpoints ) { if ( is_string( $regex ) and preg_match( $regex, $url ) ) { $endpoint = $endpoints[0]; $endtype = $endpoints[1]; break; } } if( $endpoint === NULL ) { $endpoint = $entry[0][0]; $endtype = $entry[0][1]; } } else { $endpoint = $entry[0]; $endtype = $entry[1]; } /* Call oEmbed Service */ try { $response = \IPS\Http\Url::external( $endpoint ) ->setQueryString( array( 'format' => 'json', 'url' => (string) $url->stripQueryString( 'autoplay' ), 'scheme' => ( $url->data['scheme'] === 'https' or \IPS\Request::i()->isSecure() ) ? 'https' : null ) ) ->request( static::$requestTimeout ) ->setHeaders( array( 'Accept-Language' => \IPS\Member::loggedIn()->language()->short ) ) ->get(); if( $response->httpResponseCode != '200' ) { switch( $response->httpResponseCode ) { case '404': throw new \UnexpectedValueException( 'embed__fail_404', $endtype ); break; case '401': case '403': throw new \UnexpectedValueException( 'embed__fail_403', $endtype ); break; default: throw new \UnexpectedValueException( 'embed__fail_500', $endtype ); break; } } $response = $response->decodeJson(); } /* If it error'd (connection error or unexpected response), we'll not embed this */ catch ( \IPS\Http\Request\Exception $e ) { throw new \UnexpectedValueException( 'embed__fail_500', $endtype ); } /* Gfycat and Facebook videos report as video but they're not HTML5 videos with the aspect ratio we need, so treat it as rich */ if ( $domain == 'gfycat.com' or ( $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'] ); } /* We need a type otherwise we can't embed */ if( !isset( $response['type'] ) ) { throw new \UnexpectedValueException( 'embed_no_oembed_type' ); } /* The "type" parameter is a way for services to indicate the type of content they are retruning. It is not strict, but we use it to identify the best styles to apply. */ switch ( $response['type'] ) { /* Static photo - show antag, linked if necessary, using .ipsImage to be responsive. Similar outcome to if a user had used the "insert image from URL" button */ case 'photo': return \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->photo( $response['url'], $url, $response['title'] ); /* Video - insert the provided HTML directly (it will be a video so there's nothing we need to prevent from being edited), using .ipsEmbeddedVideo to make it responsive */ case 'video': $response['html'] = str_replace( 'allowfullscreen', 'allowfullscreen="true"', $response['html'] ); return \IPS\Theme::i()->getTemplate( 'embed', 'core', 'global' )->video( $response['html'] ); /* Other - show an