Merge "Make interwiki transclusion use the WAN cache"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Mon, 27 Aug 2018 21:17:04 +0000 (21:17 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 27 Aug 2018 21:17:04 +0000 (21:17 +0000)
1  2 
includes/page/WikiPage.php
includes/parser/Parser.php

@@@ -1625,7 -1625,7 +1625,7 @@@ class WikiPage implements Page, IDBAcce
         * @return DerivedPageDataUpdater
         */
        private function newDerivedDataUpdater() {
 -              global $wgContLang, $wgRCWatchCategoryMembership, $wgArticleCountMethod;
 +              global $wgRCWatchCategoryMembership, $wgArticleCountMethod;
  
                $derivedDataUpdater = new DerivedPageDataUpdater(
                        $this, // NOTE: eventually, PageUpdater should not know about WikiPage
                        $this->getParserCache(),
                        JobQueueGroup::singleton(),
                        MessageCache::singleton(),
 -                      $wgContLang,
 +                      MediaWikiServices::getInstance()->getContentLanguage(),
                        LoggerFactory::getInstance( 'SaveParse' )
                );
  
         * Purges pages that include this page if the text was changed here.
         * Every 100th edit, prune the recent changes table.
         *
 -       * @deprecated since 1.32, use PageUpdater::doEditUpdates instead.
 +       * @deprecated since 1.32, use PageUpdater::doUpdates instead.
         *
         * @param Revision $revision
         * @param User $user User object that did the revision
         * @return string
         */
        protected function formatExpiry( $expiry ) {
 -              global $wgContLang;
 -
                if ( $expiry != 'infinity' ) {
 +                      $contLang = MediaWikiServices::getInstance()->getContentLanguage();
                        return wfMessage(
                                'protect-expiring',
 -                              $wgContLang->timeanddate( $expiry, false, false ),
 -                              $wgContLang->date( $expiry, false, false ),
 -                              $wgContLang->time( $expiry, false, false )
 +                              $contLang->timeanddate( $expiry, false, false ),
 +                              $contLang->date( $expiry, false, false ),
 +                              $contLang->time( $expiry, false, false )
                        )->inContentLanguage()->text();
                } else {
                        return wfMessage( 'protect-expiry-indefinite' )
         * @return string
         */
        public function protectDescriptionLog( array $limit, array $expiry ) {
 -              global $wgContLang;
 -
                $protectDescriptionLog = '';
  
 +              $dirMark = MediaWikiServices::getInstance()->getContentLanguage()->getDirMark();
                foreach ( array_filter( $limit ) as $action => $restrictions ) {
                        $expiryText = $this->formatExpiry( $expiry[$action] );
 -                      $protectDescriptionLog .= $wgContLang->getDirMark() .
 +                      $protectDescriptionLog .=
 +                              $dirMark .
                                "[$action=$restrictions] ($expiryText)";
                }
  
        public function commitRollback( $fromP, $summary, $bot,
                &$resultDetails, User $guser, $tags = null
        ) {
 -              global $wgUseRCPatrol, $wgContLang;
 +              global $wgUseRCPatrol;
  
                $dbw = wfGetDB( DB_MASTER );
  
                $targetEditorForPublic = $target->getUser( RevisionRecord::FOR_PUBLIC );
  
                // Allow the custom summary to use the same args as the default message
 +              $contLang = MediaWikiServices::getInstance()->getContentLanguage();
                $args = [
                        $targetEditorForPublic ? $targetEditorForPublic->getName() : null,
                        $currentEditorForPublic ? $currentEditorForPublic->getName() : null,
                        $s->rev_id,
 -                      $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
 +                      $contLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
                        $current->getId(),
 -                      $wgContLang->timeanddate( $current->getTimestamp() )
 +                      $contLang->timeanddate( $current->getTimestamp() )
                ];
                if ( $summary instanceof Message ) {
                        $summary = $summary->params( $args )->inContentLanguage()->text();
  
                // Image redirects
                RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
+               // Purge cross-wiki cache entities referencing this page
+               self::purgeInterwikiCheckKey( $title );
        }
  
        /**
                // Clear file cache for this page only
                HTMLFileCache::clearFileCache( $title );
  
+               // Purge ?action=info cache
                $revid = $revision ? $revision->getId() : null;
                DeferredUpdates::addCallableUpdate( function () use ( $title, $revid ) {
                        InfoAction::invalidateCache( $title, $revid );
                } );
+               // Purge cross-wiki cache entities referencing this page
+               self::purgeInterwikiCheckKey( $title );
        }
  
        /**#@-*/
  
+       /**
+        * Purge the check key for cross-wiki cache entries referencing this page
+        *
+        * @param Title $title
+        */
+       private static function purgeInterwikiCheckKey( Title $title ) {
+               global $wgEnableScaryTranscluding;
+               if ( !$wgEnableScaryTranscluding ) {
+                       return; // @todo: perhaps this wiki is only used as a *source* for content?
+               }
+               DeferredUpdates::addCallableUpdate( function () use ( $title ) {
+                       $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+                       $cache->resetCheckKey(
+                               // Do not include the namespace since there can be multiple aliases to it
+                               // due to different namespace text definitions on different wikis. This only
+                               // means that some cache invalidations happen that are not strictly needed.
+                               $cache->makeGlobalKey( 'interwiki-page', wfWikiID(), $title->getDBkey() )
+                       );
+               } );
+       }
        /**
         * Returns a list of categories this page is a member of.
         * Results will include hidden categories
        public function getMutableCacheKeys( WANObjectCache $cache ) {
                $linkCache = MediaWikiServices::getInstance()->getLinkCache();
  
 -              return $linkCache->getMutableCacheKeys( $cache, $this->getTitle()->getTitleValue() );
 +              return $linkCache->getMutableCacheKeys( $cache, $this->getTitle() );
        }
  
  }
@@@ -22,7 -22,6 +22,7 @@@
   */
  use MediaWiki\Linker\LinkRenderer;
  use MediaWiki\MediaWikiServices;
 +use MediaWiki\Special\SpecialPageFactory;
  use Wikimedia\ScopedCallback;
  
  /**
@@@ -52,6 -51,9 +52,6 @@@
   * - Parser::getPreloadText()
   *     removes <noinclude> sections and <includeonly> tags
   *
 - * Globals used:
 - *    object: $wgContLang
 - *
   * @warning $wgUser or $wgTitle or $wgRequest or $wgLang. Keep them away!
   *
   * @par Settings:
@@@ -151,9 -153,6 +151,9 @@@ class Parser 
        public $mImageParams = [];
        public $mImageParamsMagicArray = [];
        public $mMarkerIndex = 0;
 +      /**
 +       * @var bool Whether firstCallInit still needs to be called
 +       */
        public $mFirstCall = true;
  
        # Initialised by initialiseVariables()
         */
        protected $mLinkRenderer;
  
 +      /** @var MagicWordFactory */
 +      private $magicWordFactory;
 +
 +      /** @var Language */
 +      private $contLang;
 +
 +      /** @var ParserFactory */
 +      private $factory;
 +
 +      /** @var SpecialPageFactory */
 +      private $specialPageFactory;
 +
        /**
 -       * @param array $conf
 +       * @param array $conf See $wgParserConf documentation
 +       * @param MagicWordFactory|null $magicWordFactory
 +       * @param Language|null $contLang Content language
 +       * @param ParserFactory|null $factory
 +       * @param string|null $urlProtocols As returned from wfUrlProtocols()
 +       * @param SpecialPageFactory|null $spFactory
         */
 -      public function __construct( $conf = [] ) {
 +      public function __construct(
 +              array $conf = [], MagicWordFactory $magicWordFactory = null, Language $contLang = null,
 +              ParserFactory $factory = null, $urlProtocols = null, SpecialPageFactory $spFactory = null
 +      ) {
                $this->mConf = $conf;
 -              $this->mUrlProtocols = wfUrlProtocols();
 +              $this->mUrlProtocols = $urlProtocols ?? wfUrlProtocols();
                $this->mExtLinkBracketedRegex = '/\[(((?i)' . $this->mUrlProtocols . ')' .
                        self::EXT_LINK_ADDR .
                        self::EXT_LINK_URL_CLASS . '*)\p{Zs}*([^\]\\x00-\\x08\\x0a-\\x1F\\x{FFFD}]*?)\]/Su';
                        $this->mPreprocessorClass = Preprocessor_Hash::class;
                }
                wfDebug( __CLASS__ . ": using preprocessor: {$this->mPreprocessorClass}\n" );
 +
 +              $services = MediaWikiServices::getInstance();
 +              $this->magicWordFactory = $magicWordFactory ??
 +                      $services->getMagicWordFactory();
 +
 +              $this->contLang = $contLang ?? $services->getContentLanguage();
 +
 +              $this->factory = $factory ?? $services->getParserFactory();
 +              $this->specialPageFactory = $spFactory ?? $services->getSpecialPageFactory();
        }
  
        /**
         * @private
         */
        public function clearState() {
 -              if ( $this->mFirstCall ) {
 -                      $this->firstCallInit();
 -              }
 +              $this->firstCallInit();
                $this->mOutput = new ParserOutput;
                $this->mOptions->registerWatcher( [ $this->mOutput, 'recordOption' ] );
                $this->mAutonumber = 0;
                return $this->mLinkRenderer;
        }
  
 +      /**
 +       * Get the MagicWordFactory that this Parser is using
 +       *
 +       * @since 1.32
 +       * @return MagicWordFactory
 +       */
 +      public function getMagicWordFactory() {
 +              return $this->magicWordFactory;
 +      }
 +
 +      /**
 +       * Get the content language that this Parser is using
 +       *
 +       * @since 1.32
 +       * @return Language
 +       */
 +      public function getContentLanguage() {
 +              return $this->contLang;
 +      }
 +
        /**
         * Replaces all occurrences of HTML-style comments and the given tags
         * in the text with a random marker and returns the next text. The output
                if ( $useLinkPrefixExtension ) {
                        # Match the end of a line for a word that's not followed by whitespace,
                        # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
 -                      global $wgContLang;
 -                      $charset = $wgContLang->linkPrefixCharset();
 +                      $charset = $this->contLang->linkPrefixCharset();
                        $e2 = "/^((?>.*[^$charset]|))(.+)$/sDu";
                }
  
         *
         * @private
         *
 -       * @param string $index Magic variable identifier as mapped in MagicWord::$mVariableIDs
 +       * @param string $index Magic variable identifier as mapped in MagicWordFactory::$mVariableIDs
         * @param bool|PPFrame $frame
         *
         * @throws MWException
         * @return string
         */
        public function getVariableValue( $index, $frame = false ) {
 -              global $wgContLang, $wgSitename, $wgServer, $wgServerName;
 +              global $wgSitename, $wgServer, $wgServerName;
                global $wgArticlePath, $wgScriptPath, $wgStylePath;
  
                if ( is_null( $this->mTitle ) ) {
                                $value = $this->getRevisionSize();
                                break;
                        case 'namespace':
 -                              $value = str_replace( '_', ' ', $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
 +                              $value = str_replace( '_', ' ',
 +                                      $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
                                break;
                        case 'namespacee':
 -                              $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
 +                              $value = wfUrlencode( $this->contLang->getNsText( $this->mTitle->getNamespace() ) );
                                break;
                        case 'namespacenumber':
                                $value = $this->mTitle->getNamespace();
         * @return string
         */
        private function getRevisionTimestampSubstring( $start, $len, $mtts, $variable ) {
 -              global $wgContLang;
 -
                # Get the timezone-adjusted timestamp to be used for this revision
                $resNow = substr( $this->getRevisionTimestamp(), $start, $len );
                # Possibly set vary-revision if there is not yet an associated revision
                if ( !$this->getRevisionObject() ) {
                        # Get the timezone-adjusted timestamp $mtts seconds in the future
                        $resThen = substr(
 -                              $wgContLang->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
 +                              $this->contLang->userAdjust( wfTimestamp( TS_MW, time() + $mtts ), '' ),
                                $start,
                                $len
                        );
         * @private
         */
        public function initialiseVariables() {
 -              $variableIDs = MagicWord::getVariableIDs();
 -              $substIDs = MagicWord::getSubstIDs();
 +              $variableIDs = $this->magicWordFactory->getVariableIDs();
 +              $substIDs = $this->magicWordFactory->getSubstIDs();
  
 -              $this->mVariables = new MagicWordArray( $variableIDs );
 -              $this->mSubstWords = new MagicWordArray( $substIDs );
 +              $this->mVariables = $this->magicWordFactory->newArray( $variableIDs );
 +              $this->mSubstWords = $this->magicWordFactory->newArray( $substIDs );
        }
  
        /**
                        $id = $this->mVariables->matchStartToEnd( $part1 );
                        if ( $id !== false ) {
                                $text = $this->getVariableValue( $id, $frame );
 -                              if ( MagicWord::getCacheTTL( $id ) > -1 ) {
 -                                      $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
 +                              if ( $this->magicWordFactory->getCacheTTL( $id ) > -1 ) {
 +                                      $this->mOutput->updateCacheExpiry(
 +                                              $this->magicWordFactory->getCacheTTL( $id ) );
                                }
                                $found = true;
                        }
                # MSG, MSGNW and RAW
                if ( !$found ) {
                        # Check for MSGNW:
 -                      $mwMsgnw = MagicWord::get( 'msgnw' );
 +                      $mwMsgnw = $this->magicWordFactory->get( 'msgnw' );
                        if ( $mwMsgnw->matchStartAndRemove( $part1 ) ) {
                                $nowiki = true;
                        } else {
                                # Remove obsolete MSG:
 -                              $mwMsg = MagicWord::get( 'msg' );
 +                              $mwMsg = $this->magicWordFactory->get( 'msg' );
                                $mwMsg->matchStartAndRemove( $part1 );
                        }
  
                        # Check for RAW:
 -                      $mwRaw = MagicWord::get( 'raw' );
 +                      $mwRaw = $this->magicWordFactory->get( 'raw' );
                        if ( $mwRaw->matchStartAndRemove( $part1 ) ) {
                                $forceRawInterwiki = true;
                        }
                                        && $this->mOptions->getAllowSpecialInclusion()
                                        && $this->ot['html']
                                ) {
 -                                      $specialPage = SpecialPageFactory::getPage( $title->getDBkey() );
 +                                      $specialPage = $this->specialPageFactory->getPage( $title->getDBkey() );
                                        // Pass the template arguments as URL parameters.
                                        // "uselang" will have no effect since the Language object
                                        // is forced to the one defined in ParserOptions.
                                                $context->setUser( User::newFromName( '127.0.0.1', false ) );
                                        }
                                        $context->setLanguage( $this->mOptions->getUserLangObj() );
 -                                      $ret = SpecialPageFactory::capturePath(
 -                                              $title, $context, $this->getLinkRenderer() );
 +                                      $ret = $this->specialPageFactory->capturePath( $title, $context, $this->getLinkRenderer() );
                                        if ( $ret ) {
                                                $text = $context->getOutput()->getHTML();
                                                $this->mOutput->addOutputPageMetadata( $context->getOutput() );
         * @return array
         */
        public function callParserFunction( $frame, $function, array $args = [] ) {
 -              global $wgContLang;
 -
                # Case sensitive functions
                if ( isset( $this->mFunctionSynonyms[1][$function] ) ) {
                        $function = $this->mFunctionSynonyms[1][$function];
                } else {
                        # Case insensitive functions
 -                      $function = $wgContLang->lc( $function );
 +                      $function = $this->contLang->lc( $function );
                        if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
                                $function = $this->mFunctionSynonyms[0][$function];
                        } else {
                                        break;
                                }
                        } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
 -                              global $wgContLang;
 -                              $message = wfMessage( $wgContLang->lcfirst( $title->getText() ) )->inContentLanguage();
 +                              $message = wfMessage( MediaWikiServices::getInstance()->getContentLanguage()->
 +                                      lcfirst( $title->getText() ) )->inContentLanguage();
                                if ( !$message->exists() ) {
                                        $text = false;
                                        break;
         * Transclude an interwiki link.
         *
         * @param Title $title
-        * @param string $action
+        * @param string $action Usually one of (raw, render)
         *
         * @return string
         */
        public function interwikiTransclude( $title, $action ) {
-               global $wgEnableScaryTranscluding;
+               global $wgEnableScaryTranscluding, $wgTranscludeCacheExpiry;
  
                if ( !$wgEnableScaryTranscluding ) {
                        return wfMessage( 'scarytranscludedisabled' )->inContentLanguage()->text();
                }
  
                $url = $title->getFullURL( [ 'action' => $action ] );
-               if ( strlen( $url ) > 255 ) {
+               if ( strlen( $url ) > 1024 ) {
                        return wfMessage( 'scarytranscludetoolong' )->inContentLanguage()->text();
                }
-               return $this->fetchScaryTemplateMaybeFromCache( $url );
-       }
  
-       /**
-        * @param string $url
-        * @return mixed|string
-        */
-       public function fetchScaryTemplateMaybeFromCache( $url ) {
-               global $wgTranscludeCacheExpiry;
-               $dbr = wfGetDB( DB_REPLICA );
-               $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
-               $obj = $dbr->selectRow( 'transcache', [ 'tc_time', 'tc_contents' ],
-                               [ 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ] );
-               if ( $obj ) {
-                       return $obj->tc_contents;
-               }
-               $req = MWHttpRequest::factory( $url, [], __METHOD__ );
-               $status = $req->execute(); // Status object
-               if ( $status->isOK() ) {
-                       $text = $req->getContent();
-               } elseif ( $req->getStatus() != 200 ) {
+               $wikiId = $title->getTransWikiID(); // remote wiki ID or false
+               $fname = __METHOD__;
+               $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
+               $data = $cache->getWithSetCallback(
+                       $cache->makeGlobalKey(
+                               'interwiki-transclude',
+                               ( $wikiId !== false ) ? $wikiId : 'external',
+                               sha1( $url )
+                       ),
+                       $wgTranscludeCacheExpiry,
+                       function ( $oldValue, &$ttl ) use ( $url, $fname, $cache ) {
+                               $req = MWHttpRequest::factory( $url, [], $fname );
+                               $status = $req->execute(); // Status object
+                               if ( !$status->isOK() ) {
+                                       $ttl = $cache::TTL_UNCACHEABLE;
+                               } elseif ( $req->getResponseHeader( 'X-Database-Lagged' ) !== null ) {
+                                       $ttl = min( $cache::TTL_LAGGED, $ttl );
+                               }
+                               return [
+                                       'text' => $status->isOK() ? $req->getContent() : null,
+                                       'code' => $req->getStatus()
+                               ];
+                       },
+                       [
+                               'checkKeys' => ( $wikiId !== false )
+                                       ? [ $cache->makeGlobalKey( 'interwiki-page', $wikiId, $title->getDBkey() ) ]
+                                       : [],
+                               'pcGroup' => 'interwiki-transclude:5',
+                               'pcTTL' => $cache::TTL_PROC_LONG
+                       ]
+               );
+               if ( is_string( $data['text'] ) ) {
+                       $text = $data['text'];
+               } elseif ( $data['code'] != 200 ) {
                        // Though we failed to fetch the content, this status is useless.
-                       return wfMessage( 'scarytranscludefailed-httpstatus' )
-                               ->params( $url, $req->getStatus() /* HTTP status */ )->inContentLanguage()->text();
+                       $text = wfMessage( 'scarytranscludefailed-httpstatus' )
+                               ->params( $url, $data['code'] )->inContentLanguage()->text();
                } else {
-                       return wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
+                       $text = wfMessage( 'scarytranscludefailed', $url )->inContentLanguage()->text();
                }
  
-               $dbw = wfGetDB( DB_MASTER );
-               $dbw->replace( 'transcache', [ 'tc_url' ], [
-                       'tc_url' => $url,
-                       'tc_time' => $dbw->timestamp( time() ),
-                       'tc_contents' => $text
-               ] );
                return $text;
        }
  
         */
        public function doDoubleUnderscore( $text ) {
                # The position of __TOC__ needs to be recorded
 -              $mw = MagicWord::get( 'toc' );
 +              $mw = $this->magicWordFactory->get( 'toc' );
                if ( $mw->match( $text ) ) {
                        $this->mShowToc = true;
                        $this->mForceTocPosition = true;
                }
  
                # Now match and remove the rest of them
 -              $mwa = MagicWord::getDoubleUnderscoreArray();
 +              $mwa = $this->magicWordFactory->getDoubleUnderscoreArray();
                $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
  
                if ( isset( $this->mDoubleUnderscores['nogallery'] ) ) {
         * @return string
         */
        private function pstPass2( $text, $user ) {
 -              global $wgContLang;
 -
 -              # Note: This is the timestamp saved as hardcoded wikitext to
 -              # the database, we use $wgContLang here in order to give
 -              # everyone the same signature and use the default one rather
 -              # than the one selected in each user's preferences.
 -              # (see also T14815)
 +              # Note: This is the timestamp saved as hardcoded wikitext to the database, we use
 +              # $this->contLang here in order to give everyone the same signature and use the default one
 +              # rather than the one selected in each user's preferences.  (see also T14815)
                $ts = $this->mOptions->getTimestamp();
                $timestamp = MWTimestamp::getLocalInstance( $ts );
                $ts = $timestamp->format( 'YmdHis' );
                $tzMsg = $timestamp->getTimezoneMessage()->inContentLanguage()->text();
  
 -              $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
 +              $d = $this->contLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
  
                # Variable replacement
                # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
  
                # @todo FIXME: Regex doesn't respect extension tags or nowiki
                #  => Move this logic to braceSubstitution()
 -              $substWord = MagicWord::get( 'subst' );
 +              $substWord = $this->magicWordFactory->get( 'subst' );
                $substRegex = '/\{\{(?!(?:' . $substWord->getBaseRegex() . '))/x' . $substWord->getRegexCase();
                $substText = '{{' . $substWord->getSynonym( 0 );
  
         * @return string|callable The old callback function for this name, if any
         */
        public function setFunctionHook( $id, callable $callback, $flags = 0 ) {
 -              global $wgContLang;
 -
                $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
                $this->mFunctionHooks[$id] = [ $callback, $flags ];
  
                # Add to function cache
 -              $mw = MagicWord::get( $id );
 +              $mw = $this->magicWordFactory->get( $id );
                if ( !$mw ) {
                        throw new MWException( __METHOD__ . '() expecting a magic word identifier.' );
                }
                foreach ( $synonyms as $syn ) {
                        # Case
                        if ( !$sensitive ) {
 -                              $syn = $wgContLang->lc( $syn );
 +                              $syn = $this->contLang->lc( $syn );
                        }
                        # Add leading hash
                        if ( !( $flags & self::SFH_NO_HASH ) ) {
         * @return array
         */
        public function getFunctionHooks() {
 +              $this->firstCallInit();
                return array_keys( $this->mFunctionHooks );
        }
  
                                unset( $paramMap['img_width'] );
                        }
  
 -                      $mwArray = new MagicWordArray( array_keys( $paramMap ) );
 +                      $mwArray = $this->magicWordFactory->newArray( array_keys( $paramMap ) );
  
                        $label = '';
                        $alt = '';
                                }
                        }
                        $this->mImageParams[$handlerClass] = $paramMap;
 -                      $this->mImageParamsMagicArray[$handlerClass] = new MagicWordArray( array_keys( $paramMap ) );
 +                      $this->mImageParamsMagicArray[$handlerClass] =
 +                              $this->magicWordFactory->newArray( array_keys( $paramMap ) );
                }
                return [ $this->mImageParams[$handlerClass], $this->mImageParamsMagicArray[$handlerClass] ];
        }
         * @return array
         */
        public function getTags() {
 +              $this->firstCallInit();
                return array_merge(
                        array_keys( $this->mTransparentTagHooks ),
                        array_keys( $this->mTagHooks ),
                );
        }
  
 +      /**
 +       * @since 1.32
 +       * @return array
 +       */
 +      public function getFunctionSynonyms() {
 +              $this->firstCallInit();
 +              return $this->mFunctionSynonyms;
 +      }
 +
 +      /**
 +       * @since 1.32
 +       * @return string
 +       */
 +      public function getUrlProtocols() {
 +              return $this->mUrlProtocols;
 +      }
 +
        /**
         * Replace transparent tags in $text with the values given by the callbacks.
         *
         */
        public function getRevisionTimestamp() {
                if ( is_null( $this->mRevisionTimestamp ) ) {
 -                      global $wgContLang;
 -
                        $revObject = $this->getRevisionObject();
                        $timestamp = $revObject ? $revObject->getTimestamp() : wfTimestampNow();
  
                        # Since this value will be saved into the parser cache, served
                        # to other users, and potentially even used inside links and such,
                        # it needs to be consistent for all visitors.
 -                      $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
 -
 +                      $this->mRevisionTimestamp = $this->contLang->userAdjust( $timestamp, '' );
                }
                return $this->mRevisionTimestamp;
        }
         * @return Parser A parser object that is not parsing anything
         */
        public function getFreshParser() {
 -              global $wgParserConf;
                if ( $this->mInParse ) {
 -                      return new $wgParserConf['class']( $wgParserConf );
 +                      return $this->factory->create();
                } else {
                        return $this;
                }