+
+ // TODO remove this method once old parser cache objects have expired, probably mid-October 2018
+ public function __wakeup() {
+ // T203716 remove wrapper that was added by logic in an older version of this class,
+ // where the wrapper was included in mText. This might sometimes remove a wrapper that's
+ // genuine content (manually added to a system message), but that will work out OK, see below.
+ $text = $this->getRawText();
+ $start = Html::openElement( 'div', [
+ 'class' => 'mw-parser-output'
+ ] );
+ $startLen = strlen( $start );
+ $end = Html::closeElement( 'div' );
+ $endPos = strrpos( $text, $end );
+ $endLen = strlen( $end );
+ if ( substr( $text, 0, $startLen ) === $start && $endPos !== false
+ // if the closing div is followed by real content, bail out of unwrapping
+ && preg_match( '/^(?>\s*<!--.*?-->)*\s*$/s', substr( $text, $endPos + $endLen ) )
+ ) {
+ $text = substr( $text, $startLen );
+ $text = substr( $text, 0, $endPos - $startLen ) .
+ substr( $text, $endPos - $startLen + $endLen );
+ $this->setText( $text );
+ // We found a wrapper to remove, so the ParserOutput was probably created by the
+ // code path that now contains an addWrapperDivClass( 'mw-parser-output' ) call,
+ // but it did not contain it when this object was cached, so we need to fix the
+ // wrapper class variable.
+ // If this was a message with a manually added wrapper, we are technically wrong about
+ // this but we were wrong about the unwrapping as well so it will work out just right,
+ // except when this is a normal page view of such a message page, in which case
+ // it will be single-wrapped instead of double-wrapped (harmless) or something wants
+ // render the message with unwrap=true (in which case the message won't be wrapped even
+ // though it should, but the few code paths using unwrap=true only do it for real pages).
+ $this->clearWrapperDivClass();
+ $this->addWrapperDivClass( 'mw-parser-output' );
+ }
+ }
+
+ /**
+ * Merges internal metadata such as flags, accessed options, and profiling info
+ * from $source into this ParserOutput. This should be used whenever the state of $source
+ * has any impact on the state of this ParserOutput.
+ *
+ * @param ParserOutput $source
+ */
+ public function mergeInternalMetaDataFrom( ParserOutput $source ) {
+ $this->mOutputHooks = self::mergeList( $this->mOutputHooks, $source->getOutputHooks() );
+ $this->mWarnings = self::mergeMap( $this->mWarnings, $source->mWarnings ); // don't use getter
+ $this->mTimestamp = $this->useMaxValue( $this->mTimestamp, $source->getTimestamp() );
+
+ if ( $this->mSpeculativeRevId && $source->mSpeculativeRevId
+ && $this->mSpeculativeRevId !== $source->mSpeculativeRevId
+ ) {
+ wfLogWarning(
+ 'Inconsistent speculative revision ID encountered while merging parser output!'
+ );
+ }
+
+ $this->mSpeculativeRevId = $this->useMaxValue(
+ $this->mSpeculativeRevId,
+ $source->getSpeculativeRevIdUsed()
+ );
+ $this->mParseStartTime = $this->useEachMinValue(
+ $this->mParseStartTime,
+ $source->mParseStartTime
+ );
+
+ $this->mFlags = self::mergeMap( $this->mFlags, $source->mFlags );
+ $this->mAccessedOptions = self::mergeMap( $this->mAccessedOptions, $source->mAccessedOptions );
+
+ // TODO: maintain per-slot limit reports!
+ if ( empty( $this->mLimitReportData ) ) {
+ $this->mLimitReportData = $source->mLimitReportData;
+ }
+ if ( empty( $this->mLimitReportJSData ) ) {
+ $this->mLimitReportJSData = $source->mLimitReportJSData;
+ }
+ }
+
+ /**
+ * Merges HTML metadata such as head items, JS config vars, and HTTP cache control info
+ * from $source into this ParserOutput. This should be used whenever the HTML in $source
+ * has been somehow mered into the HTML of this ParserOutput.
+ *
+ * @param ParserOutput $source
+ */
+ public function mergeHtmlMetaDataFrom( ParserOutput $source ) {
+ // HTML and HTTP
+ $this->mHeadItems = self::mergeMixedList( $this->mHeadItems, $source->getHeadItems() );
+ $this->mModules = self::mergeList( $this->mModules, $source->getModules() );
+ $this->mModuleScripts = self::mergeList( $this->mModuleScripts, $source->getModuleScripts() );
+ $this->mModuleStyles = self::mergeList( $this->mModuleStyles, $source->getModuleStyles() );
+ $this->mJsConfigVars = self::mergeMap( $this->mJsConfigVars, $source->getJsConfigVars() );
+ $this->mMaxAdaptiveExpiry = min( $this->mMaxAdaptiveExpiry, $source->mMaxAdaptiveExpiry );
+
+ // "noindex" always wins!
+ if ( $this->mIndexPolicy === 'noindex' || $source->mIndexPolicy === 'noindex' ) {
+ $this->mIndexPolicy = 'noindex';
+ } elseif ( $this->mIndexPolicy !== 'index' ) {
+ $this->mIndexPolicy = $source->mIndexPolicy;
+ }
+
+ // Skin control
+ $this->mNewSection = $this->mNewSection || $source->getNewSection();
+ $this->mHideNewSection = $this->mHideNewSection || $source->getHideNewSection();
+ $this->mNoGallery = $this->mNoGallery || $source->getNoGallery();
+ $this->mEnableOOUI = $this->mEnableOOUI || $source->getEnableOOUI();
+ $this->mPreventClickjacking = $this->mPreventClickjacking || $source->preventClickjacking();
+
+ // TODO: we'll have to be smarter about this!
+ $this->mSections = array_merge( $this->mSections, $source->getSections() );
+ $this->mTOCHTML = $this->mTOCHTML . $source->mTOCHTML;
+
+ // XXX: we don't want to concatenate title text, so first write wins.
+ // We should use the first *modified* title text, but we don't have the original to check.
+ if ( $this->mTitleText === null || $this->mTitleText === '' ) {
+ $this->mTitleText = $source->mTitleText;
+ }
+
+ // class names are stored in array keys
+ $this->mWrapperDivClasses = self::mergeMap(
+ $this->mWrapperDivClasses,
+ $source->mWrapperDivClasses
+ );
+
+ // NOTE: last write wins, same as within one ParserOutput
+ $this->mIndicators = self::mergeMap( $this->mIndicators, $source->getIndicators() );
+
+ // NOTE: include extension data in "tracking meta data" as well as "html meta data"!
+ // TODO: add a $mergeStrategy parameter to setExtensionData to allow different
+ // kinds of extension data to be merged in different ways.
+ $this->mExtensionData = self::mergeMap(
+ $this->mExtensionData,
+ $source->mExtensionData
+ );
+ }
+
+ /**
+ * Merges dependency tracking metadata such as backlinks, images used, and extension data
+ * from $source into this ParserOutput. This allows dependency tracking to be done for the
+ * combined output of multiple content slots.
+ *
+ * @param ParserOutput $source
+ */
+ public function mergeTrackingMetaDataFrom( ParserOutput $source ) {
+ $this->mLanguageLinks = self::mergeList( $this->mLanguageLinks, $source->getLanguageLinks() );
+ $this->mCategories = self::mergeMap( $this->mCategories, $source->getCategories() );
+ $this->mLinks = self::merge2D( $this->mLinks, $source->getLinks() );
+ $this->mTemplates = self::merge2D( $this->mTemplates, $source->getTemplates() );
+ $this->mTemplateIds = self::merge2D( $this->mTemplateIds, $source->getTemplateIds() );
+ $this->mImages = self::mergeMap( $this->mImages, $source->getImages() );
+ $this->mFileSearchOptions = self::mergeMap(
+ $this->mFileSearchOptions,
+ $source->getFileSearchOptions()
+ );
+ $this->mExternalLinks = self::mergeMap( $this->mExternalLinks, $source->getExternalLinks() );
+ $this->mInterwikiLinks = self::merge2D(
+ $this->mInterwikiLinks,
+ $source->getInterwikiLinks()
+ );
+
+ // TODO: add a $mergeStrategy parameter to setProperty to allow different
+ // kinds of properties to be merged in different ways.
+ $this->mProperties = self::mergeMap( $this->mProperties, $source->getProperties() );
+
+ // NOTE: include extension data in "tracking meta data" as well as "html meta data"!
+ // TODO: add a $mergeStrategy parameter to setExtensionData to allow different
+ // kinds of extension data to be merged in different ways.
+ $this->mExtensionData = self::mergeMap(
+ $this->mExtensionData,
+ $source->mExtensionData
+ );
+ }
+
+ private static function mergeMixedList( array $a, array $b ) {
+ return array_unique( array_merge( $a, $b ), SORT_REGULAR );
+ }
+
+ private static function mergeList( array $a, array $b ) {
+ return array_values( array_unique( array_merge( $a, $b ), SORT_REGULAR ) );
+ }
+
+ private static function mergeMap( array $a, array $b ) {
+ return array_replace( $a, $b );
+ }
+
+ private static function merge2D( array $a, array $b ) {
+ $values = [];
+ $keys = array_merge( array_keys( $a ), array_keys( $b ) );
+
+ foreach ( $keys as $k ) {
+ if ( empty( $a[$k] ) ) {
+ $values[$k] = $b[$k];
+ } elseif ( empty( $b[$k] ) ) {
+ $values[$k] = $a[$k];
+ } elseif ( is_array( $a[$k] ) && is_array( $b[$k] ) ) {
+ $values[$k] = array_replace( $a[$k], $b[$k] );
+ } else {
+ $values[$k] = $b[$k];
+ }
+ }
+
+ return $values;
+ }
+
+ private static function useEachMinValue( array $a, array $b ) {
+ $values = [];
+ $keys = array_merge( array_keys( $a ), array_keys( $b ) );
+
+ foreach ( $keys as $k ) {
+ if ( is_array( $a[$k] ?? null ) && is_array( $b[$k] ?? null ) ) {
+ $values[$k] = self::useEachMinValue( $a[$k], $b[$k] );
+ } else {
+ $values[$k] = self::useMinValue( $a[$k] ?? null, $b[$k] ?? null );
+ }
+ }
+
+ return $values;
+ }
+
+ private static function useMinValue( $a, $b ) {
+ if ( $a === null ) {
+ return $b;
+ }
+
+ if ( $b === null ) {
+ return $a;
+ }
+
+ return min( $a, $b );
+ }
+
+ private static function useMaxValue( $a, $b ) {
+ if ( $a === null ) {
+ return $b;
+ }
+
+ if ( $b === null ) {
+ return $a;
+ }
+
+ return max( $a, $b );
+ }
+