mediawiki-extensions-Parser.../includes/ParserFunctions.php

855 lines
23 KiB
PHP
Raw Normal View History

<?php
namespace MediaWiki\Extensions\ParserFunctions;
use DateTime;
use DateTimeZone;
use Exception;
use Language;
use LinkCache;
use MediaWiki\MediaWikiServices;
use MWTimestamp;
use Parser;
use PPFrame;
use PPNode;
use Sanitizer;
use StringUtils;
use StubObject;
use Title;
class ParserFunctions {
private static $mExprParser;
private static $mTimeCache = [];
private static $mTimeChars = 0;
/** ~10 seconds */
const MAX_TIME_CHARS = 6000;
/**
* Register ParserClearState hook.
* We defer this until needed to avoid the loading of the code of this file
* when no parser function is actually called.
*/
private static function registerClearHook() {
static $done = false;
if ( !$done ) {
global $wgHooks;
$wgHooks['ParserClearState'][] = function () {
self::$mTimeChars = 0;
};
$done = true;
}
}
2011-01-14 00:40:30 +00:00
/**
* @return ExprParser
*/
private static function &getExprParser() {
if ( !isset( self::$mExprParser ) ) {
self::$mExprParser = new ExprParser;
}
return self::$mExprParser;
}
/**
* @param Parser $parser
* @param string $expr
* @return string
*/
public static function expr( $parser, $expr = '' ) {
try {
return self::getExprParser()->doExpression( $expr );
} catch ( ExprError $e ) {
return '<strong class="error">' . htmlspecialchars( $e->getMessage() ) . '</strong>';
}
}
/**
* @param Parser $parser
* @param PPFrame $frame
* @param array $args
* @return string
*/
public static function ifexpr( $parser, $frame, $args ) {
$expr = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
$then = $args[1] ?? '';
$else = $args[2] ?? '';
try {
$result = self::getExprParser()->doExpression( $expr );
if ( is_numeric( $result ) ) {
$result = (float)$result;
}
$result = $result ? $then : $else;
} catch ( ExprError $e ) {
return '<strong class="error">' . htmlspecialchars( $e->getMessage() ) . '</strong>';
}
if ( is_object( $result ) ) {
$result = trim( $frame->expand( $result ) );
}
return $result;
}
/**
* @param Parser $parser
* @param PPFrame $frame
* @param array $args
* @return string
*/
public static function if( $parser, $frame, $args ) {
$test = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
if ( $test !== '' ) {
return isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : '';
} else {
return isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : '';
}
}
/**
* @param Parser $parser
* @param PPFrame $frame
* @param array $args
* @return string
*/
public static function ifeq( $parser, $frame, $args ) {
$left = isset( $args[0] ) ? self::decodeTrimExpand( $args[0], $frame ) : '';
$right = isset( $args[1] ) ? self::decodeTrimExpand( $args[1], $frame ) : '';
// Strict compare is not possible here. 01 should equal 1 for example.
/** @noinspection TypeUnsafeComparisonInspection */
if ( $left == $right ) {
return isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : '';
} else {
return isset( $args[3] ) ? trim( $frame->expand( $args[3] ) ) : '';
}
}
/**
* @param Parser $parser
* @param PPFrame $frame
* @param array $args
* @return string
*/
public static function iferror( $parser, $frame, $args ) {
$test = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
$then = $args[1] ?? false;
$else = $args[2] ?? false;
if ( preg_match(
'/<(?:strong|span|p|div)\s(?:[^\s>]*\s+)*?class="(?:[^"\s>]*\s+)*?error(?:\s[^">]*)?"/',
$test )
) {
$result = $then;
} elseif ( $else === false ) {
$result = $test;
} else {
$result = $else;
}
if ( $result === false ) {
return '';
}
return trim( $frame->expand( $result ) );
}
2011-04-15 17:44:19 +00:00
/**
* @param Parser $parser
* @param PPFrame $frame
* @param array $args
2011-04-15 17:44:19 +00:00
* @return string
*/
public static function switch( $parser, $frame, $args ) {
if ( count( $args ) === 0 ) {
return '';
}
$primary = self::decodeTrimExpand( array_shift( $args ), $frame );
$found = $defaultFound = false;
$default = null;
$lastItemHadNoEquals = false;
$lastItem = '';
$mwDefault = $parser->getMagicWordFactory()->get( 'default' );
foreach ( $args as $arg ) {
$bits = $arg->splitArg();
$nameNode = $bits['name'];
$index = $bits['index'];
$valueNode = $bits['value'];
if ( $index === '' ) {
# Found "="
$lastItemHadNoEquals = false;
if ( $found ) {
# Multiple input match
return trim( $frame->expand( $valueNode ) );
} else {
$test = self::decodeTrimExpand( $nameNode, $frame );
/** @noinspection TypeUnsafeComparisonInspection */
if ( $test == $primary ) {
# Found a match, return now
return trim( $frame->expand( $valueNode ) );
} elseif ( $defaultFound || $mwDefault->matchStartToEnd( $test ) ) {
2009-06-05 16:31:43 +00:00
$default = $valueNode;
$defaultFound = false;
2009-06-05 16:31:43 +00:00
} # else wrong case, continue
}
} else {
# Multiple input, single output
# If the value matches, set a flag and continue
$lastItemHadNoEquals = true;
// $lastItem is an "out" variable
$decodedTest = self::decodeTrimExpand( $valueNode, $frame, $lastItem );
/** @noinspection TypeUnsafeComparisonInspection */
if ( $decodedTest == $primary ) {
$found = true;
} elseif ( $mwDefault->matchStartToEnd( $decodedTest ) ) {
$defaultFound = true;
}
}
}
# Default case
# Check if the last item had no = sign, thus specifying the default case
if ( $lastItemHadNoEquals ) {
return $lastItem;
} elseif ( !is_null( $default ) ) {
return trim( $frame->expand( $default ) );
} else {
return '';
}
}
/**
* Returns the absolute path to a subpage, relative to the current article
* title. Treats titles as slash-separated paths.
*
* Following subpage link syntax instead of standard path syntax, an
* initial slash is treated as a relative path, and vice versa.
2011-04-15 17:44:19 +00:00
*
* @param Parser $parser
* @param string $to
* @param string $from
*
* @return string
*/
public static function rel2abs( $parser, $to = '', $from = '' ) {
$from = trim( $from );
if ( $from === '' ) {
$from = $parser->getTitle()->getPrefixedText();
}
$to = rtrim( $to, ' /' );
// if we have an empty path, or just one containing a dot
if ( $to === '' || $to === '.' ) {
return $from;
}
// if the path isn't relative
if ( substr( $to, 0, 1 ) !== '/' &&
substr( $to, 0, 2 ) !== './' &&
substr( $to, 0, 3 ) !== '../' &&
$to !== '..'
) {
$from = '';
}
// Make a long path, containing both, enclose it in /.../
$fullPath = '/' . $from . '/' . $to . '/';
// remove redundant current path dots
$fullPath = preg_replace( '!/(\./)+!', '/', $fullPath );
// remove double slashes
$fullPath = preg_replace( '!/{2,}!', '/', $fullPath );
// remove the enclosing slashes now
$fullPath = trim( $fullPath, '/' );
$exploded = explode( '/', $fullPath );
$newExploded = [];
foreach ( $exploded as $current ) {
if ( $current === '..' ) { // removing one level
if ( !count( $newExploded ) ) {
// attempted to access a node above root node
$msg = wfMessage( 'pfunc_rel2abs_invalid_depth', $fullPath )
->inContentLanguage()->escaped();
return '<strong class="error">' . $msg . '</strong>';
}
// remove last level from the stack
array_pop( $newExploded );
} else {
// add the current level to the stack
$newExploded[] = $current;
}
}
// we can now join it again
return implode( '/', $newExploded );
}
/**
* @param Parser $parser
* @param PPFrame $frame
* @param string $titletext
* @param string $then
* @param string $else
*
* @return string
*/
private static function ifexistInternal(
$parser, $frame, $titletext = '', $then = '', $else = ''
) {
$title = Title::newFromText( $titletext );
$parser->getContentLanguage()->findVariantLink( $titletext, $title, true );
if ( $title ) {
if ( $title->getNamespace() === NS_MEDIA ) {
/* If namespace is specified as NS_MEDIA, then we want to
* check the physical file, not the "description" page.
*/
if ( !$parser->incrementExpensiveFunctionCount() ) {
return $else;
}
$file = wfFindFile( $title );
if ( !$file ) {
return $else;
}
$parser->mOutput->addImage(
$file->getName(), $file->getTimestamp(), $file->getSha1() );
return $file->exists() ? $then : $else;
} elseif ( $title->isSpecialPage() ) {
/* Don't bother with the count for special pages,
* since their existence can be checked without
* accessing the database.
*/
return MediaWikiServices::getInstance()->getSpecialPageFactory()
->exists( $title->getDBkey() ) ? $then : $else;
} elseif ( $title->isExternal() ) {
/* Can't check the existence of pages on other sites,
* so just return $else. Makes a sort of sense, since
* they don't exist _locally_.
*/
return $else;
} else {
$pdbk = $title->getPrefixedDBkey();
$lc = LinkCache::singleton();
$id = $lc->getGoodLinkID( $pdbk );
if ( $id !== 0 ) {
$parser->mOutput->addLink( $title, $id );
return $then;
} elseif ( $lc->isBadLink( $pdbk ) ) {
$parser->mOutput->addLink( $title, 0 );
return $else;
}
if ( !$parser->incrementExpensiveFunctionCount() ) {
return $else;
}
$id = $title->getArticleID();
$parser->mOutput->addLink( $title, $id );
// bug 70495: don't just check whether the ID != 0
if ( $title->exists() ) {
return $then;
}
}
}
return $else;
}
/**
* @param Parser $parser
* @param PPFrame $frame
* @param array $args
* @return string
*/
public static function ifexist( $parser, $frame, $args ) {
$title = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
$then = $args[1] ?? null;
$else = $args[2] ?? null;
$result = self::ifexistInternal( $parser, $frame, $title, $then, $else );
if ( $result === null ) {
return '';
} else {
return trim( $frame->expand( $result ) );
}
}
/**
* @param Parser $parser
* @param PPFrame|null $frame
* @param string $format
* @param string $date
* @param string $language
* @param string|bool $local
* @return string
*/
private static function timeCommon(
$parser, $frame = null, $format = '', $date = '', $language = '', $local = false
) {
global $wgLocaltimezone;
self::registerClearHook();
if ( $date === '' ) {
$cacheKey = $parser->getOptions()->getTimestamp();
$timestamp = new MWTimestamp( $cacheKey );
$date = $timestamp->getTimestamp( TS_ISO_8601 );
$useTTL = true;
} else {
$cacheKey = $date;
$useTTL = false;
}
if ( isset( self::$mTimeCache[$format][$cacheKey][$language][$local] ) ) {
$cachedVal = self::$mTimeCache[$format][$cacheKey][$language][$local];
if ( $useTTL && $cachedVal[1] !== null && $frame ) {
$frame->setTTL( $cachedVal[1] );
}
return $cachedVal[0];
}
# compute the timestamp string $ts
# PHP >= 5.2 can handle dates before 1970 or after 2038 using the DateTime object
$invalidTime = false;
# the DateTime constructor must be used because it throws exceptions
# when errors occur, whereas date_create appears to just output a warning
# that can't really be detected from within the code
try {
# Default input timezone is UTC.
$utc = new DateTimeZone( 'UTC' );
# Correct for DateTime interpreting 'XXXX' as XX:XX o'clock
if ( preg_match( '/^[0-9]{4}$/', $date ) ) {
$date = '00:00 ' . $date;
}
# Parse date
# UTC is a default input timezone.
$dateObject = new DateTime( $date, $utc );
# Set output timezone.
if ( $local ) {
if ( isset( $wgLocaltimezone ) ) {
$tz = new DateTimeZone( $wgLocaltimezone );
} else {
$tz = new DateTimeZone( date_default_timezone_get() );
}
} else {
$tz = $utc;
}
$dateObject->setTimezone( $tz );
# Generate timestamp
$ts = $dateObject->format( 'YmdHis' );
} catch ( Exception $ex ) {
$invalidTime = true;
}
$ttl = null;
# format the timestamp and return the result
if ( $invalidTime ) {
$result = '<strong class="error">' .
wfMessage( 'pfunc_time_error' )->inContentLanguage()->escaped() .
'</strong>';
} else {
self::$mTimeChars += strlen( $format );
if ( self::$mTimeChars > self::MAX_TIME_CHARS ) {
return '<strong class="error">' .
wfMessage( 'pfunc_time_too_long' )->inContentLanguage()->escaped() .
'</strong>';
} else {
if ( $ts < 0 ) { // Language can't deal with BC years
return '<strong class="error">' .
wfMessage( 'pfunc_time_too_small' )->inContentLanguage()->escaped() .
'</strong>';
} elseif ( $ts < 100000000000000 ) { // Language can't deal with years after 9999
if ( $language !== '' && Language::isValidBuiltInCode( $language ) ) {
// use whatever language is passed as a parameter
$langObject = Language::factory( $language );
} else {
// use wiki's content language
$langObject = $parser->getFunctionLang();
// $ttl is passed by reference, which doesn't work right on stub objects
StubObject::unstub( $langObject );
}
$result = $langObject->sprintfDate( $format, $ts, $tz, $ttl );
} else {
return '<strong class="error">' .
wfMessage( 'pfunc_time_too_big' )->inContentLanguage()->escaped() .
'</strong>';
}
}
}
self::$mTimeCache[$format][$cacheKey][$language][$local] = [ $result, $ttl ];
if ( $useTTL && $ttl !== null && $frame ) {
$frame->setTTL( $ttl );
}
return $result;
}
/**
* @param Parser $parser
* @param PPFrame $frame
* @param array $args
* @return string
*/
public static function time( $parser, $frame, $args ) {
$format = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
$date = isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : '';
$language = isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : '';
$local = isset( $args[3] ) && trim( $frame->expand( $args[3] ) );
return self::timeCommon( $parser, $frame, $format, $date, $language, $local );
}
/**
* @param Parser $parser
* @param PPFrame $frame
* @param array $args
* @return string
*/
public static function localTime( $parser, $frame, $args ) {
$format = isset( $args[0] ) ? trim( $frame->expand( $args[0] ) ) : '';
$date = isset( $args[1] ) ? trim( $frame->expand( $args[1] ) ) : '';
$language = isset( $args[2] ) ? trim( $frame->expand( $args[2] ) ) : '';
return self::timeCommon( $parser, $frame, $format, $date, $language, true );
}
/**
* Obtain a specified number of slash-separated parts of a title,
* e.g. {{#titleparts:Hello/World|1}} => "Hello"
*
* @param Parser $parser Parent parser
* @param string $title Title to split
* @param int $parts Number of parts to keep
* @param int $offset Offset starting at 1
* @return string
*/
public static function titleparts( $parser, $title = '', $parts = 0, $offset = 0 ) {
$parts = (int)$parts;
$offset = (int)$offset;
$ntitle = Title::newFromText( $title );
if ( $ntitle instanceof Title ) {
$bits = explode( '/', $ntitle->getPrefixedText(), 25 );
if ( count( $bits ) <= 0 ) {
return $ntitle->getPrefixedText();
} else {
if ( $offset > 0 ) {
--$offset;
}
if ( $parts === 0 ) {
return implode( '/', array_slice( $bits, $offset ) );
} else {
return implode( '/', array_slice( $bits, $offset, $parts ) );
}
}
} else {
return $title;
}
}
/**
* Verifies parameter is less than max string length.
* @param string $text
* @return bool
*/
private static function checkLength( $text ) {
global $wgPFStringLengthLimit;
return ( mb_strlen( $text ) < $wgPFStringLengthLimit );
}
/**
* Generates error message. Called when string is too long.
* @return string
*/
private static function tooLongError() {
global $wgPFStringLengthLimit;
$msg = wfMessage( 'pfunc_string_too_long' )->numParams( $wgPFStringLengthLimit );
return '<strong class="error">' . $msg->inContentLanguage()->escaped() . '</strong>';
}
/**
* {{#len:string}}
*
* Reports number of characters in string.
* @param Parser $parser
* @param string $inStr
* @return int
*/
public static function runLen( $parser, $inStr = '' ) {
$inStr = $parser->killMarkers( (string)$inStr );
return mb_strlen( $inStr );
}
/**
* {{#pos: string | needle | offset}}
*
* Finds first occurrence of "needle" in "string" starting at "offset".
*
* Note: If the needle is an empty string, single space is used instead.
* Note: If the needle is not found, empty string is returned.
* @param Parser $parser
* @param string $inStr
* @param int|string $inNeedle
* @param int $inOffset
* @return int|string
*/
public static function runPos( $parser, $inStr = '', $inNeedle = '', $inOffset = 0 ) {
$inStr = $parser->killMarkers( (string)$inStr );
$inNeedle = $parser->killMarkers( (string)$inNeedle );
if ( !self::checkLength( $inStr ) ||
2012-02-06 04:37:05 +00:00
!self::checkLength( $inNeedle ) ) {
return self::tooLongError();
}
if ( $inNeedle === '' ) {
$inNeedle = ' ';
}
$pos = mb_strpos( $inStr, $inNeedle, min( (int)$inOffset, mb_strlen( $inStr ) ) );
if ( $pos === false ) {
$pos = '';
}
return $pos;
}
/**
* {{#rpos: string | needle}}
*
* Finds last occurrence of "needle" in "string".
*
* Note: If the needle is an empty string, single space is used instead.
* Note: If the needle is not found, -1 is returned.
* @param Parser $parser
* @param string $inStr
* @param int|string $inNeedle
* @return int|string
*/
public static function runRPos( $parser, $inStr = '', $inNeedle = '' ) {
$inStr = $parser->killMarkers( (string)$inStr );
$inNeedle = $parser->killMarkers( (string)$inNeedle );
if ( !self::checkLength( $inStr ) ||
2012-02-06 04:37:05 +00:00
!self::checkLength( $inNeedle ) ) {
return self::tooLongError();
}
if ( $inNeedle === '' ) {
$inNeedle = ' ';
}
$pos = mb_strrpos( $inStr, $inNeedle );
if ( $pos === false ) {
$pos = -1;
}
return $pos;
}
/**
* {{#sub: string | start | length }}
*
* Returns substring of "string" starting at "start" and having
* "length" characters.
*
* Note: If length is zero, the rest of the input is returned.
* Note: A negative value for "start" operates from the end of the
* "string".
* Note: A negative value for "length" returns a string reduced in
* length by that amount.
*
* @param Parser $parser
* @param string $inStr
* @param int $inStart
* @param int $inLength
* @return string
*/
public static function runSub( $parser, $inStr = '', $inStart = 0, $inLength = 0 ) {
$inStr = $parser->killMarkers( (string)$inStr );
if ( !self::checkLength( $inStr ) ) {
return self::tooLongError();
}
if ( (int)$inLength === 0 ) {
$result = mb_substr( $inStr, (int)$inStart );
} else {
$result = mb_substr( $inStr, (int)$inStart, (int)$inLength );
}
return $result;
}
/**
* {{#count: string | substr }}
*
* Returns number of occurrences of "substr" in "string".
*
* Note: If "substr" is empty, a single space is used.
* @param Parser $parser
* @param string $inStr
* @param string $inSubStr
* @return int|string
*/
public static function runCount( $parser, $inStr = '', $inSubStr = '' ) {
$inStr = $parser->killMarkers( (string)$inStr );
$inSubStr = $parser->killMarkers( (string)$inSubStr );
if ( !self::checkLength( $inStr ) ||
!self::checkLength( $inSubStr ) ) {
return self::tooLongError();
}
if ( $inSubStr === '' ) {
$inSubStr = ' ';
}
$result = mb_substr_count( $inStr, $inSubStr );
return $result;
}
/**
* {{#replace:string | from | to | limit }}
*
* Replaces each occurrence of "from" in "string" with "to".
* At most "limit" replacements are performed.
*
* Note: Armored against replacements that would generate huge strings.
* Note: If "from" is an empty string, single space is used instead.
* @param Parser $parser
* @param string $inStr
* @param string $inReplaceFrom
* @param string $inReplaceTo
* @param int $inLimit
* @return mixed|string
*/
public static function runReplace( $parser, $inStr = '',
$inReplaceFrom = '', $inReplaceTo = '', $inLimit = -1 ) {
global $wgPFStringLengthLimit;
$inStr = $parser->killMarkers( (string)$inStr );
$inReplaceFrom = $parser->killMarkers( (string)$inReplaceFrom );
$inReplaceTo = $parser->killMarkers( (string)$inReplaceTo );
if ( !self::checkLength( $inStr ) ||
2012-02-06 04:37:05 +00:00
!self::checkLength( $inReplaceFrom ) ||
!self::checkLength( $inReplaceTo ) ) {
return self::tooLongError();
}
if ( $inReplaceFrom === '' ) {
$inReplaceFrom = ' ';
}
// Precompute limit to avoid generating enormous string:
$diff = mb_strlen( $inReplaceTo ) - mb_strlen( $inReplaceFrom );
if ( $diff > 0 ) {
$limit = ( ( $wgPFStringLengthLimit - mb_strlen( $inStr ) ) / $diff ) + 1;
} else {
$limit = -1;
}
$inLimit = (int)$inLimit;
if ( $inLimit >= 0 ) {
if ( $limit > $inLimit || $limit == -1 ) {
$limit = $inLimit;
}
}
// Use regex to allow limit and handle UTF-8 correctly.
$inReplaceFrom = preg_quote( $inReplaceFrom, '/' );
$inReplaceTo = StringUtils::escapeRegexReplacement( $inReplaceTo );
$result = preg_replace( '/' . $inReplaceFrom . '/u',
$inReplaceTo, $inStr, $limit );
if ( !self::checkLength( $result ) ) {
return self::tooLongError();
}
return $result;
}
/**
* {{#explode:string | delimiter | position | limit}}
*
* Breaks "string" into chunks separated by "delimiter" and returns the
* chunk identified by "position".
*
* Note: Negative position can be used to specify tokens from the end.
* Note: If the divider is an empty string, single space is used instead.
* Note: Empty string is returned if there are not enough exploded chunks.
* @param Parser $parser
* @param string $inStr
* @param string $inDiv
* @param int $inPos
* @param int|null $inLim
* @return string
*/
public static function runExplode(
$parser, $inStr = '', $inDiv = '', $inPos = 0, $inLim = null
) {
$inStr = $parser->killMarkers( (string)$inStr );
$inDiv = $parser->killMarkers( (string)$inDiv );
if ( $inDiv === '' ) {
$inDiv = ' ';
}
if ( !self::checkLength( $inStr ) ||
2012-02-06 04:37:05 +00:00
!self::checkLength( $inDiv ) ) {
return self::tooLongError();
}
$inDiv = preg_quote( $inDiv, '/' );
$matches = preg_split( '/' . $inDiv . '/u', $inStr, $inLim );
if ( $inPos >= 0 && isset( $matches[$inPos] ) ) {
$result = $matches[$inPos];
} elseif ( $inPos < 0 && isset( $matches[count( $matches ) + $inPos] ) ) {
$result = $matches[count( $matches ) + $inPos];
} else {
$result = '';
}
return $result;
}
/**
* {{#urldecode:string}}
*
* Decodes URL-encoded (like%20that) strings.
* @param Parser $parser
* @param string $inStr
* @return string
*/
public static function runUrlDecode( $parser, $inStr = '' ) {
$inStr = $parser->killMarkers( (string)$inStr );
if ( !self::checkLength( $inStr ) ) {
return self::tooLongError();
}
return urldecode( $inStr );
}
/**
* Take a PPNode (-ish thing), expand it, remove entities, and trim.
*
* For use when doing string comparisions, where user expects entities
* to be equal for what they stand for (e.g. comparisions with {{PAGENAME}})
*
* @param PPNode|string $obj Thing to expand
* @param PPFrame $frame
* @param string &$trimExpanded Expanded and trimmed version of PPNode,
* but with char refs intact
* @return string The trimmed, expanded and entity reference decoded version of the PPNode
*/
private static function decodeTrimExpand( $obj, $frame, &$trimExpanded = null ) {
$expanded = $frame->expand( $obj );
$trimExpanded = trim( $expanded );
return trim( Sanitizer::decodeCharReferences( $expanded ) );
}
}