7 use Wikimedia\Reflection\GhostFieldAccessTrait;
32 use GhostFieldAccessTrait;
240 private const SPECULATIVE_FIELDS = [
241 'speculativePageIdUsed',
243 'revisionTimestampUsed'
265 '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#s';
285 public function __construct( $text =
'', $languageLinks = [], $categoryLinks = [],
286 $unused =
false, $titletext =
''
288 $this->mText = $text;
289 $this->mLanguageLinks = $languageLinks;
290 $this->mCategories = $categoryLinks;
291 $this->mTitleText = $titletext;
305 return ( $this->mText !==
null );
317 if ( $this->mText ===
null ) {
318 throw new LogicException(
'This ParserOutput contains no text!' );
353 'enableSectionEditLinks' =>
true,
356 'deduplicateStyles' =>
true,
361 Hooks::runner()->onParserOutputPostCacheTransform( $this, $text, $options );
363 if ( $options[
'wrapperDivClass'] !==
'' && !$options[
'unwrap'] ) {
364 $text =
Html::rawElement(
'div', [
'class' => $options[
'wrapperDivClass'] ], $text );
367 if ( $options[
'enableSectionEditLinks'] ) {
371 $text = preg_replace_callback(
372 self::EDITSECTION_REGEX,
373 function ( $m ) use ( $skin ) {
375 $editsectionSection = htmlspecialchars_decode( $m[2] );
378 if ( !is_object( $editsectionPage ) ) {
379 LoggerFactory::getInstance(
'Parser' )
381 'ParserOutput::getText(): bad title in editsection placeholder',
383 'placeholder' => $m[0],
384 'editsectionPage' => $m[1],
392 return $skin->doEditSectionLink(
402 $text = preg_replace( self::EDITSECTION_REGEX,
'', $text );
405 if ( $options[
'allowTOC'] ) {
408 $text = preg_replace(
415 if ( $options[
'deduplicateStyles'] ) {
417 $text = preg_replace_callback(
418 '#<style\s+([^>]*data-mw-deduplicate\s*=[^>]*)>.*?</style>#s',
419 function ( $m ) use ( &$seen ) {
421 if ( !isset( $attr[
'data-mw-deduplicate'] ) ) {
425 $key = $attr[
'data-mw-deduplicate'];
426 if ( !isset( $seen[$key] ) ) {
436 'rel' =>
'mw-deduplicated-inline-style',
445 $text = preg_replace_callback(
446 '#<mw:slotheader>(.*?)</mw:slotheader>#',
448 $role = htmlspecialchars_decode( $m[1] );
464 $this->mText .=
"\n<!-- $msg\n -->\n";
473 $this->mWrapperDivClasses[$class] =
true;
481 $this->mWrapperDivClasses = [];
492 return implode(
' ', array_keys( $this->mWrapperDivClasses ) );
500 $this->mSpeculativeRevId = $id;
516 $this->speculativePageIdUsed = $id;
532 $this->revisionTimestampUsed = $timestamp;
548 if ( $hash ===
null ) {
553 $this->revisionUsedSha1Base36 !==
null &&
554 $this->revisionUsedSha1Base36 !== $hash
556 $this->revisionUsedSha1Base36 =
'';
558 $this->revisionUsedSha1Base36 = $hash;
579 return array_keys( $this->mCategories );
635 $this->mNoGallery = (bool)$value;
667 return array_keys( $this->mWarnings );
725 return wfSetVar( $this->mText, $text );
729 return wfSetVar( $this->mLanguageLinks, $ll );
733 return wfSetVar( $this->mCategories, $cl );
741 return wfSetVar( $this->mSections, $toc );
745 return wfSetVar( $this->mIndexPolicy, $policy );
749 return wfSetVar( $this->mTOCHTML, $tochtml );
753 return wfSetVar( $this->mTimestamp, $timestamp );
757 $this->mCategories[$c] = $sort;
777 $this->mEnableOOUI = $enable;
781 $this->mLanguageLinks[] =
$t;
785 $this->mWarnings[
$s] = 1;
789 $this->mOutputHooks[] = [ $hook, $data ];
793 $this->mNewSection = (bool)$value;
797 $this->mHideNewSection = (bool)$value;
816 return (
bool)preg_match(
'/^' .
817 # If server is proto relative, check also
for http/https links
818 ( substr( $internal, 0, 2 ) ===
'//' ?
'(?:https?:)?' :
'' ) .
819 preg_quote( $internal,
'/' ) .
820 # check
for query/path/anchor or end of link in each
case
827 # We don't register links pointing to our own server, unless... :-)
830 # Replace unnecessary URL escape codes with the referenced character
831 # This prevents spammers from hiding links from the filters
834 $registerExternalLink =
true;
838 if ( $registerExternalLink ) {
839 $this->mExternalLinks[$url] = 1;
850 if (
$title->isExternal() ) {
855 $ns =
$title->getNamespace();
856 $dbk =
$title->getDBkey();
863 $this->mLinksSpecial[$dbk] = 1;
865 } elseif ( $dbk ===
'' ) {
869 if ( !isset( $this->mLinks[$ns] ) ) {
870 $this->mLinks[$ns] = [];
872 if ( $id ===
null ) {
873 $id =
$title->getArticleID();
875 $this->mLinks[$ns][$dbk] = $id;
884 public function addImage( $name, $timestamp =
null, $sha1 =
null ) {
885 $this->mImages[$name] = 1;
886 if ( $timestamp !==
null && $sha1 !==
null ) {
887 $this->mFileSearchOptions[$name] = [
'time' => $timestamp,
'sha1' => $sha1 ];
898 $ns =
$title->getNamespace();
899 $dbk =
$title->getDBkey();
900 if ( !isset( $this->mTemplates[$ns] ) ) {
901 $this->mTemplates[$ns] = [];
903 $this->mTemplates[$ns][$dbk] = $page_id;
904 if ( !isset( $this->mTemplateIds[$ns] ) ) {
905 $this->mTemplateIds[$ns] = [];
907 $this->mTemplateIds[$ns][$dbk] = $rev_id;
915 if ( !
$title->isExternal() ) {
916 throw new MWException(
'Non-interwiki link passed, internal parser error.' );
918 $prefix =
$title->getInterwiki();
919 if ( !isset( $this->mInterwikiLinks[$prefix] ) ) {
920 $this->mInterwikiLinks[$prefix] = [];
922 $this->mInterwikiLinks[$prefix][
$title->getDBkey()] = 1;
933 if ( $tag !==
false ) {
934 $this->mHeadItems[$tag] = $section;
936 $this->mHeadItems[] = $section;
945 $this->mModules = array_merge( $this->mModules, (array)
$modules );
953 $this->mModuleStyles = array_merge( $this->mModuleStyles, (array)
$modules );
964 if ( is_array(
$keys ) ) {
965 foreach (
$keys as $key => $value ) {
966 $this->mJsConfigVars[$key] = $value;
971 $this->mJsConfigVars[
$keys] = $value;
1005 if (
$title->isSpecialPage() ) {
1006 wfDebug( __METHOD__ .
": Not adding tracking category $msg to special page!" );
1013 ->inContentLanguage()
1016 # Allow tracking categories to be disabled by setting them to "-"
1017 if ( $cat ===
'-' ) {
1022 if ( $containerCategory ) {
1023 $this->
addCategory( $containerCategory->getDBkey(), $this->getProperty(
'defaultsort' ) ?:
'' );
1026 wfDebug( __METHOD__ .
": [[MediaWiki:$msg]] is not a valid title!" );
1069 $this->mFlags[$flag] =
true;
1077 return isset( $this->mFlags[$flag] );
1085 return array_keys( $this->mFlags );
1154 $this->mProperties[$name] = $value;
1166 return $this->mProperties[$name] ??
false;
1170 unset( $this->mProperties[$name] );
1174 if ( !isset( $this->mProperties ) ) {
1175 $this->mProperties = [];
1224 if ( $value ===
null ) {
1225 unset( $this->mExtensionData[$key] );
1227 $this->mExtensionData[$key] = $value;
1243 return $this->mExtensionData[$key] ??
null;
1248 if ( !$clock || $clock ===
'wall' ) {
1249 $ret[
'wall'] = microtime(
true );
1251 if ( !$clock || $clock ===
'cpu' ) {
1252 $ru = getrusage( 0 );
1253 $ret[
'cpu'] = $ru[
'ru_utime.tv_sec'] + $ru[
'ru_utime.tv_usec'] / 1e6;
1254 $ret[
'cpu'] += $ru[
'ru_stime.tv_sec'] + $ru[
'ru_stime.tv_usec'] / 1e6;
1279 if ( !isset( $this->mParseStartTime[$clock] ) ) {
1284 return $end[$clock] - $this->mParseStartTime[$clock];
1307 $this->mLimitReportData[$key] = $value;
1309 if ( is_array( $value ) ) {
1310 if ( array_keys( $value ) === [ 0, 1 ]
1311 && is_numeric( $value[0] )
1312 && is_numeric( $value[1] )
1314 $data = [
'value' => $value[0],
'limit' => $value[1] ];
1322 if ( strpos( $key,
'-' ) ) {
1323 list( $ns, $name ) = explode(
'-', $key, 2 );
1324 $this->mLimitReportJSData[$ns][$name] = $data;
1326 $this->mLimitReportJSData[$key] = $data;
1354 return wfSetVar( $this->mPreventClickjacking, $flag );
1364 $this->mMaxAdaptiveExpiry = min( $ttl, $this->mMaxAdaptiveExpiry );
1378 $this->mExtraDefaultSrcs[] = $src;
1388 $this->mExtraStyleSrcs[] = $src;
1400 $this->mExtraScriptSrcs[] = $src;
1409 if ( is_infinite( $this->mMaxAdaptiveExpiry ) ) {
1414 if ( is_float( $runtime ) ) {
1416 / ( self::PARSE_SLOW_SEC - self::PARSE_FAST_SEC );
1418 $point = self::SLOW_AR_TTL - self::PARSE_SLOW_SEC * $slope;
1421 max( $slope * $runtime + $point, self::MIN_AR_TTL ),
1422 $this->mMaxAdaptiveExpiry
1429 return array_filter( array_keys( get_object_vars( $this ) ),
1430 function ( $field ) {
1431 if ( $field ===
'mParseStartTime' ) {
1433 } elseif ( strpos( $field,
"\0" ) !==
false ) {
1454 $this->mTimestamp = $this->
useMaxValue( $this->mTimestamp,
$source->getTimestamp() );
1456 foreach ( self::SPECULATIVE_FIELDS as $field ) {
1457 if ( $this->$field &&
$source->$field && $this->$field !==
$source->$field ) {
1458 wfLogWarning( __METHOD__ .
": inconsistent '$field' properties!" );
1464 $this->mParseStartTime,
1472 if ( empty( $this->mLimitReportData ) ) {
1473 $this->mLimitReportData =
$source->mLimitReportData;
1475 if ( empty( $this->mLimitReportJSData ) ) {
1476 $this->mLimitReportJSData =
$source->mLimitReportJSData;
1493 $this->mMaxAdaptiveExpiry = min( $this->mMaxAdaptiveExpiry,
$source->mMaxAdaptiveExpiry );
1495 $this->mExtraStyleSrcs,
1496 $source->getExtraCSPStyleSrcs()
1499 $this->mExtraScriptSrcs,
1500 $source->getExtraCSPScriptSrcs()
1503 $this->mExtraDefaultSrcs,
1504 $source->getExtraCSPDefaultSrcs()
1508 if ( $this->mIndexPolicy ===
'noindex' ||
$source->mIndexPolicy ===
'noindex' ) {
1509 $this->mIndexPolicy =
'noindex';
1510 } elseif ( $this->mIndexPolicy !==
'index' ) {
1511 $this->mIndexPolicy =
$source->mIndexPolicy;
1515 $this->mNewSection = $this->mNewSection ||
$source->getNewSection();
1516 $this->mHideNewSection = $this->mHideNewSection ||
$source->getHideNewSection();
1517 $this->mNoGallery = $this->mNoGallery ||
$source->getNoGallery();
1518 $this->mEnableOOUI = $this->mEnableOOUI ||
$source->getEnableOOUI();
1519 $this->mPreventClickjacking = $this->mPreventClickjacking ||
$source->preventClickjacking();
1522 $this->mSections = array_merge( $this->mSections,
$source->getSections() );
1523 $this->mTOCHTML .=
$source->mTOCHTML;
1527 if ( $this->mTitleText ===
null || $this->mTitleText ===
'' ) {
1528 $this->mTitleText =
$source->mTitleText;
1533 $this->mWrapperDivClasses,
1544 $this->mExtensionData,
1564 $this->mFileSearchOptions,
1565 $source->getFileSearchOptions()
1569 $this->mInterwikiLinks,
1581 $this->mExtensionData,
1587 return array_unique( array_merge( $a, $b ), SORT_REGULAR );
1591 return array_values( array_unique( array_merge( $a, $b ), SORT_REGULAR ) );
1594 private static function mergeMap( array $a, array $b ) {
1595 return array_replace( $a, $b );
1598 private static function merge2D( array $a, array $b ) {
1600 $keys = array_merge( array_keys( $a ), array_keys( $b ) );
1602 foreach (
$keys as $k ) {
1603 if ( empty( $a[$k] ) ) {
1604 $values[$k] = $b[$k];
1605 } elseif ( empty( $b[$k] ) ) {
1606 $values[$k] = $a[$k];
1607 } elseif ( is_array( $a[$k] ) && is_array( $b[$k] ) ) {
1608 $values[$k] = array_replace( $a[$k], $b[$k] );
1610 $values[$k] = $b[$k];
1619 $keys = array_merge( array_keys( $a ), array_keys( $b ) );
1621 foreach (
$keys as $k ) {
1622 if ( is_array( $a[$k] ??
null ) && is_array( $b[$k] ??
null ) ) {
1633 if ( $a ===
null ) {
1637 if ( $b ===
null ) {
1641 return min( $a, $b );
1645 if ( $a ===
null ) {
1649 if ( $b ===
null ) {
1653 return max( $a, $b );
1710 $data += parent::toJsonArray();
1714 if ( $this->mMaxAdaptiveExpiry !== INF ) {
1724 $parserOutput->initFromJson( $unserializer, $json );
1725 return $parserOutput;
1734 parent::initFromJson( $unserializer, $jsonData );
1736 $this->mText = $jsonData[
'Text'];
1737 $this->mLanguageLinks = $jsonData[
'LanguageLinks'];
1738 $this->mCategories = $jsonData[
'Categories'];
1739 $this->mIndicators = $jsonData[
'Indicators'];
1740 $this->mTitleText = $jsonData[
'TitleText'];
1741 $this->mLinks = $jsonData[
'Links'];
1742 $this->mLinksSpecial = $jsonData[
'LinksSpecial'];
1743 $this->mTemplates = $jsonData[
'Templates'];
1744 $this->mTemplateIds = $jsonData[
'TemplateIds'];
1745 $this->mImages = $jsonData[
'Images'];
1746 $this->mFileSearchOptions = $jsonData[
'FileSearchOptions'];
1747 $this->mExternalLinks = $jsonData[
'ExternalLinks'];
1748 $this->mInterwikiLinks = $jsonData[
'InterwikiLinks'];
1749 $this->mNewSection = $jsonData[
'NewSection'];
1750 $this->mHideNewSection = $jsonData[
'HideNewSection'];
1751 $this->mNoGallery = $jsonData[
'NoGallery'];
1752 $this->mHeadItems = $jsonData[
'HeadItems'];
1753 $this->mModules = $jsonData[
'Modules'];
1754 $this->mModuleStyles = $jsonData[
'ModuleStyles'];
1755 $this->mJsConfigVars = $jsonData[
'JsConfigVars'];
1756 $this->mOutputHooks = $jsonData[
'OutputHooks'];
1757 $this->mWarnings = $jsonData[
'Warnings'];
1758 $this->mSections = $jsonData[
'Sections'];
1760 $this->mTOCHTML = $jsonData[
'TOCHTML'];
1761 $this->mTimestamp = $jsonData[
'Timestamp'];
1762 $this->mEnableOOUI = $jsonData[
'EnableOOUI'];
1763 $this->mIndexPolicy = $jsonData[
'IndexPolicy'];
1764 $this->mExtensionData = $unserializer->
unserializeArray( $jsonData[
'ExtensionData'] ?? [] );
1765 $this->mExtensionData = $jsonData[
'ExtensionData'];
1766 $this->mLimitReportData = $jsonData[
'LimitReportData'];
1767 $this->mLimitReportJSData = $jsonData[
'LimitReportJSData'];
1768 $this->mParseStartTime = $jsonData[
'ParseStartTime'];
1769 $this->mPreventClickjacking = $jsonData[
'PreventClickjacking'];
1770 $this->mExtraScriptSrcs = $jsonData[
'ExtraScriptSrcs'];
1771 $this->mExtraDefaultSrcs = $jsonData[
'ExtraDefaultSrcs'];
1772 $this->mExtraStyleSrcs = $jsonData[
'ExtraStyleSrcs'];
1773 $this->mFlags = $jsonData[
'Flags'];
1774 $this->mSpeculativeRevId = $jsonData[
'SpeculativeRevId'];
1775 $this->speculativePageIdUsed = $jsonData[
'SpeculativePageIdUsed'];
1776 $this->revisionTimestampUsed = $jsonData[
'RevisionTimestampUsed'];
1777 $this->revisionUsedSha1Base36 = $jsonData[
'RevisionUsedSha1Base36'];
1778 $this->mWrapperDivClasses = $jsonData[
'WrapperDivClasses'];
1779 $this->mMaxAdaptiveExpiry = $jsonData[
'MaxAdaptiveExpiry'] ?? INF;
1792 foreach ( $properties as $key => $value ) {
1793 if ( is_string( $value ) ) {
1794 if ( !mb_detect_encoding( $value,
'UTF-8',
true ) ) {
1795 $properties[$key] = [
1796 '_type_' =>
'string',
1797 '_encoding_' =>
'base64',
1798 '_data_' => base64_encode( $value ),
1816 foreach ( $properties as $key => $value ) {
1817 if ( is_array( $value ) && isset( $value[
'_encoding_'] ) ) {
1818 if ( $value[
'_encoding_'] ===
'base64' ) {
1819 $properties[$key] = base64_decode( $value[
'_data_'] );
1829 $priorAccessedOptions = $this->getGhostFieldValue(
'mAccessedOptions' );
1830 if ( $priorAccessedOptions ) {
1831 $this->mParseUsedOptions = $priorAccessedOptions;