Merge "Require indentation of CASE statements in PHP code"
[lhc/web/wiklou.git] / includes / parser / Parser.php
index 49f2ce1..10a338e 100644 (file)
@@ -406,13 +406,6 @@ class Parser {
                $text, Title $title, ParserOptions $options,
                $linestart = true, $clearState = true, $revid = null
        ) {
-               /**
-                * First pass--just handle <nowiki> sections, pass the rest off
-                * to internalParse() which does all the real work.
-                */
-
-               global $wgShowHostnames;
-
                if ( $clearState ) {
                        // We use U+007F DELETE to construct strip markers, so we have to make
                        // sure that this character does not occur in the input text.
@@ -474,7 +467,7 @@ class Parser {
                        }
                }
 
-               # Done parsing! Compute runtime adaptive expiry if set
+               # Compute runtime adaptive expiry if set
                $this->mOutput->finalizeAdaptiveCacheExpiry();
 
                # Warn if too many heavyweight parser functions were used
@@ -485,110 +478,9 @@ class Parser {
                        );
                }
 
-               # Information on include size limits, for the benefit of users who try to skirt them
+               # Information on limits, for the benefit of users who try to skirt them
                if ( $this->mOptions->getEnableLimitReport() ) {
-                       $max = $this->mOptions->getMaxIncludeSize();
-
-                       $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
-                       if ( $cpuTime !== null ) {
-                               $this->mOutput->setLimitReportData( 'limitreport-cputime',
-                                       sprintf( "%.3f", $cpuTime )
-                               );
-                       }
-
-                       $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
-                       $this->mOutput->setLimitReportData( 'limitreport-walltime',
-                               sprintf( "%.3f", $wallTime )
-                       );
-
-                       $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
-                               [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
-                       );
-                       $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
-                               [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
-                       );
-                       $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
-                               [ $this->mIncludeSizes['post-expand'], $max ]
-                       );
-                       $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
-                               [ $this->mIncludeSizes['arg'], $max ]
-                       );
-                       $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
-                               [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
-                       );
-                       $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
-                               [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
-                       );
-                       Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
-
-                       $limitReport = "NewPP limit report\n";
-                       if ( $wgShowHostnames ) {
-                               $limitReport .= 'Parsed by ' . wfHostname() . "\n";
-                       }
-                       $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
-                       $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
-                       $limitReport .= 'Dynamic content: ' .
-                               ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
-                               "\n";
-
-                       foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
-                               if ( Hooks::run( 'ParserLimitReportFormat',
-                                       [ $key, &$value, &$limitReport, false, false ]
-                               ) ) {
-                                       $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
-                                       $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
-                                               ->inLanguage( 'en' )->useDatabase( false );
-                                       if ( !$valueMsg->exists() ) {
-                                               $valueMsg = new RawMessage( '$1' );
-                                       }
-                                       if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
-                                               $valueMsg->params( $value );
-                                               $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
-                                       }
-                               }
-                       }
-                       // Since we're not really outputting HTML, decode the entities and
-                       // then re-encode the things that need hiding inside HTML comments.
-                       $limitReport = htmlspecialchars_decode( $limitReport );
-                       // Run deprecated hook
-                       Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ], '1.22' );
-
-                       // Sanitize for comment. Note '‐' in the replacement is U+2010,
-                       // which looks much like the problematic '-'.
-                       $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
-                       $text .= "\n<!-- \n$limitReport-->\n";
-
-                       // Add on template profiling data in human/machine readable way
-                       $dataByFunc = $this->mProfiler->getFunctionStats();
-                       uasort( $dataByFunc, function ( $a, $b ) {
-                               return $a['real'] < $b['real']; // descending order
-                       } );
-                       $profileReport = [];
-                       foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
-                               $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
-                                       $item['%real'], $item['real'], $item['calls'],
-                                       htmlspecialchars( $item['name'] ) );
-                       }
-                       $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
-                       $text .= implode( "\n", $profileReport ) . "\n-->\n";
-
-                       $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
-
-                       // Add other cache related metadata
-                       if ( $wgShowHostnames ) {
-                               $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
-                       }
-                       $this->mOutput->setLimitReportData( 'cachereport-timestamp',
-                               $this->mOutput->getCacheTime() );
-                       $this->mOutput->setLimitReportData( 'cachereport-ttl',
-                               $this->mOutput->getCacheExpiry() );
-                       $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
-                               $this->mOutput->hasDynamicContent() );
-
-                       if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
-                               wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
-                                       $this->mTitle->getPrefixedDBkey() );
-                       }
+                       $text .= $this->makeLimitReport();
                }
 
                # Wrap non-interface parser output in a <div> so it can be targeted
@@ -611,6 +503,120 @@ class Parser {
                return $this->mOutput;
        }
 
+       /**
+        * Set the limit report data in the current ParserOutput, and return the
+        * limit report HTML comment.
+        *
+        * @return string
+        */
+       protected function makeLimitReport() {
+               global $wgShowHostnames;
+
+               $maxIncludeSize = $this->mOptions->getMaxIncludeSize();
+
+               $cpuTime = $this->mOutput->getTimeSinceStart( 'cpu' );
+               if ( $cpuTime !== null ) {
+                       $this->mOutput->setLimitReportData( 'limitreport-cputime',
+                               sprintf( "%.3f", $cpuTime )
+                       );
+               }
+
+               $wallTime = $this->mOutput->getTimeSinceStart( 'wall' );
+               $this->mOutput->setLimitReportData( 'limitreport-walltime',
+                       sprintf( "%.3f", $wallTime )
+               );
+
+               $this->mOutput->setLimitReportData( 'limitreport-ppvisitednodes',
+                       [ $this->mPPNodeCount, $this->mOptions->getMaxPPNodeCount() ]
+               );
+               $this->mOutput->setLimitReportData( 'limitreport-ppgeneratednodes',
+                       [ $this->mGeneratedPPNodeCount, $this->mOptions->getMaxGeneratedPPNodeCount() ]
+               );
+               $this->mOutput->setLimitReportData( 'limitreport-postexpandincludesize',
+                       [ $this->mIncludeSizes['post-expand'], $maxIncludeSize ]
+               );
+               $this->mOutput->setLimitReportData( 'limitreport-templateargumentsize',
+                       [ $this->mIncludeSizes['arg'], $maxIncludeSize ]
+               );
+               $this->mOutput->setLimitReportData( 'limitreport-expansiondepth',
+                       [ $this->mHighestExpansionDepth, $this->mOptions->getMaxPPExpandDepth() ]
+               );
+               $this->mOutput->setLimitReportData( 'limitreport-expensivefunctioncount',
+                       [ $this->mExpensiveFunctionCount, $this->mOptions->getExpensiveParserFunctionLimit() ]
+               );
+               Hooks::run( 'ParserLimitReportPrepare', [ $this, $this->mOutput ] );
+
+               $limitReport = "NewPP limit report\n";
+               if ( $wgShowHostnames ) {
+                       $limitReport .= 'Parsed by ' . wfHostname() . "\n";
+               }
+               $limitReport .= 'Cached time: ' . $this->mOutput->getCacheTime() . "\n";
+               $limitReport .= 'Cache expiry: ' . $this->mOutput->getCacheExpiry() . "\n";
+               $limitReport .= 'Dynamic content: ' .
+                       ( $this->mOutput->hasDynamicContent() ? 'true' : 'false' ) .
+                       "\n";
+
+               foreach ( $this->mOutput->getLimitReportData() as $key => $value ) {
+                       if ( Hooks::run( 'ParserLimitReportFormat',
+                               [ $key, &$value, &$limitReport, false, false ]
+                       ) ) {
+                               $keyMsg = wfMessage( $key )->inLanguage( 'en' )->useDatabase( false );
+                               $valueMsg = wfMessage( [ "$key-value-text", "$key-value" ] )
+                                       ->inLanguage( 'en' )->useDatabase( false );
+                               if ( !$valueMsg->exists() ) {
+                                       $valueMsg = new RawMessage( '$1' );
+                               }
+                               if ( !$keyMsg->isDisabled() && !$valueMsg->isDisabled() ) {
+                                       $valueMsg->params( $value );
+                                       $limitReport .= "{$keyMsg->text()}: {$valueMsg->text()}\n";
+                               }
+                       }
+               }
+               // Since we're not really outputting HTML, decode the entities and
+               // then re-encode the things that need hiding inside HTML comments.
+               $limitReport = htmlspecialchars_decode( $limitReport );
+               // Run deprecated hook
+               Hooks::run( 'ParserLimitReport', [ $this, &$limitReport ], '1.22' );
+
+               // Sanitize for comment. Note '‐' in the replacement is U+2010,
+               // which looks much like the problematic '-'.
+               $limitReport = str_replace( [ '-', '&' ], [ '‐', '&amp;' ], $limitReport );
+               $text = "\n<!-- \n$limitReport-->\n";
+
+               // Add on template profiling data in human/machine readable way
+               $dataByFunc = $this->mProfiler->getFunctionStats();
+               uasort( $dataByFunc, function ( $a, $b ) {
+                       return $a['real'] < $b['real']; // descending order
+               } );
+               $profileReport = [];
+               foreach ( array_slice( $dataByFunc, 0, 10 ) as $item ) {
+                       $profileReport[] = sprintf( "%6.2f%% %8.3f %6d %s",
+                               $item['%real'], $item['real'], $item['calls'],
+                               htmlspecialchars( $item['name'] ) );
+               }
+               $text .= "<!--\nTransclusion expansion time report (%,ms,calls,template)\n";
+               $text .= implode( "\n", $profileReport ) . "\n-->\n";
+
+               $this->mOutput->setLimitReportData( 'limitreport-timingprofile', $profileReport );
+
+               // Add other cache related metadata
+               if ( $wgShowHostnames ) {
+                       $this->mOutput->setLimitReportData( 'cachereport-origin', wfHostname() );
+               }
+               $this->mOutput->setLimitReportData( 'cachereport-timestamp',
+                       $this->mOutput->getCacheTime() );
+               $this->mOutput->setLimitReportData( 'cachereport-ttl',
+                       $this->mOutput->getCacheExpiry() );
+               $this->mOutput->setLimitReportData( 'cachereport-transientcontent',
+                       $this->mOutput->hasDynamicContent() );
+
+               if ( $this->mGeneratedPPNodeCount > $this->mOptions->getMaxGeneratedPPNodeCount() / 10 ) {
+                       wfDebugLog( 'generated-pp-node-count', $this->mGeneratedPPNodeCount . ' ' .
+                               $this->mTitle->getPrefixedDBkey() );
+               }
+               return $text;
+       }
+
        /**
         * Half-parse wikitext to half-parsed HTML. This recursive parser entry point
         * can be called from an extension tag hook.
@@ -2504,10 +2510,10 @@ class Parser {
                                $value = '|';
                                break;
                        case 'currentmonth':
-                               $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ) );
+                               $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'm' ), true );
                                break;
                        case 'currentmonth1':
-                               $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ) );
+                               $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'n' ), true );
                                break;
                        case 'currentmonthname':
                                $value = $pageLang->getMonthName( MWTimestamp::getInstance( $ts )->format( 'n' ) );
@@ -2519,16 +2525,16 @@ class Parser {
                                $value = $pageLang->getMonthAbbreviation( MWTimestamp::getInstance( $ts )->format( 'n' ) );
                                break;
                        case 'currentday':
-                               $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ) );
+                               $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'j' ), true );
                                break;
                        case 'currentday2':
-                               $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ) );
+                               $value = $pageLang->formatNum( MWTimestamp::getInstance( $ts )->format( 'd' ), true );
                                break;
                        case 'localmonth':
-                               $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ) );
+                               $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'm' ), true );
                                break;
                        case 'localmonth1':
-                               $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
+                               $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'n' ), true );
                                break;
                        case 'localmonthname':
                                $value = $pageLang->getMonthName( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
@@ -2540,10 +2546,10 @@ class Parser {
                                $value = $pageLang->getMonthAbbreviation( MWTimestamp::getLocalInstance( $ts )->format( 'n' ) );
                                break;
                        case 'localday':
-                               $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ) );
+                               $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'j' ), true );
                                break;
                        case 'localday2':
-                               $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ) );
+                               $value = $pageLang->formatNum( MWTimestamp::getLocalInstance( $ts )->format( 'd' ), true );
                                break;
                        case 'pagename':
                                $value = wfEscapeWikiText( $this->mTitle->getText() );
@@ -3492,13 +3498,7 @@ class Parser {
         * @return Revision|bool False if missing
         */
        public static function statelessFetchRevision( Title $title, $parser = false ) {
-               $pageId = $title->getArticleID();
-               $revId = $title->getLatestRevID();
-
-               $rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $pageId, $revId );
-               if ( $rev ) {
-                       $rev->setTitle( $title );
-               }
+               $rev = Revision::newKnownCurrent( wfGetDB( DB_REPLICA ), $title );
 
                return $rev;
        }
@@ -3944,7 +3944,7 @@ class Parser {
                        $this->mForceTocPosition = true;
 
                        # Set a placeholder. At the end we'll fill it in with the TOC.
-                       $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
+                       $text = $mw->replace( '<!--MWTOC\'"-->', $text, 1 );
 
                        # Only keep the first one.
                        $text = $mw->replace( '', $text );
@@ -4206,6 +4206,9 @@ class Parser {
 
                        # Decode HTML entities
                        $safeHeadline = Sanitizer::decodeCharReferences( $safeHeadline );
+
+                       $safeHeadline = self::normalizeSectionName( $safeHeadline );
+
                        $fallbackHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_FALLBACK );
                        $linkAnchor = Sanitizer::escapeIdForLink( $safeHeadline );
                        $safeHeadline = Sanitizer::escapeIdForAttribute( $safeHeadline, Sanitizer::ID_PRIMARY );
@@ -4387,7 +4390,7 @@ class Parser {
                $full .= implode( '', $sections );
 
                if ( $this->mForceTocPosition ) {
-                       return str_replace( '<!--MWTOC-->', $toc, $full );
+                       return str_replace( '<!--MWTOC\'"-->', $toc, $full );
                } else {
                        return $full;
                }
@@ -5019,40 +5022,40 @@ class Parser {
                                                $paramName = $paramMap[$magicName];
 
                                                switch ( $paramName ) {
-                                               case 'gallery-internal-alt':
-                                                       $alt = $this->stripAltText( $match, false );
-                                                       break;
-                                               case 'gallery-internal-link':
-                                                       $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
-                                                       $chars = self::EXT_LINK_URL_CLASS;
-                                                       $addr = self::EXT_LINK_ADDR;
-                                                       $prots = $this->mUrlProtocols;
-                                                       // check to see if link matches an absolute url, if not then it must be a wiki link.
-                                                       if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
-                                                               // Result of LanguageConverter::markNoConversion
-                                                               // invoked on an external link.
-                                                               $linkValue = substr( $linkValue, 4, -2 );
-                                                       }
-                                                       if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
-                                                               $link = $linkValue;
-                                                               $this->mOutput->addExternalLink( $link );
-                                                       } else {
-                                                               $localLinkTitle = Title::newFromText( $linkValue );
-                                                               if ( $localLinkTitle !== null ) {
-                                                                       $this->mOutput->addLink( $localLinkTitle );
-                                                                       $link = $localLinkTitle->getLinkURL();
+                                                       case 'gallery-internal-alt':
+                                                               $alt = $this->stripAltText( $match, false );
+                                                               break;
+                                                       case 'gallery-internal-link':
+                                                               $linkValue = strip_tags( $this->replaceLinkHoldersText( $match ) );
+                                                               $chars = self::EXT_LINK_URL_CLASS;
+                                                               $addr = self::EXT_LINK_ADDR;
+                                                               $prots = $this->mUrlProtocols;
+                                                               // check to see if link matches an absolute url, if not then it must be a wiki link.
+                                                               if ( preg_match( '/^-{R|(.*)}-$/', $linkValue ) ) {
+                                                                       // Result of LanguageConverter::markNoConversion
+                                                                       // invoked on an external link.
+                                                                       $linkValue = substr( $linkValue, 4, -2 );
+                                                               }
+                                                               if ( preg_match( "/^($prots)$addr$chars*$/u", $linkValue ) ) {
+                                                                       $link = $linkValue;
+                                                                       $this->mOutput->addExternalLink( $link );
+                                                               } else {
+                                                                       $localLinkTitle = Title::newFromText( $linkValue );
+                                                                       if ( $localLinkTitle !== null ) {
+                                                                               $this->mOutput->addLink( $localLinkTitle );
+                                                                               $link = $localLinkTitle->getLinkURL();
+                                                                       }
+                                                               }
+                                                               break;
+                                                       default:
+                                                               // Must be a handler specific parameter.
+                                                               if ( $handler->validateParam( $paramName, $match ) ) {
+                                                                       $handlerOptions[$paramName] = $match;
+                                                               } else {
+                                                                       // Guess not, consider it as caption.
+                                                                       wfDebug( "$parameterMatch failed parameter validation\n" );
+                                                                       $label = '|' . $parameterMatch;
                                                                }
-                                                       }
-                                                       break;
-                                               default:
-                                                       // Must be a handler specific parameter.
-                                                       if ( $handler->validateParam( $paramName, $match ) ) {
-                                                               $handlerOptions[$paramName] = $match;
-                                                       } else {
-                                                               // Guess not, consider it as caption.
-                                                               wfDebug( "$parameterMatch failed parameter validation\n" );
-                                                               $label = '|' . $parameterMatch;
-                                                       }
                                                }
 
                                        } else {
@@ -5214,52 +5217,52 @@ class Parser {
                                        } else {
                                                # Validate internal parameters
                                                switch ( $paramName ) {
-                                               case 'manualthumb':
-                                               case 'alt':
-                                               case 'class':
-                                                       # @todo FIXME: Possibly check validity here for
-                                                       # manualthumb? downstream behavior seems odd with
-                                                       # missing manual thumbs.
-                                                       $validated = true;
-                                                       $value = $this->stripAltText( $value, $holders );
-                                                       break;
-                                               case 'link':
-                                                       $chars = self::EXT_LINK_URL_CLASS;
-                                                       $addr = self::EXT_LINK_ADDR;
-                                                       $prots = $this->mUrlProtocols;
-                                                       if ( $value === '' ) {
-                                                               $paramName = 'no-link';
-                                                               $value = true;
+                                                       case 'manualthumb':
+                                                       case 'alt':
+                                                       case 'class':
+                                                               # @todo FIXME: Possibly check validity here for
+                                                               # manualthumb? downstream behavior seems odd with
+                                                               # missing manual thumbs.
                                                                $validated = true;
-                                                       } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
-                                                               if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
-                                                                       $paramName = 'link-url';
-                                                                       $this->mOutput->addExternalLink( $value );
-                                                                       if ( $this->mOptions->getExternalLinkTarget() ) {
-                                                                               $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
-                                                                       }
-                                                                       $validated = true;
-                                                               }
-                                                       } else {
-                                                               $linkTitle = Title::newFromText( $value );
-                                                               if ( $linkTitle ) {
-                                                                       $paramName = 'link-title';
-                                                                       $value = $linkTitle;
-                                                                       $this->mOutput->addLink( $linkTitle );
+                                                               $value = $this->stripAltText( $value, $holders );
+                                                               break;
+                                                       case 'link':
+                                                               $chars = self::EXT_LINK_URL_CLASS;
+                                                               $addr = self::EXT_LINK_ADDR;
+                                                               $prots = $this->mUrlProtocols;
+                                                               if ( $value === '' ) {
+                                                                       $paramName = 'no-link';
+                                                                       $value = true;
                                                                        $validated = true;
+                                                               } elseif ( preg_match( "/^((?i)$prots)/", $value ) ) {
+                                                                       if ( preg_match( "/^((?i)$prots)$addr$chars*$/u", $value, $m ) ) {
+                                                                               $paramName = 'link-url';
+                                                                               $this->mOutput->addExternalLink( $value );
+                                                                               if ( $this->mOptions->getExternalLinkTarget() ) {
+                                                                                       $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
+                                                                               }
+                                                                               $validated = true;
+                                                                       }
+                                                               } else {
+                                                                       $linkTitle = Title::newFromText( $value );
+                                                                       if ( $linkTitle ) {
+                                                                               $paramName = 'link-title';
+                                                                               $value = $linkTitle;
+                                                                               $this->mOutput->addLink( $linkTitle );
+                                                                               $validated = true;
+                                                                       }
                                                                }
-                                                       }
-                                                       break;
-                                               case 'frameless':
-                                               case 'framed':
-                                               case 'thumbnail':
-                                                       // use first appearing option, discard others.
-                                                       $validated = !$seenformat;
-                                                       $seenformat = true;
-                                                       break;
-                                               default:
-                                                       # Most other things appear to be empty or numeric...
-                                                       $validated = ( $value === false || is_numeric( trim( $value ) ) );
+                                                               break;
+                                                       case 'frameless':
+                                                       case 'framed':
+                                                       case 'thumbnail':
+                                                               // use first appearing option, discard others.
+                                                               $validated = !$seenformat;
+                                                               $seenformat = true;
+                                                               break;
+                                                       default:
+                                                               # Most other things appear to be empty or numeric...
+                                                               $validated = ( $value === false || is_numeric( trim( $value ) ) );
                                                }
                                        }
 
@@ -5753,21 +5756,42 @@ class Parser {
                return $this->mDefaultSort;
        }
 
+       private static function getSectionNameFromStrippedText( $text ) {
+               $text = Sanitizer::normalizeSectionNameWhitespace( $text );
+               $text = Sanitizer::decodeCharReferences( $text );
+               $text = self::normalizeSectionName( $text );
+               return $text;
+       }
+
+       private static function makeAnchor( $sectionName ) {
+               return '#' . Sanitizer::escapeIdForLink( $sectionName );
+       }
+
+       private static function makeLegacyAnchor( $sectionName ) {
+               global $wgFragmentMode;
+               if ( isset( $wgFragmentMode[1] ) && $wgFragmentMode[1] === 'legacy' ) {
+                       // ForAttribute() and ForLink() are the same for legacy encoding
+                       $id = Sanitizer::escapeIdForAttribute( $text, Sanitizer::ID_FALLBACK );
+               } else {
+                       $id = Sanitizer::escapeIdForLink( $text );
+               }
+
+               return "#$id";
+       }
+
        /**
         * Try to guess the section anchor name based on a wikitext fragment
         * presumably extracted from a heading, for example "Header" from
         * "== Header ==".
         *
         * @param string $text
-        *
-        * @return string
+        * @return string Anchor (starting with '#')
         */
        public function guessSectionNameFromWikiText( $text ) {
                # Strip out wikitext links(they break the anchor)
                $text = $this->stripSectionName( $text );
-               $text = Sanitizer::normalizeSectionNameWhitespace( $text );
-               $text = Sanitizer::decodeCharReferences( $text );
-               return '#' . Sanitizer::escapeIdForLink( $text );
+               $sectionName = self::getSectionNameFromStrippedText( $text );
+               return self::makeAnchor( $sectionName );
        }
 
        /**
@@ -5777,24 +5801,41 @@ class Parser {
         * than UTF-8, resulting in breakage.
         *
         * @param string $text The section name
-        * @return string An anchor
+        * @return string Anchor (starting with '#')
         */
        public function guessLegacySectionNameFromWikiText( $text ) {
-               global $wgFragmentMode;
-
                # Strip out wikitext links(they break the anchor)
                $text = $this->stripSectionName( $text );
-               $text = Sanitizer::normalizeSectionNameWhitespace( $text );
-               $text = Sanitizer::decodeCharReferences( $text );
+               $sectionName = self::getSectionNameFromStrippedText( $text );
+               return self::makeLegacyAnchor( $sectionName );
+       }
 
-               if ( isset( $wgFragmentMode[1] ) && $wgFragmentMode[1] === 'legacy' ) {
-                       // ForAttribute() and ForLink() are the same for legacy encoding
-                       $id = Sanitizer::escapeIdForAttribute( $text, Sanitizer::ID_FALLBACK );
-               } else {
-                       $id = Sanitizer::escapeIdForLink( $text );
-               }
+       /**
+        * Like guessSectionNameFromWikiText(), but takes already-stripped text as input.
+        * @param string $text Section name (plain text)
+        * @return string Anchor (starting with '#')
+        */
+       public static function guessSectionNameFromStrippedText( $text ) {
+               $sectionName = self::getSectionNameFromStrippedText( $text );
+               return self::makeAnchor( $sectionName );
+       }
 
-               return "#$id";
+       /**
+        * Apply the same normalization as code making links to this section would
+        *
+        * @param string $text
+        * @return string
+        */
+       private static function normalizeSectionName( $text ) {
+               # T90902: ensure the same normalization is applied for IDs as to links
+               $titleParser = MediaWikiServices::getInstance()->getTitleParser();
+               try {
+
+                       $parts = $titleParser->splitTitleString( "#$text" );
+               } catch ( MalformedTitleException $ex ) {
+                       return $text;
+               }
+               return $parts['fragment'];
        }
 
        /**