Merge "Add block type filter to Special:BlockList"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 18 Apr 2019 16:20:31 +0000 (16:20 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 18 Apr 2019 16:20:31 +0000 (16:20 +0000)
60 files changed:
.fresnel.yml
.phpcs.xml
autoload.php
includes/DefaultSettings.php
includes/EditPage.php
includes/GlobalFunctions.php
includes/Linker.php
includes/MediaWikiServices.php
includes/OutputPage.php
includes/ServiceWiring.php
includes/Storage/DerivedPageDataUpdater.php
includes/Storage/PageEditStash.php [new file with mode: 0644]
includes/actions/RawAction.php
includes/api/ApiExpandTemplates.php
includes/api/ApiFeedWatchlist.php
includes/api/ApiFormatFeedWrapper.php
includes/api/ApiParse.php
includes/api/ApiQueryRevisionsBase.php
includes/api/ApiQuerySiteinfo.php
includes/api/ApiStashEdit.php
includes/cache/MessageCache.php
includes/content/CssContent.php
includes/content/JavaScriptContent.php
includes/content/TextContent.php
includes/content/WikitextContent.php
includes/installer/i18n/fr.json
includes/libs/rdbms/lbfactory/LBFactory.php
includes/parser/Parser.php
includes/resourceloader/ResourceLoader.php
includes/resourceloader/ResourceLoaderWikiModule.php
includes/session/SessionProvider.php
includes/specials/SpecialExpandTemplates.php
includes/specials/SpecialVersion.php
languages/Language.php
languages/i18n/arz.json
languages/i18n/ast.json
languages/i18n/be-tarask.json
languages/i18n/exif/sah.json
languages/i18n/fa.json
languages/i18n/fi.json
languages/i18n/fr.json
languages/i18n/fy.json
languages/i18n/hr.json
languages/i18n/lb.json
languages/i18n/th.json
languages/i18n/zh-hans.json
languages/i18n/zh-hant.json
maintenance/language/generateUcfirstOverrides.php [new file with mode: 0644]
maintenance/language/generateUpperCharTable.php [new file with mode: 0644]
maintenance/preprocessDump.php
maintenance/preprocessorFuzzTest.php
maintenance/runJobs.php
tests/parser/ParserTestRunner.php
tests/phpunit/includes/MessageTest.php
tests/phpunit/includes/api/ApiQuerySiteinfoTest.php
tests/phpunit/includes/api/ApiStashEditTest.php
tests/phpunit/includes/parser/ParserMethodsTest.php
tests/phpunit/includes/resourceloader/ResourceLoaderTest.php
tests/phpunit/languages/LanguageTest.php
tests/phpunit/suites/UploadFromUrlTestSuite.php

index e85de79..334f971 100644 (file)
@@ -1,5 +1,5 @@
 warmup: true
-runs: 5
+runs: 7
 scenarios:
   Read a page:
     # The only page that exists by default is the main page.
index cc9e53c..fef07e6 100644 (file)
@@ -14,7 +14,6 @@
                <exclude name="MediaWiki.NamingConventions.LowerCamelFunctionsName.FunctionName" />
                <exclude name="MediaWiki.Usage.DbrQueryUsage.DbrQueryFound" />
                <exclude name="MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgContLang" />
-               <exclude name="MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgParser" />
                <exclude name="MediaWiki.Usage.DeprecatedGlobalVariables.Deprecated$wgTitle" />
                <exclude name="MediaWiki.Usage.ForbiddenFunctions.passthru" />
                <exclude name="MediaWiki.WhiteSpace.SpaceBeforeSingleLineComment.NewLineComment" />
index ab36d84..4f41c8a 100644 (file)
@@ -564,6 +564,8 @@ $wgAutoloadLocalClasses = [
        'GenerateNormalizerDataAr' => __DIR__ . '/maintenance/language/generateNormalizerDataAr.php',
        'GenerateNormalizerDataMl' => __DIR__ . '/maintenance/language/generateNormalizerDataMl.php',
        'GenerateSitemap' => __DIR__ . '/maintenance/generateSitemap.php',
+       'GenerateUcfirstOverrides' => __DIR__ . '/maintenance/language/generateUcfirstOverrides.php',
+       'GenerateUpperCharTable' => __DIR__ . '/maintenance/language/generateUpperCharTable.php',
        'GenericArrayObject' => __DIR__ . '/includes/libs/GenericArrayObject.php',
        'GenericParameterJob' => __DIR__ . '/includes/jobqueue/GenericParameterJob.php',
        'GetConfiguration' => __DIR__ . '/maintenance/getConfiguration.php',
index 4547009..af830fd 100644 (file)
@@ -3194,6 +3194,19 @@ $wgLocaltimezone = null;
  */
 $wgLocalTZoffset = null;
 
+/**
+ * List of Unicode characters for which capitalization is overridden in
+ * Language::ucfirst. The characters should be
+ * represented as char_to_convert => conversion_override. See T219279 for details
+ * on why this is useful during php version transitions.
+ *
+ * @warning: EXPERIMENTAL!
+ *
+ * @since 1.34
+ * @var array
+ */
+$wgOverrideUcfirstCharacters = [];
+
 /** @} */ # End of language/charset settings
 
 /*************************************************************************//**
@@ -4111,7 +4124,7 @@ $wgInvalidRedirectTargets = [ 'Filepath', 'Mypage', 'Mytalk', 'Redirect' ];
  *
  * The entire associative array will be passed through to the constructor as
  * the first parameter. Note that only Setup.php can use this variable --
- * the configuration will change at runtime via $wgParser member functions, so
+ * the configuration will change at runtime via Parser member functions, so
  * the contents of this variable will be out-of-date. The variable can only be
  * changed during LocalSettings.php, in particular, it can't be changed during
  * an extension setup function.
index add48d9..6990934 100644 (file)
@@ -1816,15 +1816,14 @@ ERROR;
         * @return string
         */
        private function newSectionSummary( &$sectionanchor = null ) {
-               global $wgParser;
-
                if ( $this->sectiontitle !== '' ) {
                        $sectionanchor = $this->guessSectionName( $this->sectiontitle );
                        // If no edit summary was specified, create one automatically from the section
                        // title and have it link to the new section. Otherwise, respect the summary as
                        // passed.
                        if ( $this->summary === '' ) {
-                               $cleanSectionTitle = $wgParser->stripSectionName( $this->sectiontitle );
+                               $cleanSectionTitle = MediaWikiServices::getInstance()->getParser()
+                                       ->stripSectionName( $this->sectiontitle );
                                return $this->context->msg( 'newsectionsummary' )
                                        ->plaintextParams( $cleanSectionTitle )->inContentLanguage()->text();
                        }
@@ -1832,7 +1831,8 @@ ERROR;
                        $sectionanchor = $this->guessSectionName( $this->summary );
                        # This is a new section, so create a link to the new section
                        # in the revision summary.
-                       $cleanSummary = $wgParser->stripSectionName( $this->summary );
+                       $cleanSummary = MediaWikiServices::getInstance()->getParser()
+                               ->stripSectionName( $this->summary );
                        return $this->context->msg( 'newsectionsummary' )
                                ->plaintextParams( $cleanSummary )->inContentLanguage()->text();
                }
@@ -3058,8 +3058,8 @@ ERROR;
        public static function extractSectionTitle( $text ) {
                preg_match( "/^(=+)(.+)\\1\\s*(\n|$)/i", $text, $matches );
                if ( !empty( $matches[2] ) ) {
-                       global $wgParser;
-                       return $wgParser->stripSectionName( trim( $matches[2] ) );
+                       return MediaWikiServices::getInstance()->getParser()
+                               ->stripSectionName( trim( $matches[2] ) );
                } else {
                        return false;
                }
@@ -3329,11 +3329,10 @@ ERROR;
                        return "";
                }
 
-               global $wgParser;
-
                if ( $isSubjectPreview ) {
                        $summary = $this->context->msg( 'newsectionsummary' )
-                               ->rawParams( $wgParser->stripSectionName( $summary ) )
+                               ->rawParams( MediaWikiServices::getInstance()->getParser()
+                                       ->stripSectionName( $summary ) )
                                ->inContentLanguage()->text();
                }
 
@@ -4538,16 +4537,15 @@ ERROR;
         * @return string
         */
        private function guessSectionName( $text ) {
-               global $wgParser;
-
                // Detect Microsoft browsers
                $userAgent = $this->context->getRequest()->getHeader( 'User-Agent' );
+               $parser = MediaWikiServices::getInstance()->getParser();
                if ( $userAgent && preg_match( '/MSIE|Edge/', $userAgent ) ) {
                        // ...and redirect them to legacy encoding, if available
-                       return $wgParser->guessLegacySectionNameFromWikiText( $text );
+                       return $parser->guessLegacySectionNameFromWikiText( $text );
                }
                // Meanwhile, real browsers get real anchors
-               $name = $wgParser->guessSectionNameFromWikiText( $text );
+               $name = $parser->guessSectionNameFromWikiText( $text );
                // With one little caveat: per T216029, fragments in HTTP redirects need to be urlencoded,
                // otherwise Chrome double-escapes the rest of the URL.
                return '#' . urlencode( mb_substr( $name, 1 ) );
index b6a1470..1ac2f83 100644 (file)
@@ -24,10 +24,11 @@ if ( !defined( 'MEDIAWIKI' ) ) {
        die( "This file is part of MediaWiki, it is not a valid entry point" );
 }
 
+use MediaWiki\Linker\LinkTarget;
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
 use MediaWiki\ProcOpenError;
 use MediaWiki\Session\SessionManager;
-use MediaWiki\MediaWikiServices;
 use MediaWiki\Shell\Shell;
 use Wikimedia\ScopedCallback;
 use Wikimedia\WrappedString;
@@ -2670,7 +2671,7 @@ function wfGetLBFactory() {
  * Find a file.
  * Shortcut for RepoGroup::singleton()->findFile()
  *
- * @param string|Title $title String or Title object
+ * @param string|LinkTarget $title String or LinkTarget object
  * @param array $options Associative array of options (see RepoGroup::findFile)
  * @return File|bool File, or false if the file does not exist
  */
index 4f0ab6a..d2936a9 100644 (file)
@@ -41,12 +41,12 @@ class Linker {
        /**
         * This function returns an HTML link to the given target.  It serves a few
         * purposes:
-        *   1) If $target is a Title, the correct URL to link to will be figured
+        *   1) If $target is a LinkTarget, the correct URL to link to will be figured
         *      out automatically.
         *   2) It automatically adds the usual classes for various types of link
         *      targets: "new" for red links, "stub" for short articles, etc.
         *   3) It escapes all attribute values safely so there's no risk of XSS.
-        *   4) It provides a default tooltip if the target is a Title (the page
+        *   4) It provides a default tooltip if the target is a LinkTarget (the page
         *      name of the target).
         * link() replaces the old functions in the makeLink() family.
         *
@@ -57,7 +57,7 @@ class Linker {
         *   change to support Images, literal URLs, etc.
         * @param string $html The HTML contents of the <a> element, i.e.,
         *   the link text.  This is raw HTML and will not be escaped.  If null,
-        *   defaults to the prefixed text of the Title; or if the Title is just a
+        *   defaults to the prefixed text of the LinkTarget; or if the LinkTarget is just a
         *   fragment, the contents of the fragment.
         * @param array $customAttribs A key => value array of extra HTML attributes,
         *   such as title and class.  (href is ignored.)  Classes will be
@@ -136,7 +136,7 @@ class Linker {
         * @since 1.16.3
         * @deprecated since 1.28, use MediaWiki\Linker\LinkRenderer instead
         * @see Linker::link
-        * @param Title $target
+        * @param LinkTarget $target
         * @param string $html
         * @param array $customAttribs
         * @param array $query
@@ -157,7 +157,7 @@ class Linker {
         * make*LinkObj static functions, but $query is not used.
         *
         * @since 1.16.3
-        * @param Title $nt
+        * @param LinkTarget $nt
         * @param string $html [optional]
         * @param string $query [optional]
         * @param string $trail [optional]
@@ -166,6 +166,7 @@ class Linker {
         * @return string
         */
        public static function makeSelfLinkObj( $nt, $html = '', $query = '', $trail = '', $prefix = '' ) {
+               $nt = Title::newFromLinkTarget( $nt );
                $ret = "<a class=\"mw-selflink selflink\">{$prefix}{$html}</a>{$trail}";
                if ( !Hooks::run( 'SelfLinkBegin', [ $nt, &$html, &$trail, &$prefix, &$ret ] ) ) {
                        return $ret;
@@ -272,7 +273,7 @@ class Linker {
         * HTML that that syntax inserts in the page.
         *
         * @param Parser $parser
-        * @param Title $title Title object of the file (not the currently viewed page)
+        * @param LinkTarget $title LinkTarget object of the file (not the currently viewed page)
         * @param File $file File object, or false if it doesn't exist
         * @param array $frameParams Associative array of parameters external to the media handler.
         *     Boolean parameters are indicated by presence or absence, the value is arbitrary and
@@ -291,7 +292,7 @@ class Linker {
         *          class           HTML for image classes. Plain text.
         *          caption         HTML for image caption.
         *          link-url        URL to link to
-        *          link-title      Title object to link to
+        *          link-title      LinkTarget object to link to
         *          link-target     Value for the target attribute, only with link-url
         *          no-link         Boolean, suppress description link
         *          targetlang      (optional) Target language code, see Parser::getTargetLanguage()
@@ -304,10 +305,11 @@ class Linker {
         * @since 1.20
         * @return string HTML for an image, with links, wrappers, etc.
         */
-       public static function makeImageLink( Parser $parser, Title $title,
+       public static function makeImageLink( Parser $parser, LinkTarget $title,
                $file, $frameParams = [], $handlerParams = [], $time = false,
                $query = "", $widthOption = null
        ) {
+               $title = Title::newFromLinkTarget( $title );
                $res = null;
                $dummy = new DummyLinker;
                if ( !Hooks::run( 'ImageBeforeProduceHTML', [ &$dummy, &$title,
@@ -483,7 +485,7 @@ class Linker {
 
        /**
         * Make HTML for a thumbnail including image, border and caption
-        * @param Title $title
+        * @param LinkTarget $title
         * @param File|bool $file File object or false if it doesn't exist
         * @param string $label
         * @param string $alt
@@ -493,7 +495,7 @@ class Linker {
         * @param string $manualthumb
         * @return string
         */
-       public static function makeThumbLinkObj( Title $title, $file, $label = '', $alt = '',
+       public static function makeThumbLinkObj( LinkTarget $title, $file, $label = '', $alt = '',
                $align = 'right', $params = [], $framed = false, $manualthumb = ""
        ) {
                $frameParams = [
@@ -511,7 +513,7 @@ class Linker {
        }
 
        /**
-        * @param Title $title
+        * @param LinkTarget $title
         * @param File $file
         * @param array $frameParams
         * @param array $handlerParams
@@ -519,7 +521,7 @@ class Linker {
         * @param string $query
         * @return string
         */
-       public static function makeThumbLink2( Title $title, $file, $frameParams = [],
+       public static function makeThumbLink2( LinkTarget $title, $file, $frameParams = [],
                $handlerParams = [], $time = false, $query = ""
        ) {
                $exists = $file && $file->exists();
@@ -585,7 +587,7 @@ class Linker {
                # ThumbnailImage::toHtml() already adds page= onto the end of DjVu URLs
                # So we don't need to pass it here in $query. However, the URL for the
                # zoom icon still needs it, so we make a unique query for it. See T16771
-               $url = $title->getLocalURL( $query );
+               $url = Title::newFromLinkTarget( $title )->getLocalURL( $query );
                if ( $page ) {
                        $url = wfAppendQuery( $url, [ 'page' => $page ] );
                }
@@ -668,7 +670,7 @@ class Linker {
         * Make a "broken" link to an image
         *
         * @since 1.16.3
-        * @param Title $title
+        * @param LinkTarget $title
         * @param string $label Link label (plain text)
         * @param string $query Query string
         * @param string $unused1 Unused parameter kept for b/c
@@ -679,11 +681,13 @@ class Linker {
        public static function makeBrokenImageLinkObj( $title, $label = '',
                $query = '', $unused1 = '', $unused2 = '', $time = false
        ) {
-               if ( !$title instanceof Title ) {
-                       wfWarn( __METHOD__ . ': Requires $title to be a Title object.' );
+               if ( !$title instanceof LinkTarget ) {
+                       wfWarn( __METHOD__ . ': Requires $title to be a LinkTarget object.' );
                        return "<!-- ERROR -->" . htmlspecialchars( $label );
                }
 
+               $title = Title::castFromLinkTarget( $title );
+
                global $wgEnableUploads, $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
                if ( $label == '' ) {
                        $label = $title->getPrefixedText();
@@ -722,13 +726,13 @@ class Linker {
         * Get the URL to upload a certain file
         *
         * @since 1.16.3
-        * @param Title $destFile Title object of the file to upload
+        * @param LinkTarget $destFile LinkTarget object of the file to upload
         * @param string $query Urlencoded query string to prepend
         * @return string Urlencoded URL
         */
        protected static function getUploadUrl( $destFile, $query = '' ) {
                global $wgUploadMissingFileUrl, $wgUploadNavigationUrl;
-               $q = 'wpDestFile=' . $destFile->getPartialURL();
+               $q = 'wpDestFile=' . Title::castFromLinkTarget( $destFile )->getPartialURL();
                if ( $query != '' ) {
                        $q .= '&' . $query;
                }
@@ -750,7 +754,7 @@ class Linker {
         * Create a direct link to a given uploaded file.
         *
         * @since 1.16.3
-        * @param Title $title
+        * @param LinkTarget $title
         * @param string $html Pre-sanitized HTML
         * @param string $time MW timestamp of file creation time
         * @return string HTML
@@ -765,14 +769,14 @@ class Linker {
         * This will make a broken link if $file is false.
         *
         * @since 1.16.3
-        * @param Title $title
+        * @param LinkTarget $title
         * @param File|bool $file File object or false
         * @param string $html Pre-sanitized HTML
         * @return string HTML
         *
         * @todo Handle invalid or missing images better.
         */
-       public static function makeMediaLinkFile( Title $title, $file, $html = '' ) {
+       public static function makeMediaLinkFile( LinkTarget $title, $file, $html = '' ) {
                if ( $file && $file->exists() ) {
                        $url = $file->getUrl();
                        $class = 'internal';
@@ -794,7 +798,7 @@ class Linker {
                ];
 
                if ( !Hooks::run( 'LinkerMakeMediaLinkFile',
-                       [ $title, $file, &$html, &$attribs, &$ret ] ) ) {
+                       [ Title::castFromLinkTarget( $title ), $file, &$html, &$attribs, &$ret ] ) ) {
                        wfDebug( "Hook LinkerMakeMediaLinkFile changed the output of link "
                                . "with url {$url} and text {$html} to {$ret}\n", true );
                        return $ret;
@@ -835,7 +839,7 @@ class Linker {
         * @param-taint $linktype escapes_html
         * @param array $attribs Array of extra attributes to <a>
         * @param-taint $attribs escapes_html
-        * @param Title|null $title Title object used for title specific link attributes
+        * @param LinkTarget|null $title LinkTarget object used for title specific link attributes
         * @param-taint $title none
         * @return string
         */
@@ -902,7 +906,7 @@ class Linker {
                        }
                        $classes .= ' mw-anonuserlink'; // Separate link class for anons (T45179)
                } else {
-                       $page = Title::makeTitle( NS_USER, $userName );
+                       $page = new TitleValue( NS_USER, strtr( $userName, ' ', '_' ) );
                }
 
                // Wrap the output with <bdi> tags for directionality isolation
@@ -1011,7 +1015,7 @@ class Linker {
         * @return string HTML fragment with user talk link
         */
        public static function userTalkLink( $userId, $userText ) {
-               $userTalkPage = Title::makeTitle( NS_USER_TALK, $userText );
+               $userTalkPage = new TitleValue( NS_USER_TALK, strtr( $userText, ' ', '_' ) );
                $moreLinkAttribs['class'] = 'mw-usertoollinks-talk';
 
                return self::link( $userTalkPage,
@@ -1108,8 +1112,8 @@ class Linker {
         * @since 1.16.3. $wikiId added in 1.26
         *
         * @param string $comment
-        * @param Title|null $title Title object (to generate link to the section in autocomment)
-        *  or null
+        * @param LinkTarget|null $title LinkTarget object (to generate link to the section in
+        *  autocomment) or null
         * @param bool $local Whether section links should refer to local page
         * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
         *  For use with external changes.
@@ -1139,7 +1143,7 @@ class Linker {
         * Called by Linker::formatComment.
         *
         * @param string $comment Comment text
-        * @param Title|null $title An optional title object used to links to sections
+        * @param LinkTarget|null $title An optional LinkTarget object used to links to sections
         * @param bool $local Whether section links should refer to local page
         * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
         *  as used by WikiMap.
@@ -1175,7 +1179,8 @@ class Linker {
 
                                Hooks::run(
                                        'FormatAutocomments',
-                                       [ &$comment, $pre, $auto, $post, $title, $local, $wikiId ]
+                                       [ &$comment, $pre, $auto, $post, Title::castFromLinkTarget( $title ), $local,
+                                       $wikiId ]
                                );
 
                                if ( $comment === null ) {
@@ -1195,10 +1200,9 @@ class Linker {
 
                                                $section = substr( Parser::guessSectionNameFromStrippedText( $section ), 1 );
                                                if ( $local ) {
-                                                       $sectionTitle = Title::makeTitleSafe( NS_MAIN, '', $section );
+                                                       $sectionTitle = new TitleValue( NS_MAIN, '', $section );
                                                } else {
-                                                       $sectionTitle = Title::makeTitleSafe( $title->getNamespace(),
-                                                               $title->getDBkey(), $section );
+                                                       $sectionTitle = $title->createFragmentTarget( $section );
                                                }
                                                if ( $sectionTitle ) {
                                                        $auto = Linker::makeCommentLink(
@@ -1239,7 +1243,7 @@ class Linker {
         *      function is html, $comment must be sanitized for use as html. You probably want
         *      to pass $comment through Sanitizer::escapeHtmlAllowEntities() before calling
         *      this function.
-        * @param Title|null $title An optional title object used to links to sections
+        * @param LinkTarget|null $title An optional LinkTarget object used to links to sections
         * @param bool $local Whether section links should refer to local page
         * @param string|null $wikiId Id of the wiki to link to (if not the local wiki),
         *  as used by WikiMap.
@@ -1318,8 +1322,11 @@ class Linker {
                                                $linkText = $text;
                                                $linkTarget = Linker::normalizeSubpageLink( $title, $match[1], $linkText );
 
-                                               $target = Title::newFromText( $linkTarget );
-                                               if ( $target ) {
+                                               Title::newFromText( $linkTarget );
+                                               try {
+                                                       $target = MediaWikiServices::getInstance()->getTitleParser()->
+                                                               parseTitle( $linkTarget );
+
                                                        if ( $target->getText() == '' && !$target->isExternal()
                                                                && !$local && $title
                                                        ) {
@@ -1327,6 +1334,8 @@ class Linker {
                                                        }
 
                                                        $thelink = Linker::makeCommentLink( $target, $linkText . $inside, $wikiId ) . $trail;
+                                               } catch ( MalformedTitleException $e ) {
+                                                       // Fall through
                                                }
                                        }
                                }
@@ -1347,7 +1356,7 @@ class Linker {
        }
 
        /**
-        * Generates a link to the given Title
+        * Generates a link to the given LinkTarget
         *
         * @note This is only public for technical reasons. It's not intended for use outside Linker.
         *
@@ -1383,7 +1392,7 @@ class Linker {
        }
 
        /**
-        * @param Title $contextTitle
+        * @param LinkTarget $contextTitle
         * @param string $target
         * @param string &$text
         * @return string
@@ -1414,6 +1423,8 @@ class Linker {
                        }
                        # T9425
                        $target = trim( $target );
+                       $contextPrefixedText = MediaWikiServices::getInstance()->getTitleFormatter()->
+                               getPrefixedText( $contextTitle );
                        # Look at the first character
                        if ( $target != '' && $target[0] === '/' ) {
                                # / at end means we don't want the slash to be shown
@@ -1425,7 +1436,7 @@ class Linker {
                                        $noslash = substr( $target, 1 );
                                }
 
-                               $ret = $contextTitle->getPrefixedText() . '/' . trim( $noslash ) . $suffix;
+                               $ret = $contextPrefixedText . '/' . trim( $noslash ) . $suffix;
                                if ( $text === '' ) {
                                        $text = $target . $suffix;
                                } # this might be changed for ugliness reasons
@@ -1438,7 +1449,7 @@ class Linker {
                                        $nodotdot = substr( $nodotdot, 3 );
                                }
                                if ( $dotdotcount > 0 ) {
-                                       $exploded = explode( '/', $contextTitle->getPrefixedText() );
+                                       $exploded = explode( '/', $contextPrefixedText );
                                        if ( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
                                                $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
                                                # / at the end means don't show full path
@@ -1467,7 +1478,8 @@ class Linker {
         *
         * @since 1.16.3. $wikiId added in 1.26
         * @param string $comment
-        * @param Title|null $title Title object (to generate link to section in autocomment) or null
+        * @param LinkTarget|null $title LinkTarget object (to generate link to section in autocomment)
+        *  or null
         * @param bool $local Whether section links should refer to local page
         * @param string|null $wikiId Id (as used by WikiMap) of the wiki to generate links to.
         *  For use with external changes.
@@ -2040,10 +2052,10 @@ class Linker {
         *
         * @param User $user
         * @param Revision $rev
-        * @param Title $title
+        * @param LinkTarget $title
         * @return string HTML fragment
         */
-       public static function getRevDeleteLink( User $user, Revision $rev, Title $title ) {
+       public static function getRevDeleteLink( User $user, Revision $rev, LinkTarget $title ) {
                $canHide = $user->isAllowed( 'deleterevision' );
                if ( !$canHide && !( $rev->getVisibility() && $user->isAllowed( 'deletedhistory' ) ) ) {
                        return '';
@@ -2052,12 +2064,14 @@ class Linker {
                if ( !$rev->userCan( Revision::DELETED_RESTRICTED, $user ) ) {
                        return self::revDeleteLinkDisabled( $canHide ); // revision was hidden from sysops
                }
+               $prefixedDbKey = MediaWikiServices::getInstance()->getTitleFormatter()->
+                       getPrefixedDBkey( $title );
                if ( $rev->getId() ) {
                        // RevDelete links using revision ID are stable across
                        // page deletion and undeletion; use when possible.
                        $query = [
                                'type' => 'revision',
-                               'target' => $title->getPrefixedDBkey(),
+                               'target' => $prefixedDbKey,
                                'ids' => $rev->getId()
                        ];
                } else {
@@ -2065,7 +2079,7 @@ class Linker {
                        // We have to refer to these by timestamp, ick!
                        $query = [
                                'type' => 'archive',
-                               'target' => $title->getPrefixedDBkey(),
+                               'target' => $prefixedDbKey,
                                'ids' => $rev->getTimestamp()
                        ];
                }
index 655946f..c13d33f 100644 (file)
@@ -63,6 +63,7 @@ use Wikimedia\Services\ServiceContainer;
 use Wikimedia\Services\NoSuchServiceException;
 use MediaWiki\Interwiki\InterwikiLookup;
 use MagicWordFactory;
+use MediaWiki\Storage\PageEditStash;
 
 /**
  * Service locator for MediaWiki core services.
@@ -690,6 +691,14 @@ class MediaWikiServices extends ServiceContainer {
                return $this->getService( 'OldRevisionImporter' );
        }
 
+       /**
+        * @return PageEditStash
+        * @since 1.34
+        */
+       public function getPageEditStash() {
+               return $this->getService( 'PageEditStash' );
+       }
+
        /**
         * @since 1.29
         * @return Parser
index 859593b..3e91fb3 100644 (file)
@@ -2198,8 +2198,6 @@ class OutputPage extends ContextSource {
         * @return ParserOutput
         */
        private function parseInternal( $text, $title, $linestart, $tidy, $interface, $language ) {
-               global $wgParser;
-
                if ( is_null( $title ) ) {
                        throw new MWException( 'Empty $mTitle in ' . __METHOD__ );
                }
@@ -2212,7 +2210,7 @@ class OutputPage extends ContextSource {
                        $oldLang = $popts->setTargetLanguage( $language );
                }
 
-               $parserOutput = $wgParser->getFreshParser()->parse(
+               $parserOutput = MediaWikiServices::getInstance()->getParser()->getFreshParser()->parse(
                        $text, $title, $popts,
                        $linestart, true, $this->mRevisionId
                );
index d39a0c7..40e9f87 100644 (file)
@@ -62,6 +62,7 @@ use MediaWiki\Storage\BlobStore;
 use MediaWiki\Storage\BlobStoreFactory;
 use MediaWiki\Storage\NameTableStoreFactory;
 use MediaWiki\Storage\SqlBlobStore;
+use MediaWiki\Storage\PageEditStash;
 
 return [
        'ActorMigration' => function ( MediaWikiServices $services ) : ActorMigration {
@@ -350,6 +351,20 @@ return [
                );
        },
 
+       'PageEditStash' => function ( MediaWikiServices $services ) : PageEditStash {
+               $config = $services->getMainConfig();
+
+               return new PageEditStash(
+                       ObjectCache::getLocalClusterInstance(),
+                       $services->getDBLoadBalancer(),
+                       LoggerFactory::getInstance( 'StashEdit' ),
+                       $services->getStatsdDataFactory(),
+                       defined( 'MEDIAWIKI_JOB_RUNNER' ) || $config->get( 'CommandLineMode' )
+                               ? PageEditStash::INITIATOR_JOB_OR_CLI
+                               : PageEditStash::INITIATOR_USER
+               );
+       },
+
        'Parser' => function ( MediaWikiServices $services ) : Parser {
                return $services->getParserFactory()->create();
        },
@@ -443,8 +458,17 @@ return [
                        $config,
                        LoggerFactory::getInstance( 'resourceloader' )
                );
+
                $rl->addSource( $config->get( 'ResourceLoaderSources' ) );
+
+               // Core modules, then extension/skin modules
                $rl->register( include "$IP/resources/Resources.php" );
+               $rl->register( $config->get( 'ResourceModules' ) );
+               Hooks::run( 'ResourceLoaderRegisterModules', [ &$rl ] );
+
+               if ( $config->get( 'EnableJavaScriptTest' ) === true ) {
+                       $rl->registerTestModules();
+               }
 
                return $rl;
        },
index 3dbe0a8..401806b 100644 (file)
@@ -22,7 +22,6 @@
 
 namespace MediaWiki\Storage;
 
-use ApiStashEdit;
 use CategoryMembershipChangeJob;
 use Content;
 use ContentHandler;
@@ -38,6 +37,7 @@ use LinksDeletionUpdate;
 use LinksUpdate;
 use LogicException;
 use MediaWiki\Edit\PreparedEdit;
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Revision\MutableRevisionRecord;
 use MediaWiki\Revision\RenderedRevision;
 use MediaWiki\Revision\RevisionRecord;
@@ -755,9 +755,12 @@ class DerivedPageDataUpdater implements IDBAccessObject {
 
                // TODO: MCR: allow output for all slots to be stashed.
                if ( $useStash && $slotsUpdate->isModifiedSlot( SlotRecord::MAIN ) ) {
-                       $mainContent = $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent();
-                       $legacyUser = User::newFromIdentity( $user );
-                       $stashedEdit = ApiStashEdit::checkCache( $title, $mainContent, $legacyUser );
+                       $editStash = MediaWikiServices::getInstance()->getPageEditStash();
+                       $stashedEdit = $editStash->checkCache(
+                               $title,
+                               $slotsUpdate->getModifiedSlot( SlotRecord::MAIN )->getContent(),
+                               User::newFromIdentity( $user )
+                       );
                }
 
                $userPopts = ParserOptions::newFromUserAndLang( $user, $this->contLang );
diff --git a/includes/Storage/PageEditStash.php b/includes/Storage/PageEditStash.php
new file mode 100644 (file)
index 0000000..cc3e4bc
--- /dev/null
@@ -0,0 +1,504 @@
+<?php
+/**
+ * Predictive edit preparation system for MediaWiki page.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+namespace MediaWiki\Storage;
+
+use ActorMigration;
+use BagOStuff;
+use Content;
+use Hooks;
+use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
+use ParserOutput;
+use Psr\Log\LoggerInterface;
+use stdClass;
+use Title;
+use User;
+use Wikimedia\Rdbms\ILoadBalancer;
+use Wikimedia\ScopedCallback;
+use WikiPage;
+
+/**
+ * Class for managing stashed edits used by the page updater classes
+ *
+ * @since 1.34
+ */
+class PageEditStash {
+       /** @var BagOStuff */
+       private $cache;
+       /** @var ILoadBalancer */
+       private $lb;
+       /** @var LoggerInterface */
+       private $logger;
+       /** @var StatsdDataFactoryInterface */
+       private $stats;
+       /** @var int */
+       private $initiator;
+
+       const ERROR_NONE = 'stashed';
+       const ERROR_PARSE = 'error_parse';
+       const ERROR_CACHE = 'error_cache';
+       const ERROR_UNCACHEABLE = 'uncacheable';
+       const ERROR_BUSY = 'busy';
+
+       const PRESUME_FRESH_TTL_SEC = 30;
+       const MAX_CACHE_TTL = 300; // 5 minutes
+       const MAX_SIGNATURE_TTL = 60;
+
+       const MAX_CACHE_RECENT = 2;
+
+       const INITIATOR_USER = 1;
+       const INITIATOR_JOB_OR_CLI = 2;
+
+       /**
+        * @param BagOStuff $cache
+        * @param ILoadBalancer $lb
+        * @param LoggerInterface $logger
+        * @param StatsdDataFactoryInterface $stats
+        * @param int $initiator Class INITIATOR__* constant
+        */
+       public function __construct(
+               BagOStuff $cache,
+               ILoadBalancer $lb,
+               LoggerInterface $logger,
+               StatsdDataFactoryInterface $stats,
+               $initiator
+       ) {
+               $this->cache = $cache;
+               $this->lb = $lb;
+               $this->logger = $logger;
+               $this->stats = $stats;
+               $this->initiator = $initiator;
+       }
+
+       /**
+        * @param WikiPage $page
+        * @param Content $content Edit content
+        * @param User $user
+        * @param string $summary Edit summary
+        * @return string Class ERROR_* constant
+        */
+       public function parseAndCache( WikiPage $page, Content $content, User $user, $summary ) {
+               $logger = $this->logger;
+
+               $title = $page->getTitle();
+               $key = $this->getStashKey( $title, $this->getContentHash( $content ), $user );
+               $fname = __METHOD__;
+
+               // Use the master DB to allow for fast blocking locks on the "save path" where this
+               // value might actually be used to complete a page edit. If the edit submission request
+               // happens before this edit stash requests finishes, then the submission will block until
+               // the stash request finishes parsing. For the lock acquisition below, there is not much
+               // need to duplicate parsing of the same content/user/summary bundle, so try to avoid
+               // blocking at all here.
+               $dbw = $this->lb->getConnection( DB_MASTER );
+               if ( !$dbw->lock( $key, $fname, 0 ) ) {
+                       // De-duplicate requests on the same key
+                       return self::ERROR_BUSY;
+               }
+               /** @noinspection PhpUnusedLocalVariableInspection */
+               $unlocker = new ScopedCallback( function () use ( $dbw, $key, $fname ) {
+                       $dbw->unlock( $key, $fname );
+               } );
+
+               $cutoffTime = time() - self::PRESUME_FRESH_TTL_SEC;
+
+               // Reuse any freshly build matching edit stash cache
+               $editInfo = $this->getStashValue( $key );
+               if ( $editInfo && wfTimestamp( TS_UNIX, $editInfo->timestamp ) >= $cutoffTime ) {
+                       $alreadyCached = true;
+               } else {
+                       $format = $content->getDefaultFormat();
+                       $editInfo = $page->prepareContentForEdit( $content, null, $user, $format, false );
+                       $editInfo->output->setCacheTime( $editInfo->timestamp );
+                       $alreadyCached = false;
+               }
+
+               $context = [ 'cachekey' => $key, 'title' => $title->getPrefixedText() ];
+
+               if ( $editInfo && $editInfo->output ) {
+                       // Let extensions add ParserOutput metadata or warm other caches
+                       Hooks::run( 'ParserOutputStashForEdit',
+                               [ $page, $content, $editInfo->output, $summary, $user ] );
+
+                       if ( $alreadyCached ) {
+                               $logger->debug( "Parser output for key '{cachekey}' already cached.", $context );
+
+                               return self::ERROR_NONE;
+                       }
+
+                       $code = $this->storeStashValue(
+                               $key,
+                               $editInfo->pstContent,
+                               $editInfo->output,
+                               $editInfo->timestamp,
+                               $user
+                       );
+
+                       if ( $code === true ) {
+                               $logger->debug( "Cached parser output for key '{cachekey}'.", $context );
+
+                               return self::ERROR_NONE;
+                       } elseif ( $code === 'uncacheable' ) {
+                               $logger->info(
+                                       "Uncacheable parser output for key '{cachekey}' [{code}].",
+                                       $context + [ 'code' => $code ]
+                               );
+
+                               return self::ERROR_UNCACHEABLE;
+                       } else {
+                               $logger->error(
+                                       "Failed to cache parser output for key '{cachekey}'.",
+                                       $context + [ 'code' => $code ]
+                               );
+
+                               return self::ERROR_CACHE;
+                       }
+               }
+
+               return self::ERROR_PARSE;
+       }
+
+       /**
+        * Check that a prepared edit is in cache and still up-to-date
+        *
+        * This method blocks if the prepared edit is already being rendered,
+        * waiting until rendering finishes before doing final validity checks.
+        *
+        * The cache is rejected if template or file changes are detected.
+        * Note that foreign template or file transclusions are not checked.
+        *
+        * This returns an object with the following fields:
+        *   - pstContent: the Content after pre-save-transform
+        *   - output: the ParserOutput instance
+        *   - timestamp: the timestamp of the parse
+        *   - edits: author edit count if they are logged in or NULL otherwise
+        *
+        * @param Title $title
+        * @param Content $content
+        * @param User $user User to get parser options from
+        * @return stdClass|bool Returns edit stash object or false on cache miss
+        */
+       public function checkCache( Title $title, Content $content, User $user ) {
+               if (
+                       // The context is not an HTTP POST request
+                       !$user->getRequest()->wasPosted() ||
+                       // The context is a CLI script or a job runner HTTP POST request
+                       $this->initiator !== self::INITIATOR_USER ||
+                       // The editor account is a known bot
+                       $user->isBot()
+               ) {
+                       // Avoid wasted queries and statsd pollution
+                       return false;
+               }
+
+               $logger = $this->logger;
+
+               $key = $this->getStashKey( $title, $this->getContentHash( $content ), $user );
+               $context = [
+                       'key' => $key,
+                       'title' => $title->getPrefixedText(),
+                       'user' => $user->getName()
+               ];
+
+               $editInfo = $this->getAndWaitForStashValue( $key );
+               if ( !is_object( $editInfo ) || !$editInfo->output ) {
+                       $this->stats->increment( 'editstash.cache_misses.no_stash' );
+                       if ( $this->recentStashEntryCount( $user ) > 0 ) {
+                               $logger->info( "Empty cache for key '{key}' but not for user.", $context );
+                       } else {
+                               $logger->debug( "Empty cache for key '{key}'.", $context );
+                       }
+
+                       return false;
+               }
+
+               $age = time() - wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() );
+               $context['age'] = $age;
+
+               $isCacheUsable = true;
+               if ( $age <= self::PRESUME_FRESH_TTL_SEC ) {
+                       // Assume nothing changed in this time
+                       $this->stats->increment( 'editstash.cache_hits.presumed_fresh' );
+                       $logger->debug( "Timestamp-based cache hit for key '{key}'.", $context );
+               } elseif ( $user->isAnon() ) {
+                       $lastEdit = $this->lastEditTime( $user );
+                       $cacheTime = $editInfo->output->getCacheTime();
+                       if ( $lastEdit < $cacheTime ) {
+                               // Logged-out user made no local upload/template edits in the meantime
+                               $this->stats->increment( 'editstash.cache_hits.presumed_fresh' );
+                               $logger->debug( "Edit check based cache hit for key '{key}'.", $context );
+                       } else {
+                               $isCacheUsable = false;
+                               $this->stats->increment( 'editstash.cache_misses.proven_stale' );
+                               $logger->info( "Stale cache for key '{key}' due to outside edits.", $context );
+                       }
+               } else {
+                       if ( $editInfo->edits === $user->getEditCount() ) {
+                               // Logged-in user made no local upload/template edits in the meantime
+                               $this->stats->increment( 'editstash.cache_hits.presumed_fresh' );
+                               $logger->debug( "Edit count based cache hit for key '{key}'.", $context );
+                       } else {
+                               $isCacheUsable = false;
+                               $this->stats->increment( 'editstash.cache_misses.proven_stale' );
+                               $logger->info( "Stale cache for key '{key}'due to outside edits.", $context );
+                       }
+               }
+
+               if ( !$isCacheUsable ) {
+                       return false;
+               }
+
+               if ( $editInfo->output->getFlag( 'vary-revision' ) ) {
+                       // This can be used for the initial parse, e.g. for filters or doEditContent(),
+                       // but a second parse will be triggered in doEditUpdates(). This is not optimal.
+                       $logger->info(
+                               "Cache for key '{key}' has vary_revision; post-insertion parse inevitable.",
+                               $context
+                       );
+               } elseif ( $editInfo->output->getFlag( 'vary-revision-id' ) ) {
+                       // Similar to the above if we didn't guess the ID correctly.
+                       $logger->debug(
+                               "Cache for key '{key}' has vary_revision_id; post-insertion parse possible.",
+                               $context
+                       );
+               }
+
+               return $editInfo;
+       }
+
+       /**
+        * @param string $key
+        * @return bool|stdClass
+        */
+       private function getAndWaitForStashValue( $key ) {
+               $editInfo = $this->getStashValue( $key );
+
+               if ( !$editInfo ) {
+                       $start = microtime( true );
+                       // We ignore user aborts and keep parsing. Block on any prior parsing
+                       // so as to use its results and make use of the time spent parsing.
+                       // Skip this logic if there no master connection in case this method
+                       // is called on an HTTP GET request for some reason.
+                       $dbw = $this->lb->getAnyOpenConnection( $this->lb->getWriterIndex() );
+                       if ( $dbw && $dbw->lock( $key, __METHOD__, 30 ) ) {
+                               $editInfo = $this->getStashValue( $key );
+                               $dbw->unlock( $key, __METHOD__ );
+                       }
+
+                       $timeMs = 1000 * max( 0, microtime( true ) - $start );
+                       $this->stats->timing( 'editstash.lock_wait_time', $timeMs );
+               }
+
+               return $editInfo;
+       }
+
+       /**
+        * @param string $textHash
+        * @return string|bool Text or false if missing
+        */
+       public function fetchInputText( $textHash ) {
+               $textKey = $this->cache->makeKey( 'stashedit', 'text', $textHash );
+
+               return $this->cache->get( $textKey );
+       }
+
+       /**
+        * @param string $text
+        * @param string $textHash
+        * @return bool Success
+        */
+       public function stashInputText( $text, $textHash ) {
+               $textKey = $this->cache->makeKey( 'stashedit', 'text', $textHash );
+
+               return $this->cache->set( $textKey, $text, self::MAX_CACHE_TTL );
+       }
+
+       /**
+        * @param User $user
+        * @return string|null TS_MW timestamp or null
+        */
+       private function lastEditTime( User $user ) {
+               $db = $this->lb->getConnection( DB_REPLICA );
+               $actorQuery = ActorMigration::newMigration()->getWhere( $db, 'rc_user', $user, false );
+               $time = $db->selectField(
+                       [ 'recentchanges' ] + $actorQuery['tables'],
+                       'MAX(rc_timestamp)',
+                       [ $actorQuery['conds'] ],
+                       __METHOD__,
+                       [],
+                       $actorQuery['joins']
+               );
+
+               return wfTimestampOrNull( TS_MW, $time );
+       }
+
+       /**
+        * Get hash of the content, factoring in model/format
+        *
+        * @param Content $content
+        * @return string
+        */
+       private function getContentHash( Content $content ) {
+               return sha1( implode( "\n", [
+                       $content->getModel(),
+                       $content->getDefaultFormat(),
+                       $content->serialize( $content->getDefaultFormat() )
+               ] ) );
+       }
+
+       /**
+        * Get the temporary prepared edit stash key for a user
+        *
+        * This key can be used for caching prepared edits provided:
+        *   - a) The $user was used for PST options
+        *   - b) The parser output was made from the PST using cannonical matching options
+        *
+        * @param Title $title
+        * @param string $contentHash Result of getContentHash()
+        * @param User $user User to get parser options from
+        * @return string
+        */
+       private function getStashKey( Title $title, $contentHash, User $user ) {
+               return $this->cache->makeKey(
+                       'stashed-edit-info',
+                       md5( $title->getPrefixedDBkey() ),
+                       // Account for the edit model/text
+                       $contentHash,
+                       // Account for user name related variables like signatures
+                       md5( $user->getId() . "\n" . $user->getName() )
+               );
+       }
+
+       /**
+        * @param string $hash
+        * @return string
+        */
+       private function getStashParserOutputKey( $hash ) {
+               return $this->cache->makeKey( 'stashed-edit-output', $hash );
+       }
+
+       /**
+        * @param string $key
+        * @return stdClass|bool Object map (pstContent,output,outputID,timestamp,edits) or false
+        */
+       private function getStashValue( $key ) {
+               $stashInfo = $this->cache->get( $key );
+               if ( !is_object( $stashInfo ) ) {
+                       return false;
+               }
+
+               $parserOutputKey = $this->getStashParserOutputKey( $stashInfo->outputID );
+               $parserOutput = $this->cache->get( $parserOutputKey );
+               if ( $parserOutput instanceof ParserOutput ) {
+                       $stashInfo->output = $parserOutput;
+
+                       return $stashInfo;
+               }
+
+               return false;
+       }
+
+       /**
+        * Build a value to store in memcached based on the PST content and parser output
+        *
+        * This makes a simple version of WikiPage::prepareContentForEdit() as stash info
+        *
+        * @param string $key
+        * @param Content $pstContent Pre-Save transformed content
+        * @param ParserOutput $parserOutput
+        * @param string $timestamp TS_MW
+        * @param User $user
+        * @return string|bool True or an error code
+        */
+       private function storeStashValue(
+               $key,
+               Content $pstContent,
+               ParserOutput $parserOutput,
+               $timestamp,
+               User $user
+       ) {
+               // If an item is renewed, mind the cache TTL determined by config and parser functions.
+               // Put an upper limit on the TTL for sanity to avoid extreme template/file staleness.
+               $age = time() - wfTimestamp( TS_UNIX, $parserOutput->getCacheTime() );
+               $ttl = min( $parserOutput->getCacheExpiry() - $age, self::MAX_CACHE_TTL );
+               // Avoid extremely stale user signature timestamps (T84843)
+               if ( $parserOutput->getFlag( 'user-signature' ) ) {
+                       $ttl = min( $ttl, self::MAX_SIGNATURE_TTL );
+               }
+
+               if ( $ttl <= 0 ) {
+                       return 'uncacheable'; // low TTL due to a tag, magic word, or signature?
+               }
+
+               // Store what is actually needed and split the output into another key (T204742)
+               $parserOutputID = md5( $key );
+               $stashInfo = (object)[
+                       'pstContent' => $pstContent,
+                       'outputID'   => $parserOutputID,
+                       'timestamp'  => $timestamp,
+                       'edits'      => $user->getEditCount()
+               ];
+
+               $ok = $this->cache->set( $key, $stashInfo, $ttl );
+               if ( $ok ) {
+                       $ok = $this->cache->set(
+                               $this->getStashParserOutputKey( $parserOutputID ),
+                               $parserOutput,
+                               $ttl
+                       );
+               }
+
+               if ( $ok ) {
+                       // These blobs can waste slots in low cardinality memcached slabs
+                       $this->pruneExcessStashedEntries( $user, $key );
+               }
+
+               return $ok ? true : 'store_error';
+       }
+
+       /**
+        * @param User $user
+        * @param string $newKey
+        */
+       private function pruneExcessStashedEntries( User $user, $newKey ) {
+               $key = $this->cache->makeKey( 'stash-edit-recent', sha1( $user->getName() ) );
+
+               $keyList = $this->cache->get( $key ) ?: [];
+               if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
+                       $oldestKey = array_shift( $keyList );
+                       $this->cache->delete( $oldestKey );
+               }
+
+               $keyList[] = $newKey;
+               $this->cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL );
+       }
+
+       /**
+        * @param User $user
+        * @return int
+        */
+       private function recentStashEntryCount( User $user ) {
+               $key = $this->cache->makeKey( 'stash-edit-recent', sha1( $user->getName() ) );
+
+               return count( $this->cache->get( $key ) ?: [] );
+       }
+}
index c9d0ae9..14f7603 100644 (file)
@@ -27,6 +27,7 @@
  */
 
 use MediaWiki\Logger\LoggerFactory;
+use MediaWiki\MediaWikiServices;
 
 /**
  * A simple method to retrieve the plain source of an article,
@@ -181,8 +182,6 @@ class RawAction extends FormlessAction {
         * @return string|bool
         */
        public function getRawText() {
-               global $wgParser;
-
                $text = false;
                $title = $this->getTitle();
                $request = $this->getRequest();
@@ -221,7 +220,7 @@ class RawAction extends FormlessAction {
                }
 
                if ( $text !== false && $text !== '' && $request->getRawVal( 'templates' ) === 'expand' ) {
-                       $text = $wgParser->preprocess(
+                       $text = MediaWikiServices::getInstance()->getParser()->preprocess(
                                $text,
                                $title,
                                ParserOptions::newFromContext( $this->getContext() )
index 22f5235..851373d 100644 (file)
@@ -86,7 +86,6 @@ class ApiExpandTemplates extends ApiBase {
                $result = $this->getResult();
 
                // Parse text
-               global $wgParser;
                $options = ParserOptions::newFromContext( $this->getContext() );
 
                if ( $params['includecomments'] ) {
@@ -100,9 +99,10 @@ class ApiExpandTemplates extends ApiBase {
 
                $retval = [];
 
+               $parser = MediaWikiServices::getInstance()->getParser();
                if ( isset( $prop['parsetree'] ) || $params['generatexml'] ) {
-                       $wgParser->startExternalParse( $titleObj, $options, Parser::OT_PREPROCESS );
-                       $dom = $wgParser->preprocessToDom( $params['text'] );
+                       $parser->startExternalParse( $titleObj, $options, Parser::OT_PREPROCESS );
+                       $dom = $parser->preprocessToDom( $params['text'] );
                        if ( is_callable( [ $dom, 'saveXML' ] ) ) {
                                $xml = $dom->saveXML();
                        } else {
@@ -121,14 +121,14 @@ class ApiExpandTemplates extends ApiBase {
                // if they didn't want any output except (probably) the parse tree,
                // then don't bother actually fully expanding it
                if ( $prop || $params['prop'] === null ) {
-                       $wgParser->startExternalParse( $titleObj, $options, Parser::OT_PREPROCESS );
-                       $frame = $wgParser->getPreprocessor()->newFrame();
-                       $wikitext = $wgParser->preprocess( $params['text'], $titleObj, $options, $revid, $frame );
+                       $parser->startExternalParse( $titleObj, $options, Parser::OT_PREPROCESS );
+                       $frame = $parser->getPreprocessor()->newFrame();
+                       $wikitext = $parser->preprocess( $params['text'], $titleObj, $options, $revid, $frame );
                        if ( $params['prop'] === null ) {
                                // the old way
                                ApiResult::setContentValue( $retval, 'wikitext', $wikitext );
                        } else {
-                               $p_output = $wgParser->getOutput();
+                               $p_output = $parser->getOutput();
                                if ( isset( $prop['categories'] ) ) {
                                        $categories = $p_output->getCategories();
                                        if ( $categories ) {
index 11b5d91..c4977f4 100644 (file)
@@ -20,6 +20,8 @@
  * @file
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * This action allows users to get their watchlist items in RSS/Atom formats.
  * When executed, it performs a nested call to the API to get the needed data,
@@ -209,8 +211,8 @@ class ApiFeedWatchlist extends ApiBase {
                if ( $this->linkToSections && $comment !== null &&
                        preg_match( '!(.*)/\*\s*(.*?)\s*\*/(.*)!', $comment, $matches )
                ) {
-                       global $wgParser;
-                       $titleUrl .= $wgParser->guessSectionNameFromWikiText( $matches[ 2 ] );
+                       $titleUrl .= MediaWikiServices::getInstance()->getParser()
+                               ->guessSectionNameFromWikiText( $matches[ 2 ] );
                }
 
                $timestamp = $info['timestamp'];
index 262eb1f..40cc738 100644 (file)
@@ -79,7 +79,9 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
 
                $data = $this->getResult()->getResultData();
                if ( isset( $data['_feed'] ) && isset( $data['_feeditems'] ) ) {
-                       $data['_feed']->httpHeaders();
+                       /** @var ChannelFeed $feed */
+                       $feed = $data['_feed'];
+                       $feed->httpHeaders();
                } else {
                        // Error has occurred, print something useful
                        ApiBase::dieDebug( __METHOD__, 'Invalid feed class/item' );
@@ -94,6 +96,7 @@ class ApiFormatFeedWrapper extends ApiFormatBase {
        public function execute() {
                $data = $this->getResult()->getResultData();
                if ( isset( $data['_feed'] ) && isset( $data['_feeditems'] ) ) {
+                       /** @var ChannelFeed $feed */
                        $feed = $data['_feed'];
                        $items = $data['_feeditems'];
 
index 5e4639d..84fff96 100644 (file)
@@ -83,7 +83,7 @@ class ApiParse extends ApiBase {
                // The parser needs $wgTitle to be set, apparently the
                // $title parameter in Parser::parse isn't enough *sigh*
                // TODO: Does this still need $wgTitle?
-               global $wgParser, $wgTitle;
+               global $wgTitle;
 
                $redirValues = null;
 
@@ -488,8 +488,9 @@ class ApiParse extends ApiBase {
                                $this->dieWithError( 'apierror-parsetree-notwikitext', 'notwikitext' );
                        }
 
-                       $wgParser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
-                       $xml = $wgParser->preprocessToDom( $this->content->getText() )->__toString();
+                       $parser = MediaWikiServices::getInstance()->getParser();
+                       $parser->startExternalParse( $titleObj, $popts, Parser::OT_PREPROCESS );
+                       $xml = $parser->preprocessToDom( $this->content->getText() )->__toString();
                        $result_array['parsetree'] = $xml;
                        $result_array[ApiResult::META_BC_SUBELEMENTS][] = 'parsetree';
                }
@@ -631,7 +632,6 @@ class ApiParse extends ApiBase {
         * @return Content|bool
         */
        private function formatSummary( $title, $params ) {
-               global $wgParser;
                $summary = $params['summary'] ?? '';
                $sectionTitle = $params['sectiontitle'] ?? '';
 
@@ -641,8 +641,9 @@ class ApiParse extends ApiBase {
                        }
                        if ( $summary !== '' ) {
                                $summary = wfMessage( 'newsectionsummary' )
-                                       ->rawParams( $wgParser->stripSectionName( $summary ) )
-                                               ->inContentLanguage()->text();
+                                       ->rawParams( MediaWikiServices::getInstance()->getParser()
+                                               ->stripSectionName( $summary ) )
+                                       ->inContentLanguage()->text();
                        }
                }
                return Linker::formatComment( $summary, $title, $this->section === 'new' );
index 565e615..d0b152e 100644 (file)
@@ -496,8 +496,6 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
         * @return array
         */
        private function extractDeprecatedContent( Content $content, RevisionRecord $revision ) {
-               global $wgParser;
-
                $vals = [];
                $title = Title::newFromLinkTarget( $revision->getPageAsLinkTarget() );
 
@@ -505,12 +503,13 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
                        if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
                                $t = $content->getText(); # note: don't set $text
 
-                               $wgParser->startExternalParse(
+                               $parser = MediaWikiServices::getInstance()->getParser();
+                               $parser->startExternalParse(
                                        $title,
                                        ParserOptions::newFromContext( $this->getContext() ),
                                        Parser::OT_PREPROCESS
                                );
-                               $dom = $wgParser->preprocessToDom( $t );
+                               $dom = $parser->preprocessToDom( $t );
                                if ( is_callable( [ $dom, 'saveXML' ] ) ) {
                                        $xml = $dom->saveXML();
                                } else {
@@ -537,7 +536,7 @@ abstract class ApiQueryRevisionsBase extends ApiQueryGeneratorBase {
                                if ( $content->getModel() === CONTENT_MODEL_WIKITEXT ) {
                                        $text = $content->getText();
 
-                                       $text = $wgParser->preprocess(
+                                       $text = MediaWikiServices::getInstance()->getParser()->preprocess(
                                                $text,
                                                $title,
                                                ParserOptions::newFromContext( $this->getContext() )
index ea2f31b..68ab725 100644 (file)
@@ -787,12 +787,11 @@ class ApiQuerySiteinfo extends ApiQueryBase {
        }
 
        public function appendExtensionTags( $property ) {
-               global $wgParser;
                $tags = array_map(
                        function ( $item ) {
                                return "<$item>";
                        },
-                       $wgParser->getTags()
+                       MediaWikiServices::getInstance()->getParser()->getTags()
                );
                ApiResult::setArrayType( $tags, 'BCarray' );
                ApiResult::setIndexedTagName( $tags, 't' );
@@ -801,8 +800,7 @@ class ApiQuerySiteinfo extends ApiQueryBase {
        }
 
        public function appendFunctionHooks( $property ) {
-               global $wgParser;
-               $hooks = $wgParser->getFunctionHooks();
+               $hooks = MediaWikiServices::getInstance()->getParser()->getFunctionHooks();
                ApiResult::setArrayType( $hooks, 'BCarray' );
                ApiResult::setIndexedTagName( $hooks, 'h' );
 
index 5184562..d6d15c7 100644 (file)
@@ -18,9 +18,7 @@
  * @file
  */
 
-use MediaWiki\Logger\LoggerFactory;
 use MediaWiki\MediaWikiServices;
-use Wikimedia\ScopedCallback;
 
 /**
  * Prepare an edit in shared cache so that it can be reused on edit
@@ -36,18 +34,6 @@ use Wikimedia\ScopedCallback;
  * @since 1.25
  */
 class ApiStashEdit extends ApiBase {
-       const ERROR_NONE = 'stashed';
-       const ERROR_PARSE = 'error_parse';
-       const ERROR_CACHE = 'error_cache';
-       const ERROR_UNCACHEABLE = 'uncacheable';
-       const ERROR_BUSY = 'busy';
-
-       const PRESUME_FRESH_TTL_SEC = 30;
-       const MAX_CACHE_TTL = 300; // 5 minutes
-       const MAX_SIGNATURE_TTL = 60;
-
-       const MAX_CACHE_RECENT = 2;
-
        public function execute() {
                $user = $this->getUser();
                $params = $this->extractRequestParams();
@@ -56,7 +42,7 @@ class ApiStashEdit extends ApiBase {
                        $this->dieWithError( 'apierror-botsnotsupported' );
                }
 
-               $cache = ObjectCache::getLocalClusterInstance();
+               $editStash = MediaWikiServices::getInstance()->getPageEditStash();
                $page = $this->getTitleOrPageId( $params );
                $title = $page->getTitle();
 
@@ -79,8 +65,7 @@ class ApiStashEdit extends ApiBase {
                        if ( !preg_match( '/^[0-9a-f]{40}$/', $textHash ) ) {
                                $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
                        }
-                       $textKey = $cache->makeKey( 'stashedit', 'text', $textHash );
-                       $text = $cache->get( $textKey );
+                       $text = $editStash->fetchInputText( $textHash );
                        if ( !is_string( $text ) ) {
                                $this->dieWithError( 'apierror-stashedit-missingtext', 'missingtext' );
                        }
@@ -145,9 +130,8 @@ class ApiStashEdit extends ApiBase {
                if ( $user->pingLimiter( 'stashedit' ) ) {
                        $status = 'ratelimited';
                } else {
-                       $status = self::parseAndStash( $page, $content, $user, $params['summary'] );
-                       $textKey = $cache->makeKey( 'stashedit', 'text', $textHash );
-                       $cache->set( $textKey, $text, self::MAX_CACHE_TTL );
+                       $status = $editStash->parseAndCache( $page, $content, $user, $params['summary'] );
+                       $editStash->stashInputText( $text, $textHash );
                }
 
                $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
@@ -169,324 +153,12 @@ class ApiStashEdit extends ApiBase {
         * @param string $summary Edit summary
         * @return string ApiStashEdit::ERROR_* constant
         * @since 1.25
+        * @deprecated Since 1.34
         */
-       public static function parseAndStash( WikiPage $page, Content $content, User $user, $summary ) {
-               $logger = LoggerFactory::getInstance( 'StashEdit' );
-
-               $title = $page->getTitle();
-               $key = self::getStashKey( $title, self::getContentHash( $content ), $user );
-               $fname = __METHOD__;
-
-               // Use the master DB to allow for fast blocking locks on the "save path" where this
-               // value might actually be used to complete a page edit. If the edit submission request
-               // happens before this edit stash requests finishes, then the submission will block until
-               // the stash request finishes parsing. For the lock acquisition below, there is not much
-               // need to duplicate parsing of the same content/user/summary bundle, so try to avoid
-               // blocking at all here.
-               $dbw = wfGetDB( DB_MASTER );
-               if ( !$dbw->lock( $key, $fname, 0 ) ) {
-                       // De-duplicate requests on the same key
-                       return self::ERROR_BUSY;
-               }
-               /** @noinspection PhpUnusedLocalVariableInspection */
-               $unlocker = new ScopedCallback( function () use ( $dbw, $key, $fname ) {
-                       $dbw->unlock( $key, $fname );
-               } );
-
-               $cutoffTime = time() - self::PRESUME_FRESH_TTL_SEC;
-
-               // Reuse any freshly build matching edit stash cache
-               $editInfo = self::getStashValue( $key );
-               if ( $editInfo && wfTimestamp( TS_UNIX, $editInfo->timestamp ) >= $cutoffTime ) {
-                       $alreadyCached = true;
-               } else {
-                       $format = $content->getDefaultFormat();
-                       $editInfo = $page->prepareContentForEdit( $content, null, $user, $format, false );
-                       $alreadyCached = false;
-               }
-
-               if ( $editInfo && $editInfo->output ) {
-                       // Let extensions add ParserOutput metadata or warm other caches
-                       Hooks::run( 'ParserOutputStashForEdit',
-                               [ $page, $content, $editInfo->output, $summary, $user ] );
-
-                       $titleStr = (string)$title;
-                       if ( $alreadyCached ) {
-                               $logger->debug( "Already cached parser output for key '{cachekey}' ('{title}').",
-                                       [ 'cachekey' => $key, 'title' => $titleStr ] );
-                               return self::ERROR_NONE;
-                       }
-
-                       $code = self::storeStashValue(
-                               $key,
-                               $editInfo->pstContent,
-                               $editInfo->output,
-                               $editInfo->timestamp,
-                               $user
-                       );
-
-                       if ( $code === true ) {
-                               $logger->debug( "Cached parser output for key '{cachekey}' ('{title}').",
-                                       [ 'cachekey' => $key, 'title' => $titleStr ] );
-                               return self::ERROR_NONE;
-                       } elseif ( $code === 'uncacheable' ) {
-                               $logger->info(
-                                       "Uncacheable parser output for key '{cachekey}' ('{title}') [{code}].",
-                                       [ 'cachekey' => $key, 'title' => $titleStr, 'code' => $code ] );
-                               return self::ERROR_UNCACHEABLE;
-                       } else {
-                               $logger->error( "Failed to cache parser output for key '{cachekey}' ('{title}').",
-                                       [ 'cachekey' => $key, 'title' => $titleStr, 'code' => $code ] );
-                               return self::ERROR_CACHE;
-                       }
-               }
-
-               return self::ERROR_PARSE;
-       }
-
-       /**
-        * Check that a prepared edit is in cache and still up-to-date
-        *
-        * This method blocks if the prepared edit is already being rendered,
-        * waiting until rendering finishes before doing final validity checks.
-        *
-        * The cache is rejected if template or file changes are detected.
-        * Note that foreign template or file transclusions are not checked.
-        *
-        * The result is a map (pstContent,output,timestamp) with fields
-        * extracted directly from WikiPage::prepareContentForEdit().
-        *
-        * @param Title $title
-        * @param Content $content
-        * @param User $user User to get parser options from
-        * @return stdClass|bool Returns false on cache miss
-        */
-       public static function checkCache( Title $title, Content $content, User $user ) {
-               if ( $user->isBot() ) {
-                       return false; // bots never stash - don't pollute stats
-               }
-
-               $logger = LoggerFactory::getInstance( 'StashEdit' );
-               $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
-
-               $key = self::getStashKey( $title, self::getContentHash( $content ), $user );
-               $editInfo = self::getStashValue( $key );
-               if ( !is_object( $editInfo ) ) {
-                       $start = microtime( true );
-                       // We ignore user aborts and keep parsing. Block on any prior parsing
-                       // so as to use its results and make use of the time spent parsing.
-                       // Skip this logic if there no master connection in case this method
-                       // is called on an HTTP GET request for some reason.
-                       $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
-                       $dbw = $lb->getAnyOpenConnection( $lb->getWriterIndex() );
-                       if ( $dbw && $dbw->lock( $key, __METHOD__, 30 ) ) {
-                               $editInfo = self::getStashValue( $key );
-                               $dbw->unlock( $key, __METHOD__ );
-                       }
-
-                       $timeMs = 1000 * max( 0, microtime( true ) - $start );
-                       $stats->timing( 'editstash.lock_wait_time', $timeMs );
-               }
-
-               if ( !is_object( $editInfo ) || !$editInfo->output ) {
-                       $stats->increment( 'editstash.cache_misses.no_stash' );
-                       $logger->debug( "Empty cache for key '$key' ('$title'); user '{$user->getName()}'." );
-                       return false;
-               }
-
-               $age = time() - wfTimestamp( TS_UNIX, $editInfo->output->getCacheTime() );
-               if ( $age <= self::PRESUME_FRESH_TTL_SEC ) {
-                       // Assume nothing changed in this time
-                       $stats->increment( 'editstash.cache_hits.presumed_fresh' );
-                       $logger->debug( "Timestamp-based cache hit for key '$key' (age: $age sec)." );
-               } elseif ( isset( $editInfo->edits ) && $editInfo->edits === $user->getEditCount() ) {
-                       // Logged-in user made no local upload/template edits in the meantime
-                       $stats->increment( 'editstash.cache_hits.presumed_fresh' );
-                       $logger->debug( "Edit count based cache hit for key '$key' (age: $age sec)." );
-               } elseif ( $user->isAnon()
-                       && self::lastEditTime( $user ) < $editInfo->output->getCacheTime()
-               ) {
-                       // Logged-out user made no local upload/template edits in the meantime
-                       $stats->increment( 'editstash.cache_hits.presumed_fresh' );
-                       $logger->debug( "Edit check based cache hit for key '$key' (age: $age sec)." );
-               } else {
-                       // User may have changed included content
-                       $editInfo = false;
-               }
-
-               if ( !$editInfo ) {
-                       $stats->increment( 'editstash.cache_misses.proven_stale' );
-                       $logger->info( "Stale cache for key '$key'; old key with outside edits. (age: $age sec)" );
-               } elseif ( $editInfo->output->getFlag( 'vary-revision' ) ) {
-                       // This can be used for the initial parse, e.g. for filters or doEditContent(),
-                       // but a second parse will be triggered in doEditUpdates(). This is not optimal.
-                       $logger->info( "Cache for key '$key' ('$title') has vary_revision." );
-               } elseif ( $editInfo->output->getFlag( 'vary-revision-id' ) ) {
-                       // Similar to the above if we didn't guess the ID correctly.
-                       $logger->info( "Cache for key '$key' ('$title') has vary_revision_id." );
-               }
-
-               return $editInfo;
-       }
-
-       /**
-        * @param User $user
-        * @return string|null TS_MW timestamp or null
-        */
-       private static function lastEditTime( User $user ) {
-               $db = wfGetDB( DB_REPLICA );
-               $actorQuery = ActorMigration::newMigration()->getWhere( $db, 'rc_user', $user, false );
-               $time = $db->selectField(
-                       [ 'recentchanges' ] + $actorQuery['tables'],
-                       'MAX(rc_timestamp)',
-                       [ $actorQuery['conds'] ],
-                       __METHOD__,
-                       [],
-                       $actorQuery['joins']
-               );
-
-               return wfTimestampOrNull( TS_MW, $time );
-       }
-
-       /**
-        * Get hash of the content, factoring in model/format
-        *
-        * @param Content $content
-        * @return string
-        */
-       private static function getContentHash( Content $content ) {
-               return sha1( implode( "\n", [
-                       $content->getModel(),
-                       $content->getDefaultFormat(),
-                       $content->serialize( $content->getDefaultFormat() )
-               ] ) );
-       }
-
-       /**
-        * Get the temporary prepared edit stash key for a user
-        *
-        * This key can be used for caching prepared edits provided:
-        *   - a) The $user was used for PST options
-        *   - b) The parser output was made from the PST using cannonical matching options
-        *
-        * @param Title $title
-        * @param string $contentHash Result of getContentHash()
-        * @param User $user User to get parser options from
-        * @return string
-        */
-       private static function getStashKey( Title $title, $contentHash, User $user ) {
-               return ObjectCache::getLocalClusterInstance()->makeKey(
-                       'stashed-edit-info',
-                       md5( $title->getPrefixedDBkey() ),
-                       // Account for the edit model/text
-                       $contentHash,
-                       // Account for user name related variables like signatures
-                       md5( $user->getId() . "\n" . $user->getName() )
-               );
-       }
-
-       /**
-        * @param string $uuid
-        * @return string
-        */
-       private static function getStashParserOutputKey( $uuid ) {
-               return ObjectCache::getLocalClusterInstance()->makeKey( 'stashed-edit-output', $uuid );
-       }
-
-       /**
-        * @param string $key
-        * @return stdClass|bool Object map (pstContent,output,outputID,timestamp,edits) or false
-        */
-       private static function getStashValue( $key ) {
-               $cache = ObjectCache::getLocalClusterInstance();
-
-               $stashInfo = $cache->get( $key );
-               if ( !is_object( $stashInfo ) ) {
-                       return false;
-               }
-
-               $parserOutputKey = self::getStashParserOutputKey( $stashInfo->outputID );
-               $parserOutput = $cache->get( $parserOutputKey );
-               if ( $parserOutput instanceof ParserOutput ) {
-                       $stashInfo->output = $parserOutput;
-
-                       return $stashInfo;
-               }
-
-               return false;
-       }
-
-       /**
-        * Build a value to store in memcached based on the PST content and parser output
-        *
-        * This makes a simple version of WikiPage::prepareContentForEdit() as stash info
-        *
-        * @param string $key
-        * @param Content $pstContent Pre-Save transformed content
-        * @param ParserOutput $parserOutput
-        * @param string $timestamp TS_MW
-        * @param User $user
-        * @return string|bool True or an error code
-        */
-       private static function storeStashValue(
-               $key, Content $pstContent, ParserOutput $parserOutput, $timestamp, User $user
-       ) {
-               // If an item is renewed, mind the cache TTL determined by config and parser functions.
-               // Put an upper limit on the TTL for sanity to avoid extreme template/file staleness.
-               $age = time() - wfTimestamp( TS_UNIX, $parserOutput->getCacheTime() );
-               $ttl = min( $parserOutput->getCacheExpiry() - $age, self::MAX_CACHE_TTL );
-               // Avoid extremely stale user signature timestamps (T84843)
-               if ( $parserOutput->getFlag( 'user-signature' ) ) {
-                       $ttl = min( $ttl, self::MAX_SIGNATURE_TTL );
-               }
-
-               if ( $ttl <= 0 ) {
-                       return 'uncacheable'; // low TTL due to a tag, magic word, or signature?
-               }
-
-               // Store what is actually needed and split the output into another key (T204742)
-               $parseroutputID = md5( $key );
-               $stashInfo = (object)[
-                       'pstContent' => $pstContent,
-                       'outputID'   => $parseroutputID,
-                       'timestamp'  => $timestamp,
-                       'edits'      => $user->getEditCount()
-               ];
-
-               $cache = ObjectCache::getLocalClusterInstance();
-               $ok = $cache->set( $key, $stashInfo, $ttl );
-               if ( $ok ) {
-                       $ok = $cache->set(
-                               self::getStashParserOutputKey( $parseroutputID ),
-                               $parserOutput,
-                               $ttl
-                       );
-               }
-
-               if ( $ok ) {
-                       // These blobs can waste slots in low cardinality memcached slabs
-                       self::pruneExcessStashedEntries( $cache, $user, $key );
-               }
-
-               return $ok ? true : 'store_error';
-       }
-
-       /**
-        * @param BagOStuff $cache
-        * @param User $user
-        * @param string $newKey
-        */
-       private static function pruneExcessStashedEntries( BagOStuff $cache, User $user, $newKey ) {
-               $key = $cache->makeKey( 'stash-edit-recent', sha1( $user->getName() ) );
-
-               $keyList = $cache->get( $key ) ?: [];
-               if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
-                       $oldestKey = array_shift( $keyList );
-                       $cache->delete( $oldestKey );
-               }
+       public function parseAndStash( WikiPage $page, Content $content, User $user, $summary ) {
+               $editStash = MediaWikiServices::getInstance()->getPageEditStash();
 
-               $keyList[] = $newKey;
-               $cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL );
+               return $editStash->parseAndCache( $page, $content, $user, $summary );
        }
 
        public function getAllowedParams() {
index 157d88e..fb4c7b6 100644 (file)
@@ -1203,18 +1203,18 @@ class MessageCache {
         * @return Parser
         */
        public function getParser() {
-               global $wgParser, $wgParserConf;
-
-               if ( !$this->mParser && isset( $wgParser ) ) {
+               global $wgParserConf;
+               if ( !$this->mParser ) {
+                       $parser = MediaWikiServices::getInstance()->getParser();
                        # Do some initialisation so that we don't have to do it twice
-                       $wgParser->firstCallInit();
+                       $parser->firstCallInit();
                        # Clone it and store it
                        $class = $wgParserConf['class'];
                        if ( $class == ParserDiffTest::class ) {
                                # Uncloneable
                                $this->mParser = new $class( $wgParserConf );
                        } else {
-                               $this->mParser = clone $wgParser;
+                               $this->mParser = clone $parser;
                        }
                }
 
index d32fa88..87c5ff2 100644 (file)
@@ -25,6 +25,8 @@
  * @author Daniel Kinzler
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Content object for CSS pages.
  *
@@ -58,11 +60,11 @@ class CssContent extends TextContent {
         * @see TextContent::preSaveTransform
         */
        public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
-               global $wgParser;
                // @todo Make pre-save transformation optional for script pages
 
                $text = $this->getText();
-               $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+               $pst = MediaWikiServices::getInstance()->getParser()
+                       ->preSaveTransform( $text, $title, $user, $popts );
 
                return new static( $pst );
        }
index e637798..4804758 100644 (file)
@@ -25,6 +25,8 @@
  * @author Daniel Kinzler
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Content for JavaScript pages.
  *
@@ -56,12 +58,12 @@ class JavaScriptContent extends TextContent {
         * @return JavaScriptContent
         */
        public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
-               global $wgParser;
                // @todo Make pre-save transformation optional for script pages
                // See T34858
 
                $text = $this->getText();
-               $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+               $pst = MediaWikiServices::getInstance()->getParser()
+                       ->preSaveTransform( $text, $title, $user, $popts );
 
                return new static( $pst );
        }
index 750b958..71dd35c 100644 (file)
@@ -253,11 +253,12 @@ class TextContent extends AbstractContent {
        protected function fillParserOutput( Title $title, $revId,
                ParserOptions $options, $generateHtml, ParserOutput &$output
        ) {
-               global $wgParser, $wgTextModelsToParse;
+               global $wgTextModelsToParse;
 
                if ( in_array( $this->getModel(), $wgTextModelsToParse ) ) {
                        // parse just to get links etc into the database, HTML is replaced below.
-                       $output = $wgParser->parse( $this->getText(), $title, $options, true, true, $revId );
+                       $output = MediaWikiServices::getInstance()->getParser()
+                               ->parse( $this->getText(), $title, $options, true, true, $revId );
                }
 
                if ( $generateHtml ) {
index 3e2313c..455eb0d 100644 (file)
@@ -59,10 +59,9 @@ class WikitextContent extends TextContent {
         * @see Content::getSection()
         */
        public function getSection( $sectionId ) {
-               global $wgParser;
-
                $text = $this->getText();
-               $sect = $wgParser->getSection( $text, $sectionId, false );
+               $sect = MediaWikiServices::getInstance()->getParser()
+                       ->getSection( $text, $sectionId, false );
 
                if ( $sect === false ) {
                        return false;
@@ -109,9 +108,8 @@ class WikitextContent extends TextContent {
                        }
                } else {
                        # Replacing an existing section; roll out the big guns
-                       global $wgParser;
-
-                       $text = $wgParser->replaceSection( $oldtext, $sectionId, $text );
+                       $text = MediaWikiServices::getInstance()->getParser()
+                               ->replaceSection( $oldtext, $sectionId, $text );
                }
 
                $newContent = new static( $text );
@@ -147,10 +145,10 @@ class WikitextContent extends TextContent {
         * @return Content
         */
        public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
-               global $wgParser;
-
                $text = $this->getText();
-               $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+
+               $parser = MediaWikiServices::getInstance()->getParser();
+               $pst = $parser->preSaveTransform( $text, $title, $user, $popts );
 
                if ( $text === $pst ) {
                        return $this;
@@ -158,7 +156,7 @@ class WikitextContent extends TextContent {
 
                $ret = new static( $pst );
 
-               if ( $wgParser->getOutput()->getFlag( 'user-signature' ) ) {
+               if ( $parser->getOutput()->getFlag( 'user-signature' ) ) {
                        $ret->hadSignature = true;
                }
 
@@ -176,10 +174,9 @@ class WikitextContent extends TextContent {
         * @return Content
         */
        public function preloadTransform( Title $title, ParserOptions $popts, $params = [] ) {
-               global $wgParser;
-
                $text = $this->getText();
-               $plt = $wgParser->getPreloadText( $text, $title, $popts, $params );
+               $plt = MediaWikiServices::getInstance()->getParser()
+                       ->getPreloadText( $text, $title, $popts, $params );
 
                return new static( $plt );
        }
@@ -329,7 +326,7 @@ class WikitextContent extends TextContent {
 
        /**
         * Returns a ParserOutput object resulting from parsing the content's text
-        * using $wgParser.
+        * using the global Parser service.
         *
         * @param Title $title
         * @param int $revId Revision to pass to the parser (default: null)
@@ -341,8 +338,6 @@ class WikitextContent extends TextContent {
        protected function fillParserOutput( Title $title, $revId,
                        ParserOptions $options, $generateHtml, ParserOutput &$output
        ) {
-               global $wgParser;
-
                $stackTrace = ( new RuntimeException() )->getTraceAsString();
                if ( $this->previousParseStackTrace ) {
                        // NOTE: there may be legitimate changes to re-parse the same WikiText content,
@@ -366,7 +361,8 @@ class WikitextContent extends TextContent {
                $this->previousParseStackTrace = $stackTrace;
 
                list( $redir, $text ) = $this->getRedirectTargetAndText();
-               $output = $wgParser->parse( $text, $title, $options, true, true, $revId );
+               $output = MediaWikiServices::getInstance()->getParser()
+                       ->parse( $text, $title, $options, true, true, $revId );
 
                // Add redirect indicator at the top
                if ( $redir ) {
index eb1805c..3a9de62 100644 (file)
        "config-support-info": "MediaWiki prend en charge ces systèmes de bases de données :\n\n$1\n\nSi vous ne voyez pas le système de base de données que vous essayez d’utiliser ci-dessous, alors suivez les instructions ci-dessus (voir liens) pour activer la prise en charge.",
        "config-dbsupport-mysql": "* [{{int:version-db-mariadb-url}} MariaDB] est le premier choix pour MediaWiki et est le mieux pris en charge. MediaWiki fonctionne aussi avec [{{int:version-db-mysql-url}} MySQL] et [{{int:version-db-percona-url}} Percona Server], qui sont compatibles avec MariaDB. ([https://www.php.net/manual/en/mysqli.installation.php Comment compiler PHP avec la prise en charge de MySQL])",
        "config-dbsupport-postgres": "* [{{int:version-db-postgres-url}} PostgreSQL] est un système de base de données populaire en ''source ouverte'' qui peut être une alternative à MySQL ([https://www.php.net/manual/en/pgsql.installation.php Comment compiler PHP avec la prise en charge de PostgreSQL]).",
-       "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] est un système de base de données léger bien pris en charge ([https://www.php.net/manual/en/pdo.installation.php Comment compiler PHP avec la prise en charge de SQLite], en utilisant PDO).",
+       "config-dbsupport-sqlite": "* [{{int:version-db-sqlite-url}} SQLite] est un système de base de données léger bien pris en charge ([https://www.php.net/manual/en/pdo.installation.php Comment compiler PHP avec la prise en charge de SQLite], en utilisant PDO)",
        "config-dbsupport-oracle": "* [{{int:version-db-oracle-url}} Oracle] est un système commercial de gestion de base de données d’entreprise. ([https://www.php.net/manual/en/oci8.installation.php Comment compiler PHP avec la prise en charge d’OCI8])",
        "config-dbsupport-mssql": "* [{{int:version-db-mssql-url}} Microsoft SQL Server] est une base de données commerciale d’entreprise pour Windows. ([https://www.php.net/manual/en/sqlsrv.installation.php Comment compiler PHP avec la prise en charge de SQLSRV])",
        "config-header-mysql": "Paramètres de MariaDB/MySQL",
        "config-license-help": "Beaucoup de wikis publics mettent l’ensemble des contributions sous une [https://freedomdefined.org/Definition/Fr licence libre].\nCela contribue à créer un sentiment d’appartenance à une communauté et encourage les contributions sur le long terme.\nCe n’est généralement pas nécessaire pour un wiki privé ou d’entreprise.\n\nSi vous souhaitez utiliser des textes de Wikipédia, et souhaitez que Wikipédia puisse réutiliser des textes copiés depuis votre wiki, vous devriez choisir <strong>{{int:config-license-cc-by-sa}}</strong>.\n\nWikipédia utilisait auparavant la Licence de Documentation Libre GNU (GFDL).\nC’est une licence valide, mais difficile à comprendre. \nIl est aussi difficile de réutiliser du contenu sous la licence GFDL.",
        "config-email-settings": "Paramètres de courriel",
        "config-enable-email": "Activer les courriels sortants",
-       "config-enable-email-help": "Si vous souhaitez utiliser le courriel, vous devez avoir les [https://www.php.net/manual/en/mail.configuration.php paramètres courriel de PHP] configurés correctement (texte en anglais).\nSi vous ne voulez pas du service de courriel, vous pouvez le désactiver ici.",
+       "config-enable-email-help": "Si vous souhaitez utiliser le courriel, vous devez avoir configuré correctement les [https://www.php.net/manual/en/mail.configuration.php paramètres courriel de PHP] (texte en anglais).\nSi vous ne voulez pas du service de courriel, vous pouvez le désactiver ici.",
        "config-email-user": "Activer les courriers électroniques d'utilisateur à utilisateur",
        "config-email-user-help": "Permet à tous les utilisateurs d'envoyer des courriels à d'autres utilisateurs si cela est activé dans leurs préférences.",
        "config-email-usertalk": "Activer la notification des pages de discussion des utilisateurs",
index 3a8f2e1..4ea2eb9 100644 (file)
@@ -399,7 +399,7 @@ abstract class LBFactory implements ILBFactory {
                                $lbs[] = $lb;
                        } );
                        if ( !$lbs ) {
-                               return; // nothing actually used
+                               return true; // nothing actually used
                        }
                }
 
index c28d842..2764983 100644 (file)
@@ -22,6 +22,7 @@
  */
 use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\Linker\LinkRendererFactory;
+use MediaWiki\Linker\LinkTarget;
 use MediaWiki\MediaWikiServices;
 use MediaWiki\Special\SpecialPageFactory;
 use Wikimedia\ScopedCallback;
@@ -1980,7 +1981,7 @@ class Parser {
         * @since 1.21
         * @param string|bool $url Optional URL, to extract the domain from for rel =>
         *   nofollow if appropriate
-        * @param Title|null $title Optional Title, for wgNoFollowNsExceptions lookups
+        * @param LinkTarget|null $title Optional LinkTarget, for wgNoFollowNsExceptions lookups
         * @return string|null Rel attribute for $url
         */
        public static function getExternalLinkRel( $url = false, $title = null ) {
@@ -2589,8 +2590,9 @@ class Parser {
                 * Some of these require message or data lookups and can be
                 * expensive to check many times.
                 */
-               if ( Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] )
-                       && isset( $this->mVarCache[$index] )
+               if (
+                       Hooks::run( 'ParserGetVariableValueVarCache', [ &$parser, &$this->mVarCache ] ) &&
+                       isset( $this->mVarCache[$index] )
                ) {
                        return $this->mVarCache[$index];
                }
@@ -2598,24 +2600,6 @@ class Parser {
                $ts = wfTimestamp( TS_UNIX, $this->mOptions->getTimestamp() );
                Hooks::run( 'ParserGetVariableValueTs', [ &$parser, &$ts ] );
 
-               // In miser mode, disable words that always cause double-parses on page save (T137900)
-               static $slowRevWords = [ 'revisionid' => true ]; // @TODO: 'revisiontimestamp'
-               if (
-                       isset( $slowRevWords[$index] ) &&
-                       $this->siteConfig->get( 'MiserMode' ) &&
-                       !$this->mOptions->getInterfaceMessage() &&
-                       // @TODO: disallow this word on all namespaces
-                       $this->nsInfo->isContent( $this->mTitle->getNamespace() )
-               ) {
-                       if ( $this->mRevisionId || $this->mOptions->getSpeculativeRevId() ) {
-                               return '-';
-                       } else {
-                               $this->mOutput->setFlag( 'vary-revision-exists' );
-
-                               return '';
-                       }
-               };
-
                $pageLang = $this->getFunctionLang();
 
                switch ( $index ) {
@@ -2739,23 +2723,35 @@ class Parser {
                                $value = $pageid ?: null;
                                break;
                        case 'revisionid':
-                               # Let the edit saving system know we should parse the page
-                               # *after* a revision ID has been assigned.
-                               $this->mOutput->setFlag( 'vary-revision-id' );
-                               wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
-                               $value = $this->mRevisionId;
-
-                               if ( !$value ) {
-                                       $rev = $this->getRevisionObject();
-                                       if ( $rev ) {
-                                               $value = $rev->getId();
+                               if (
+                                       $this->siteConfig->get( 'MiserMode' ) &&
+                                       !$this->mOptions->getInterfaceMessage() &&
+                                       // @TODO: disallow this word on all namespaces
+                                       $this->nsInfo->isContent( $this->mTitle->getNamespace() )
+                               ) {
+                                       // Use a stub result instead of the actual revision ID in order to avoid
+                                       // double parses on page save but still allow preview detection (T137900)
+                                       if ( $this->getRevisionId() || $this->mOptions->getSpeculativeRevId() ) {
+                                               $value = '-';
+                                       } else {
+                                               $this->mOutput->setFlag( 'vary-revision-exists' );
+                                               $value = '';
                                        }
-                               }
-
-                               if ( !$value ) {
-                                       $value = $this->mOptions->getSpeculativeRevId();
-                                       if ( $value ) {
-                                               $this->mOutput->setSpeculativeRevIdUsed( $value );
+                               } else {
+                                       # Inform the edit saving system that getting the canonical output after
+                                       # revision insertion requires another parse using the actual revision ID
+                                       $this->mOutput->setFlag( 'vary-revision-id' );
+                                       wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision-id...\n" );
+                                       $value = $this->getRevisionId();
+                                       if ( $value === 0 ) {
+                                               $rev = $this->getRevisionObject();
+                                               $value = $rev ? $rev->getId() : $value;
+                                       }
+                                       if ( !$value ) {
+                                               $value = $this->mOptions->getSpeculativeRevId();
+                                               if ( $value ) {
+                                                       $this->mOutput->setSpeculativeRevIdUsed( $value );
+                                               }
                                        }
                                }
                                break;
@@ -4672,7 +4668,7 @@ class Parser {
         * If you have pre-fetched the nickname or the fancySig option, you can
         * specify them here to save a database query.
         * Do not reuse this parser instance after calling getUserSig(),
-        * as it may have changed if it's the $wgParser.
+        * as it may have changed.
         *
         * @param User &$user
         * @param string|bool $nickname Nickname to use or false to use user's default nickname
@@ -5850,6 +5846,11 @@ class Parser {
        /**
         * Get the ID of the revision we are parsing
         *
+        * The return value will be either:
+        *   - a) Positive, indicating a specific revision ID (current or old)
+        *   - b) Zero, meaning the revision ID specified by getCurrentRevisionCallback()
+        *   - c) Null, meaning the parse is for preview mode and there is no revision
+        *
         * @return int|null
         */
        public function getRevisionId() {
@@ -6359,9 +6360,9 @@ class Parser {
        /**
         * Return this parser if it is not doing anything, otherwise
         * get a fresh parser. You can use this method by doing
-        * $myParser = $wgParser->getFreshParser(), or more simply
-        * $wgParser->getFreshParser()->parse( ... );
-        * if you're unsure if $wgParser is safe to use.
+        * $newParser = $oldParser->getFreshParser(), or more simply
+        * $oldParser->getFreshParser()->parse( ... );
+        * if you're unsure if $oldParser is safe to use.
         *
         * @since 1.24
         * @return Parser A parser object that is not parsing anything
index 8d60e0f..3371069 100644 (file)
@@ -255,17 +255,6 @@ class ResourceLoader implements LoggerAwareInterface {
                // Special module that always exists
                $this->register( 'startup', [ 'class' => ResourceLoaderStartUpModule::class ] );
 
-               // Register extension modules
-               $this->register( $config->get( 'ResourceModules' ) );
-
-               // Avoid PHP 7.1 warning from passing $this by reference
-               $rl = $this;
-               Hooks::run( 'ResourceLoaderRegisterModules', [ &$rl ] );
-
-               if ( $config->get( 'EnableJavaScriptTest' ) === true ) {
-                       $this->registerTestModules();
-               }
-
                $this->setMessageBlobStore( new MessageBlobStore( $this, $this->logger ) );
        }
 
@@ -394,6 +383,9 @@ class ResourceLoader implements LoggerAwareInterface {
                }
        }
 
+       /**
+        * @internal For use by ServiceWiring only
+        */
        public function registerTestModules() {
                global $IP;
 
index 276d9a1..4c11fce 100644 (file)
@@ -144,15 +144,18 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
        }
 
        /**
-        * Get the Database object used in getTitleInfo().
+        * Get the Database handle used for computing the module version.
         *
-        * Defaults to the local replica DB. Subclasses may want to override this to return a foreign
-        * database object, or null if getTitleInfo() shouldn't access the database.
+        * Subclasses may override this to return a foreign database, which would
+        * allow them to register a module on wiki A that fetches wiki pages from
+        * wiki B.
         *
-        * NOTE: This ONLY works for getTitleInfo() and isKnownEmpty(), NOT FOR ANYTHING ELSE.
-        * In particular, it doesn't work for getContent() or getScript() etc.
+        * The way this works is that the local module is a placeholder that can
+        * only computer a module version hash. The 'source' of the module must
+        * be set to the foreign wiki directly. Methods getScript() and getContent()
+        * will not use this handle and are not valid on the local wiki.
         *
-        * @return IDatabase|null
+        * @return IDatabase
         */
        protected function getDB() {
                return wfGetDB( DB_REPLICA );
@@ -379,10 +382,6 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
         */
        protected function getTitleInfo( ResourceLoaderContext $context ) {
                $dbr = $this->getDB();
-               if ( !$dbr ) {
-                       // We're dealing with a subclass that doesn't have a DB
-                       return [];
-               }
 
                $pageNames = array_keys( $this->getPages( $context ) );
                sort( $pageNames );
@@ -462,8 +461,8 @@ class ResourceLoaderWikiModule extends ResourceLoaderModule {
                        $module = $rl->getModule( $name );
                        if ( $module instanceof self ) {
                                $mDB = $module->getDB();
-                               // Subclasses may disable getDB and implement getTitleInfo differently
-                               if ( $mDB && $mDB->getDomainID() === $db->getDomainID() ) {
+                               // Subclasses may implement getDB differently
+                               if ( $mDB->getDomainID() === $db->getDomainID() ) {
                                        $wikiModules[] = $module;
                                        $allPages += $module->getPages( $context );
                                }
index 781fc33..def3bc3 100644 (file)
@@ -141,9 +141,9 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI
         * unless only max-priority makes sense.
         *
         * @warning This will be called early in the MediaWiki setup process,
-        *  before $wgUser, $wgLang, $wgOut, $wgParser, $wgTitle, and corresponding
-        *  pieces of the main RequestContext are set up! If you try to use these,
-        *  things *will* break.
+        *  before $wgUser, $wgLang, $wgOut, $wgTitle, the global parser, and
+        *  corresponding pieces of the main RequestContext are set up! If you try
+        *  to use these, things *will* break.
         * @note The SessionProvider must not attempt to auto-create users.
         *  MediaWiki will do this later (when it's safe) if the chosen session has
         *  a user with a valid name but no ID.
@@ -469,7 +469,7 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI
         * @note If self::__toString() is overridden, this will likely need to be
         *  overridden as well.
         * @warning This will be called early during MediaWiki startup. Do not
-        *  use $wgUser, $wgLang, $wgOut, $wgParser, or their equivalents via
+        *  use $wgUser, $wgLang, $wgOut, the global Parser, or their equivalents via
         *  RequestContext from this method!
         * @return \Message
         */
index 9ea5e08..ceba987 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * A special page that expands submitted templates, parser functions,
  * and variables, allowing easier debugging of these.
@@ -53,8 +55,6 @@ class SpecialExpandTemplates extends SpecialPage {
         * @param string|null $subpage
         */
        function execute( $subpage ) {
-               global $wgParser;
-
                $this->setHeaders();
                $this->addHelpLink( 'Help:ExpandTemplates' );
 
@@ -77,9 +77,10 @@ class SpecialExpandTemplates extends SpecialPage {
                        $options->setTidy( true );
                        $options->setMaxIncludeSize( self::MAX_INCLUDE_SIZE );
 
+                       $parser = MediaWikiServices::getInstance()->getParser();
                        if ( $this->generateXML ) {
-                               $wgParser->startExternalParse( $title, $options, Parser::OT_PREPROCESS );
-                               $dom = $wgParser->preprocessToDom( $input );
+                               $parser->startExternalParse( $title, $options, Parser::OT_PREPROCESS );
+                               $dom = $parser->preprocessToDom( $input );
 
                                if ( method_exists( $dom, 'saveXML' ) ) {
                                        $xml = $dom->saveXML();
@@ -88,7 +89,7 @@ class SpecialExpandTemplates extends SpecialPage {
                                }
                        }
 
-                       $output = $wgParser->preprocess( $input, $title, $options );
+                       $output = $parser->preprocess( $input, $title, $options );
                } else {
                        $this->removeComments = $request->getBool( 'wpRemoveComments', true );
                        $this->removeNowiki = $request->getBool( 'wpRemoveNowiki', false );
@@ -246,11 +247,9 @@ class SpecialExpandTemplates extends SpecialPage {
         * @return ParserOutput
         */
        private function generateHtml( Title $title, $text ) {
-               global $wgParser;
-
                $popts = ParserOptions::newFromContext( $this->getContext() );
                $popts->setTargetLanguage( $title->getPageLanguage() );
-               return $wgParser->parse( $text, $title, $popts );
+               return MediaWikiServices::getInstance()->getParser()->parse( $text, $title, $popts );
        }
 
        /**
index 2632092..c4dd6e3 100644 (file)
@@ -23,6 +23,8 @@
  * @ingroup SpecialPage
  */
 
+use MediaWiki\MediaWikiServices;
+
 /**
  * Give information about the version of MediaWiki, PHP, the DB and extensions
  *
@@ -555,9 +557,7 @@ class SpecialVersion extends SpecialPage {
         * @return string HTML output
         */
        protected function getParserTags() {
-               global $wgParser;
-
-               $tags = $wgParser->getTags();
+               $tags = MediaWikiServices::getInstance()->getParser()->getTags();
 
                if ( count( $tags ) ) {
                        $out = Html::rawElement(
@@ -599,9 +599,7 @@ class SpecialVersion extends SpecialPage {
         * @return string HTML output
         */
        protected function getParserFunctionHooks() {
-               global $wgParser;
-
-               $fhooks = $wgParser->getFunctionHooks();
+               $fhooks = MediaWikiServices::getInstance()->getParser()->getFunctionHooks();
                if ( count( $fhooks ) ) {
                        $out = Html::rawElement(
                                'h2',
index a9bbc20..1b5580c 100644 (file)
@@ -2713,7 +2713,7 @@ class Language {
        public function uc( $str, $first = false ) {
                if ( $first ) {
                        if ( $this->isMultibyte( $str ) ) {
-                               return mb_strtoupper( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
+                               return $this->mbUpperChar( mb_substr( $str, 0, 1 ) ) . mb_substr( $str, 1 );
                        } else {
                                return ucfirst( $str );
                        }
@@ -2722,6 +2722,28 @@ class Language {
                }
        }
 
+       /**
+        * Convert character to uppercase, allowing overrides of the default mb_upper
+        * behaviour, which is buggy in many ways. Having a conversion table can be
+        * useful during transitions between PHP versions where unicode changes happen.
+        * This can make some resources unreachable on-wiki, see discussion at T219279.
+        * Providing such a conversion table can allow to manage the transition period.
+        *
+        * @since 1.34
+        *
+        * @param string $char
+        *
+        * @return string
+        */
+       protected function mbUpperChar( $char ) {
+               global $wgOverrideUcfirstCharacters;
+               if ( array_key_exists( $char, $wgOverrideUcfirstCharacters ) ) {
+                       return $wgOverrideUcfirstCharacters[$char];
+               } else {
+                       return mb_strtoupper( $char );
+               }
+       }
+
        /**
         * @param string $str
         * @return mixed|string
index f07fbd3..0a36627 100644 (file)
        "histfirst": "اول",
        "histlast": "آخر",
        "historysize": "({{PLURAL:$1|1 بايت|$1 بايت}})",
-       "historyempty": "(فاضى)",
+       "historyempty": "فاضى",
        "history-feed-title": "تاريخ المراجعة",
        "history-feed-description": "تاريخ التعديل بتاع الصفحة دى على الويكي",
        "history-feed-item-nocomment": "$1 فى $2",
index 77a312a..5104364 100644 (file)
        "rcfilters-watchlist-markseen-button": "Marcar tolos cambios como vistos",
        "rcfilters-watchlist-edit-watchlist-button": "Edita la to llista de páxines siguíes",
        "rcfilters-watchlist-showupdated": "Los cambeos fechos en páxines que nun visitasti desque se ficieron apaecen en <strong>negrina</strong>, con marcadores sólidos.",
-       "rcfilters-preference-label": "Tapecer la versión meyorada de Cambios recién",
+       "rcfilters-preference-label": "Usar la interfaz ensin JavaScript",
        "rcfilters-preference-help": "Revierte'l rediseñu de la interfaz de 2017 y toles ferramientes añadíes d'entós aquí.",
        "rcfilters-watchlist-preference-label": "Tapecer la versión ameyorada de la Llista de siguimientu",
        "rcfilters-watchlist-preference-help": "Desfai el rediseñu de la interfaz de 2017 y toles ferramientes añadíes d'entós acá.",
index 77477a0..33eef77 100644 (file)
        "action-autoconfirmed": "адсутнасьць абмежаваньня хуткасьці паводле IP-адрасу",
        "action-bigdelete": "выдаленьне старонак зь вялікай гісторыяй",
        "action-blockemail": "блякаваньне ўдзельніку магчымасьці адпраўкі лістоў электроннай поштай",
+       "action-bot": "тое, каб лічыцца аўтаматычным працэсам",
        "nchanges": "$1 {{PLURAL:$1|зьмена|зьмены|зьменаў}}",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|з апошняга візыту}}",
        "enhancedrc-history": "гісторыя",
index b0ad6c0..e3839a7 100644 (file)
        "exif-compression-4": "CCITT Group 4, факс куодтааһына",
        "exif-copyrighted-true": "Ааптар быраабынан араҥаччыланар",
        "exif-copyrighted-false": "Бас билиитэ чопчуламматах",
+       "exif-photometricinterpretation-0": "Хара уонна маҥан (маҥан - 0).",
        "exif-photometricinterpretation-1": "Хара уонна маҥан (хара - 0).",
+       "exif-photometricinterpretation-3": "Өҥ палитрата",
+       "exif-photometricinterpretation-4": "Дьэҥкир мааска",
+       "exif-photometricinterpretation-5": "Хайытыллыбыт (арааһа CMYK)",
+       "exif-photometricinterpretation-8": "CIE L*a*b*",
+       "exif-photometricinterpretation-9": "CIE L*a*b* (ICC-кодирование)",
+       "exif-photometricinterpretation-10": "CIE L*a*b* (ITU-кодирование)",
        "exif-unknowndate": "Күнэ-ыйа биллибэт",
        "exif-orientation-1": "Нуорма",
        "exif-orientation-2": "Сытыары көстүбүт",
index 915d9a0..b046b83 100644 (file)
        "page_first": "نخست",
        "page_last": "واپسین",
        "histlegend": "انتخاب تفاوت: دکمه‌های گرد کنار ویرایش‌هایی که می‌خواهید با هم مقایسه کنید را علامت بزنید و دکمهٔ Enter را بزنید یا دکمهٔ پایین را فشار دهید.<br />\nاختصارات: '''({{int:cur}})''' = تفاوت با نسخهٔ فعلی، '''({{int:last}})''' = تفاوت با نسخهٔ قبلی، '''({{int:minoreditletter}})''' = ویرایش جزئی.",
-       "history-fieldset-title": "جستجو برای نسخه‌ها",
+       "history-fieldset-title": "فیلتر کردن نسخه‌ها",
        "history-show-deleted": "فقط نسخه‌های حذف شده",
        "histfirst": "قدیمی‌ترین",
        "histlast": "جدیدترین",
        "action-changetags": "افزودن یا حذف برچسب قراردادی بر روی نسخه یا سیاهه ورودی‌ها",
        "action-deletechangetags": "حذف برچسب‌ها از پایگاه داده",
        "action-purge": "خالی‌کردن میانگیر این صفحه",
+       "action-apihighlimits": "افزایش محدودیت‌ها برای پرسمان‌های رابط برنامه‌نویسی",
+       "action-autoconfirmed": "از محدودیت‌های سرعت آی‌پی‌-محور تاثیر نمی‌گیرد",
+       "action-bigdelete": "حذف صفحه‌های دارای تاریخچهٔ بزرگ",
+       "action-blockemail": "قطع دسترسی دیگر کاربران برای ارسال ایمیل",
+       "action-bot": "تلقی‌شده به عنوان یک فرآیند خودکار",
+       "action-editprotected": "ویرایش صفحه‌های محافظت‌شده به عنوان «{{int:protect-level-sysop}}»",
+       "action-editsemiprotected": "ویرایش صفحه محافظت‌شده به عنوان «{{int:protect-level-autoconfirmed}}»",
+       "action-editinterface": "ویرایش واسط کاربری",
+       "action-editusercss": "ویرایش صفحه‌های CSS دیگر کاربرها",
+       "action-edituserjson": "ویرایش پرونده‌های JSON دیگر کاربرها",
+       "action-edituserjs": "ویرایش صفحه‌های JS دیگر کاربرها",
+       "action-editsitecss": "ویرایش گسترده CSS وب‌گاه",
+       "action-editsitejson": "ویرایش گسترده JSON وب‌گاه",
+       "action-editsitejs": "ویرایش گسترده JavaScript وب‌گاه",
+       "action-editmyusercss": "پرونده‌های سی‌اس‌اس کاربری خود را ویرایش کنید",
+       "action-editmyuserjson": "پرونده‌های JSON کاربری خود را ویرایش کنید",
+       "action-editmyuserjs": "پرونده‌های جاوااسکریپت کاربری خود را ویرایش کنید",
+       "action-viewsuppressed": "مشاهده نسخه‌هایی که از کاربران مخفی شده‌اند",
+       "action-hideuser": "قطع دسترسی کاربر و پنهان کردن آن از دید عموم",
+       "action-ipblock-exempt": "تأثیر نپذیرفتن از قطع دسترسی‌های آی‌پی، خودکار یا فاصله‌ای",
+       "action-unblockself": "بازکردن دسترسی خود",
+       "action-noratelimit": "تاثیر نپذیرفتن از محدودیت سرعت",
+       "action-reupload-own": "بارگذاری دوبارهٔ پرونده‌ای که پیش از این توسط همان کاربر بارگذاری شده‌است",
+       "action-nominornewtalk": "ویرایش جزئی صفحه‌های بحث به شکلی که باعث اعلان پیغام تازه نشود",
+       "action-markbotedits": "علامت زدن ویرایش‌های واگردانی‌شده به عنوان ویرایش ربات",
+       "action-patrolmarks": "مشاهدهٔ برچسب گشت تغییرات اخیر",
+       "action-override-export-depth": "برون‌بری صفحه‌ها شامل صفحه‌های پیوند شده تا عمق ۵",
+       "action-suppressredirect": "انتقال صفحه بدون ایجاد تغییرمسیر از نام قبلی",
        "nchanges": "$1 تغییر",
        "enhancedrc-since-last-visit": "$1 {{PLURAL:$1|از آخرین بازدید}}",
        "enhancedrc-history": "تاریخچه",
        "delete-confirm": "حذف «$1»",
        "delete-legend": "حذف",
        "historywarning": "<strong>هشدار:</strong> صفحه‌ای که در حال پاک کردن آن هستید دارای یک تاریخچه همراه با $1 {{PLURAL:$1|بازبینی|بازبینی}} است:",
-       "historyaction-submit": "نمایش",
+       "historyaction-submit": "نمایش نسخه‌ها",
        "confirmdeletetext": "شما در حال حذف کردن یک صفحه یا تصویر از پایگاه‌های داده همراه با تمام تاریخچهٔ آن هستید.\nلطفاً این عمل را تأیید کنید و اطمینان حاصل کنید که عواقب این کار را می‌دانید و این عمل را مطابق با [[{{MediaWiki:Policy-url}}|سیاست‌ها]] انجام می‌دهید.",
        "actioncomplete": "عمل انجام شد",
        "actionfailed": "عمل ناموفق بود",
index fa30514..322d72b 100644 (file)
        "action-apihighlimits": "käyttää korkeampia rajoja API-kyselyissä",
        "action-bigdelete": "poistaa sivuja, joilla on pitkä historia",
        "action-blockemail": "estää käyttäjää lähettämästä sähköpostia",
+       "action-editprotected": "muokata sivuja, jotka on suojattu tasolle ”{{int:protect-level-sysop}}”",
+       "action-editsemiprotected": "muokata sivuja, jotka on suojattu tasolle ”{{int:protect-level-autoconfirmed}}”",
        "action-editusercss": "muokata toisten käyttäjien CSS-tiedostoja",
        "action-edituserjson": "muokata toisten käyttäjien JSON-tiedostoja",
        "action-edituserjs": "muokata toisten käyttäjien JavaScript-tiedostoja",
index a172ef7..0b1d943 100644 (file)
        "action-changetags": "ajouter et supprimer de façon arbitraire des balises sur des révisions individuelles et des entrées de journal",
        "action-deletechangetags": "supprimer des balises de la base de données",
        "action-purge": "purger cette page",
-       "action-apihighlimits": "utiliser de limites plus élevées dans les requêtes à l’API",
+       "action-apihighlimits": "utiliser des limites plus élevées dans les requêtes à l’API",
        "action-autoconfirmed": "ne pas être impacté par les limites de taux basées sur l’IP",
        "action-bigdelete": "supprimer des pages avec de grands historiques",
        "action-blockemail": "empêcher un utilisateur d’envoyer des courriels",
        "action-ipblock-exempt": "contourner les blocages d’IP, blocages automatiques et blocages de plages d’IP",
        "action-unblockself": "vous débloquer vous-même",
        "action-noratelimit": "ne pas être impacté par les limites de taux",
-       "action-reupload-own": "écraser des fichiers que vous avez vous-même importés",
+       "action-reupload-own": "écraser les fichiers existants que vous avez vous-même téléversés",
        "action-nominornewtalk": "ne pas déclencher la notification de nouveau message lors d’une modification mineure sur une pages de discussion",
        "action-markbotedits": "marquer des modifications révoquées comme ayant été faites par un robot",
        "action-patrolmarks": "voir les indications de relecture dans les modifications récentes",
index 615df0b..612276b 100644 (file)
        "rcfilters-filter-editsbyself-label": "Wizigings fan jo",
        "rcfilters-filter-editsbyself-description": "Jo eigen bydragen.",
        "rcfilters-filter-editsbyother-label": "Wizigings fan oaren",
-       "rcfilters-filter-editsbyother-description": "Alle wizigings útsein jo eigen.",
+       "rcfilters-filter-editsbyother-description": "Alle wizigings, útsein jo eigen.",
        "rcfilters-filtergroup-userExpLevel": "Meidoggerynskriuwing en bedreaunens",
        "rcfilters-filter-user-experience-level-registered-label": "Ynskreaun",
        "rcfilters-filter-user-experience-level-registered-description": "Oanmelde bewurkers.",
        "rcfilters-filter-user-experience-level-unregistered-label": "Net-ynskreaun",
        "rcfilters-filter-user-experience-level-unregistered-description": "Bewurkers dy't net oanmeld binne.",
        "rcfilters-filter-user-experience-level-newcomer-label": "Nijkommers",
+       "rcfilters-filter-user-experience-level-newcomer-description": "Ynskreaune bewurkers dy't minder hawwe as 10 bewurkings of 4 dagen dwaande.",
        "rcfilters-filter-user-experience-level-learner-label": "Learders",
+       "rcfilters-filter-user-experience-level-learner-description": "Ynskreaune bewurkers waans ûnderfining falt tusken \"Nijkommers\" en \"Bedreaune meidoggers\".",
        "rcfilters-filter-user-experience-level-experienced-label": "Bedreaune meidoggers",
+       "rcfilters-filter-user-experience-level-experienced-description": "Ynskreaune bewurkers mei mear as 500 bewurkings en 30 dagen dwaande.",
        "rcfilters-filtergroup-automated": "Automatisearre bydragen",
        "rcfilters-filter-bots-description": "Wizigings makke troch automatisearre helpmiddels.",
        "rcfilters-filter-humans-label": "Minske (gjin bot)",
        "rcfilters-filter-major-description": "Feroarings net lebele as fan lytse betsjutting.",
        "rcfilters-filtergroup-watchlist": "Folchlistsiden",
        "rcfilters-filter-watchlist-watched-label": "Op 'e folchlist",
+       "rcfilters-filter-watchlist-watched-description": "Wizigings oan siden op jo folchlist.",
        "rcfilters-filter-watchlist-watchednew-label": "Nije folchlistwizigings",
+       "rcfilters-filter-watchlist-watchednew-description": "Wizigings oan folchlistsiden dy't jo dêrnei noch net besocht hawwe.",
        "rcfilters-filter-watchlist-notwatched-label": "Net op 'e folchlist",
+       "rcfilters-filter-watchlist-notwatched-description": "Alles, útsein wizigings oan jo folchlistsiden.",
        "rcfilters-filtergroup-changetype": "Wizigingssoarte",
        "rcfilters-filter-pageedits-label": "Sidebewurkings",
+       "rcfilters-filter-pageedits-description": "Wizigings oan wikiynhâld, diskusjes, kategorybeskriuwings …",
        "rcfilters-filter-newpages-label": "Nije siden",
+       "rcfilters-filter-newpages-description": "Wizigings wêrmei't nije siden oanmakke wurde.",
        "rcfilters-filter-categorization-label": "Kategoryferoarings",
+       "rcfilters-filter-categorization-description": "Ynfo oer siden taheakke oan, as weihelle út kategoryen.",
        "rcfilters-filter-logactions-label": "Lochaksjes",
+       "rcfilters-filter-logactions-description": "Administrative hannelings, akkounts oanmeitsjen, siden wiskjen, bestannen opladen …",
        "rcfilters-filtergroup-lastRevision": "Lêste ferzjes",
        "rcfilters-filter-lastrevision-label": "Lêste ferzje",
+       "rcfilters-filter-lastrevision-description": "Allinnich de resintste wiziging fan in side.",
        "rcfilters-filter-previousrevision-label": "Net de lêste ferzje",
+       "rcfilters-filter-previousrevision-description": "Alle wizigings dy't net de \"lêste ferzje\" binne.",
        "rcfilters-filter-excluded": "Utsein",
        "rcfilters-exclude-button-off": "Seleksje omkeare",
        "rcfilters-exclude-button-on": "Omkearde seleksje",
        "recentchangeslinked-summary": "Dizze spesjale side lit de lêste bewurkings sjen op siden dy't keppele wurde fan in spesifisearre side ôf (of fan in spesifisearre Kategory ôf). Siden dy't op [[Special:Watchlist|jo folchlist]] steane, wurde '''tsjûk''' werjûn.",
        "recentchangeslinked-page": "Sidenamme:",
        "recentchangeslinked-to": "Feroarings oan siden mei ferwizings nei dizze side besjen",
+       "recentchanges-page-added-to-category": "[[:$1]] oan kategory taheakke",
+       "recentchanges-page-added-to-category-bundled": "[[:$1]] oan kategory taheakke; [[Special:WhatLinksHere/$1|dizze side is opnommen yn oare siden]]",
+       "recentchanges-page-removed-from-category": "[[:$1]] út kategory weihelle",
+       "recentchanges-page-removed-from-category-bundled": "[[:$1]] út kategory weihelle; [[Special:WhatLinksHere/$1|dizze side is opnommen yn oare siden]]",
        "upload": "Bied bestân oan",
        "uploadbtn": "Bied bestân oan",
        "reuploaddesc": "Opladen annulearje en weromgean nei it oanbiedformulier",
index 4785e11..81ac629 100644 (file)
        "prefs-help-gender": "Postavljanje ove mogućnosti je opcionalno.\nProgramska oprema koristi danu vrijednost kako bi Vam se obratila i spomenula Vas drugima rabeći odgovarajući gramatički rod.\nOvaj podatak bit će javno dostupan.",
        "email": "Adresa elektroničke pošte *",
        "prefs-help-realname": "Pravo ime nije obvezno. Ako ga navedete, može biti rabljeno za pripisivanje Vaših doprinosa.",
-       "prefs-help-email": "E-mail adresa nije obvezna, ali je potrebna za obnovu lozinke u slučaju da ju zaboravite.",
-       "prefs-help-email-others": "Također možete odabrati da vas ostali kontaktiraju preko vaše suradničke ili stranice za razgovor bez javnog otkrivanja vašeg identiteta.",
+       "prefs-help-email": "Adresa e-pošte nije obvezna, ali je potrebna u slučaju ponovnih postavljanja zaporke, ako zaboravite Vašu zaporku.",
+       "prefs-help-email-others": "Također možete dopustiti drugima da Vas kontaktiraju preko poveznice na lijevoj strani Vaše stranice odnosno stranice za razgovor.\nVaša adresa e-pošte ne će biti prikazana drugim suradnicima koji Vas kontaktiraju.",
        "prefs-help-email-required": "Potrebno je navesti adresu e-pošte (e-mail).",
        "prefs-info": "Osnovni podatci",
        "prefs-i18n": "Internacionalizacija",
index d4b3e7f..6266c29 100644 (file)
        "page_first": "éischt",
        "page_last": "lescht",
        "histlegend": "Fir d'Ännerungen unzeweisen: Klickt déi zwou Versiounen un, déi solle verglach ginn.<br />\n*(aktuell) = Ënnerscheed mat der aktueller Versioun,\n*(lescht) = Ënnerscheed mat der aler Versioun,\n*k = Kleng Ännerung.",
-       "history-fieldset-title": "No Versioune sichen",
+       "history-fieldset-title": "Versioune filteren",
        "history-show-deleted": "Nëmme geläscht Versiounen",
        "histfirst": "eelst",
        "histlast": "neist",
index 64ec265..96ae0dd 100644 (file)
        "suppress": "ระงับ",
        "querypage-disabled": "หน้าพิเศษนี้ถูกปิดใช้งานด้วยเหตุผลด้านสมรรถภาพ",
        "apihelp-no-such-module": "ไม่พบมอดูล \"$1\"",
+       "apisandbox": "ทดลองเขียนเอพีไอ",
        "apisandbox-api-disabled": "ไซต์นี้ไม่เปิดใช้ API",
        "apisandbox-submit": "ส่งคำขอ",
        "apisandbox-reset": "ล้าง",
        "logentry-contentmodel-change-revert": "ย้อน",
        "protectlogpage": "ปูมการป้องกัน",
        "protectlogtext": "ด้านล่างเป็นรายการการเปลี่ยนแปลงการล็อกหน้า\nดู[[Special:ProtectedPages|รายการหน้าที่ถูกล็อก]]สำหรับการล็อกหน้าที่มีผลอยู่ในปัจจุบัน",
-       "protectedarticle": "à¹\84à¸\94à¹\89à¸\9bà¹\89อà¸\87à¸\81ัà¸\99 \"[[$1]]\"",
+       "protectedarticle": "ป้องกัน \"[[$1]]\"",
        "modifiedarticleprotection": "เปลี่ยนระดับการล็อกของ \"[[$1]]\"",
        "unprotectedarticle": "ยกเลิกการล็อกจาก \"[[$1]]\"",
-       "movedarticleprotection": "ยà¹\89ายà¸\81ารà¸\95ัà¹\89à¸\87à¸\84à¹\88าà¸\81ารลà¹\87อà¸\81จาก \"[[$2]]\" ไป \"[[$1]]\"",
+       "movedarticleprotection": "ยà¹\89ายà¸\81ารà¸\95ัà¹\89à¸\87à¸\84à¹\88าà¸\81ารà¸\9bà¹\89อà¸\87à¸\81ัà¸\99จาก \"[[$2]]\" ไป \"[[$1]]\"",
        "protectedarticle-comment": "ป้องกัน \"[[$1]]\"",
        "modifiedarticleprotection-comment": "{{GENDER:$2|}}เปลี่ยนระดับการล็อกสำหรับ \"[[$1]]\"",
        "unprotectedarticle-comment": "{{GENDER:$2|ยกเลิกการป้องกัน}}จาก \"[[$1]]\"",
        "tag-list-wrapper": "[[Special:Tags|{{PLURAL:$1|Tag|ป้ายระบุ}}]]: $2",
        "tag-mw-new-redirect": "เปลี่ยนทางใหม่",
        "tag-mw-new-redirect-description": "การแก้ไขที่สร้างหน้าเปลี่ยนทางใหม่หรือเปลี่ยนแปลงหน้าเป็นหน้าเปลี่ยนทาง",
-       "tag-mw-removed-redirect": "ลà¸\9aหà¸\99à¹\89าà¹\80à¸\9bลà¹\88ีà¹\88ยà¸\99à¸\97าà¸\87",
+       "tag-mw-removed-redirect": "ลบหน้าเปลี่ยนทาง",
        "tag-mw-removed-redirect-description": "การแก้ไขี่เปลี่ยนหน้าเปลีี่ยนทางเดิมให้มิใช่หน้าเปลี่ยนทาง",
        "tag-mw-changed-redirect-target": "เปลี่ยนเป้าหมายหน้าเปลี่ยนทาง",
        "tag-mw-changed-redirect-target-description": "การแก้ไขที่เปลี่ยนเป้าหมายของหน้าเปลี่ยนทาง",
        "logentry-newusers-autocreate": "บัญชีผู้ใช้ $1 ถูกสร้างขึ้นอัตโนมัติ",
        "logentry-protect-move_prot": "$1 ย้ายการตั้งค่าการล็อกจาก $4 ไป $3",
        "logentry-protect-unprotect": "$1 ลบการล็อกจาก $3",
-       "logentry-protect-protect": "$1 {{GENDER:$2|à¹\84à¸\94à¹\89à¸\9bà¹\89อà¸\87à¸\81ัà¸\99}} $3 $4",
-       "logentry-protect-protect-cascade": "$1 {{GENDER:$2|à¹\84à¸\94à¹\89à¸\9bà¹\89อà¸\87à¸\81ัà¸\99}} $3 $4 [à¸\95à¹\88อà¹\80รียà¸\87]",
+       "logentry-protect-protect": "$1 {{GENDER:$2|ป้องกัน}} $3 $4",
+       "logentry-protect-protect-cascade": "$1 {{GENDER:$2|ป้องกัน}} $3 $4 [ต่อเรียง]",
        "logentry-protect-modify": "$1 เปลี่ยนระดับการตั้งค่าสำหรับ $3 $4",
        "logentry-protect-modify-cascade": "$1 เปลี่ยนระดับการตั้งค่าสำหรับ $3 $4 [ต่อเรียง]",
        "logentry-rights-rights": "$1 {{GENDER:$2|เปลี่ยน}}กลุ่มสมาชิกของ $3 จาก $4 เป็น $5",
index 9be74fd..157d33b 100644 (file)
        "ipb-confirm": "确认封禁",
        "ipb-sitewide": "全站范围",
        "ipb-partial": "部分的",
-       "ipb-sitewide-help": "在 wiki 上的各个页面以及其它贡献行为。",
+       "ipb-sitewide-help": "在维基上的所有页面以及其它贡献行为。",
        "ipb-partial-help": "特殊页面或名字空间。",
        "ipb-pages-label": "页面",
        "ipb-namespaces-label": "名字空间",
index 0d376f5..eb99f3e 100644 (file)
        "ipb-confirm": "確認封鎖",
        "ipb-sitewide": "全站範圍",
        "ipb-partial": "部分",
-       "ipb-sitewide-help": "在 wiki 上的各個頁面以及其它貢獻行為。",
-       "ipb-partial-help": "特殊頁面或命名空間。",
+       "ipb-sitewide-help": "在維基上的所有頁面以及其它貢獻行為。",
+       "ipb-partial-help": "指定頁面或命名空間。",
        "ipb-pages-label": "頁面",
        "ipb-namespaces-label": "命名空間",
        "badipaddress": "無效的 IP 位址",
diff --git a/maintenance/language/generateUcfirstOverrides.php b/maintenance/language/generateUcfirstOverrides.php
new file mode 100644 (file)
index 0000000..c1e93f4
--- /dev/null
@@ -0,0 +1,83 @@
+<?php
+/**
+ * Generate a php file containg an array of
+ *   utf8_lowercase => utf8_uppercase
+ * overrides. Takes as input two json files generated with generateUpperCharTable.php
+ * as input.
+ *
+ * Example run:
+ * # this will prepare a file to use to make hhvm's Language::ucfirst work like php7's
+ *
+ * $ php7.2 maintenance/language/generateUpperCharTable.php --outfile php7.2.json
+ * $ hhvm --php maintenance/language/generateUpperCharTable.php --outfile hhvm.json
+ * $ hhvm maintenance/language/generateUcfirstOverrides.php \
+ *       --override hhvm.json --with php7.2.json --outfile test.php
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup MaintenanceLanguage
+ */
+
+require_once __DIR__ . '/../Maintenance.php';
+
+class GenerateUcfirstOverrides extends Maintenance {
+
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription(
+                       'Generates a php source file containing a definition for mb_strtoupper overrides' );
+               $this->addOption( 'outfile', 'Output file', true, true, 'o' );
+               $this->addOption( 'override', 'Char table we want to override', true, true );
+               $this->addOption( 'with', 'Char table we want to obtain', true, true );
+       }
+
+       public function execute() {
+               $outfile = $this->getOption( 'outfile' );
+               $from = $this->loadJson( $this->getOption( 'override' ) );
+               $to = $this->loadJson( $this->getOption( 'with' ) );
+               $overrides = [];
+
+               foreach ( $from as $lc => $uc ) {
+                       $ref = $to[$lc] ?? null;
+                       if ( $ref !== null && $ref !== $uc ) {
+                               $overrides[$lc] = $uc;
+                       }
+               }
+               $writer = new StaticArrayWriter();
+               file_put_contents(
+                       $outfile,
+                       $writer->create( $overrides, 'File created by generateUcfirstOverrides.php' )
+               );
+       }
+
+       private function loadJson( $filename ) {
+               $data = file_get_contents( $filename );
+               if ( $data === false ) {
+                       $msg = sprintf( "Could not load data from file '%s'\n", $filename );
+                       $this->fatalError( $msg );
+               }
+               $json = json_decode( $data );
+               if ( $result === null ) {
+                       $msg = sprintf( "Invalid json in the data file %s\n", $filename );
+                       $this->fatalError( $msg, 2 );
+               }
+               return $json;
+       }
+}
+
+$maintClass = GenerateUcfirstOverrides::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
diff --git a/maintenance/language/generateUpperCharTable.php b/maintenance/language/generateUpperCharTable.php
new file mode 100644 (file)
index 0000000..b03d704
--- /dev/null
@@ -0,0 +1,49 @@
+<?php
+/**
+ * Generate a json file containing an array of
+ *   utf8_lowercase => utf8_uppercase
+ * for all of the utf-8 range. This provides the input for generateUcfirstOverrides.php
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup MaintenanceLanguage
+ */
+
+require_once __DIR__ . '/../Maintenance.php';
+
+class GenerateUpperCharTable extends Maintenance {
+
+       public function __construct() {
+               parent::__construct();
+               $this->addDescription( 'Generates the lowercase => uppercase json table' );
+               $this->addOption( 'outfile', 'Output file', true, true, 'o' );
+       }
+
+       public function execute() {
+               $outfile = $this->getOption( 'outfile', 'upperchar.json' );
+               $toUpperTable = [];
+               for ( $i = 0; $i <= 0x10ffff; $i++ ) {
+                       $char = UtfNormal\Utils::codepointToUtf8( $i );
+                       $upper = mb_strtoupper( $char );
+                       $toUpperTable[$char] = $upper;
+               }
+               file_put_contents( $outfile, json_encode( $toUpperTable ) );
+       }
+}
+
+$maintClass = GenerateUpperCharTable::class;
+require_once RUN_MAINTENANCE_IF_MAIN;
index 75904d0..a62e019 100644 (file)
@@ -58,7 +58,7 @@ class PreprocessDump extends DumpIterator {
        }
 
        public function checkOptions() {
-               global $wgParser, $wgParserConf, $wgPreprocessorCacheThreshold;
+               global $wgParserConf, $wgPreprocessorCacheThreshold;
 
                if ( !$this->hasOption( 'cache' ) ) {
                        $wgPreprocessorCacheThreshold = false;
@@ -72,7 +72,7 @@ class PreprocessDump extends DumpIterator {
                        $name = Preprocessor_DOM::class;
                }
 
-               $wgParser->firstCallInit();
+               MediaWikiServices::getInstance()->getParser()->firstCallInit();
                $this->mPreprocessor = new $name( $this );
        }
 
index 8df01e6..e57e977 100644 (file)
@@ -195,7 +195,7 @@ class PPFuzzTest {
        }
 
        function execute() {
-               global $wgParser, $wgUser;
+               global $wgUser;
 
                $wgUser = new PPFuzzUser;
                $wgUser->mName = 'Fuzz';
@@ -206,7 +206,7 @@ class PPFuzzTest {
                $options->setTemplateCallback( [ $this, 'templateHook' ] );
                $options->setTimestamp( wfTimestampNow() );
                $this->output = call_user_func(
-                       [ $wgParser, $this->entryPoint ],
+                       [ MediaWikiServices::getInstance()->getParser(), $this->entryPoint ],
                        $this->mainText,
                        $this->title,
                        $options
index 802a114..9354e4f 100644 (file)
  * @ingroup Maintenance
  */
 
+if ( !defined( 'MEDIAWIKI' ) ) {
+       // So extensions (and other code) can check whether they're running in job mode.
+       // This is not defined if this script is included from installer/updater or phpunit.
+       define( 'MEDIAWIKI_JOB_RUNNER', true );
+}
+
 require_once __DIR__ . '/Maintenance.php';
 
 use MediaWiki\Logger\LoggerFactory;
 
-// So extensions (and other code) can check whether they're running in job mode
-define( 'MEDIAWIKI_JOB_RUNNER', true );
-
 /**
  * Maintenance script that runs pending jobs.
  *
index b40b769..3eb25a9 100644 (file)
@@ -1719,10 +1719,10 @@ class ParserTestRunner {
         * @return bool True if tag hook is present
         */
        public function requireHook( $name ) {
-               global $wgParser;
+               $parser = MediaWikiServices::getInstance()->getParser();
 
-               $wgParser->firstCallInit(); // make sure hooks are loaded.
-               if ( isset( $wgParser->mTagHooks[$name] ) ) {
+               $parser->firstCallInit(); // make sure hooks are loaded.
+               if ( isset( $parser->mTagHooks[$name] ) ) {
                        return true;
                } else {
                        $this->recorder->warning( "   This test suite requires the '$name' hook " .
@@ -1738,11 +1738,11 @@ class ParserTestRunner {
         * @return bool True if function hook is present
         */
        public function requireFunctionHook( $name ) {
-               global $wgParser;
+               $parser = MediaWikiServices::getInstance()->getParser();
 
-               $wgParser->firstCallInit(); // make sure hooks are loaded.
+               $parser->firstCallInit(); // make sure hooks are loaded.
 
-               if ( isset( $wgParser->mFunctionHooks[$name] ) ) {
+               if ( isset( $parser->mFunctionHooks[$name] ) ) {
                        return true;
                } else {
                        $this->recorder->warning( "   This test suite requires the '$name' function " .
@@ -1758,11 +1758,11 @@ class ParserTestRunner {
         * @return bool True if function hook is present
         */
        public function requireTransparentHook( $name ) {
-               global $wgParser;
+               $parser = MediaWikiServices::getInstance()->getParser();
 
-               $wgParser->firstCallInit(); // make sure hooks are loaded.
+               $parser->firstCallInit(); // make sure hooks are loaded.
 
-               if ( isset( $wgParser->mTransparentTagHooks[$name] ) ) {
+               if ( isset( $parser->mTransparentTagHooks[$name] ) ) {
                        return true;
                } else {
                        $this->recorder->warning( "   This test suite requires the '$name' transparent " .
index 5d77ceb..e745960 100644 (file)
@@ -1,6 +1,5 @@
 <?php
 
-use MediaWiki\MediaWikiServices;
 use Wikimedia\TestingAccessWrapper;
 
 /**
@@ -408,8 +407,7 @@ class MessageTest extends MediaWikiLangTestCase {
                // We have to reset the core hook registration.
                // to register the html hook
                MessageCache::destroyInstance();
-               $this->setMwGlobals( 'wgParser',
-                       MediaWikiServices::getInstance()->getParserFactory()->create() );
+               $this->overrideMwServices();
 
                $msg = new RawMessage( '<html><script>alert("xss")</script></html>' );
                $txt = '<span class="error">&lt;html&gt; tags cannot be' .
index 55f4a33..282188d 100644 (file)
@@ -571,22 +571,19 @@ class ApiQuerySiteinfoTest extends ApiTestCase {
        }
 
        public function testExtensionTags() {
-               global $wgParser;
-
                $expected = array_map(
                        function ( $tag ) {
                                return "<$tag>";
                        },
-                       $wgParser->getTags()
+                       MediaWikiServices::getInstance()->getParser()->getTags()
                );
 
                $this->assertSame( $expected, $this->doQuery( 'extensiontags' ) );
        }
 
        public function testFunctionHooks() {
-               global $wgParser;
-
-               $this->assertSame( $wgParser->getFunctionHooks(), $this->doQuery( 'functionhooks' ) );
+               $this->assertSame( MediaWikiServices::getInstance()->getParser()->getFunctionHooks(),
+                       $this->doQuery( 'functionhooks' ) );
        }
 
        public function testVariables() {
index a63f8aa..c6ed8a7 100644 (file)
@@ -1,9 +1,13 @@
 <?php
 
+use MediaWiki\MediaWikiServices;
+use MediaWiki\Storage\PageEditStash;
 use Wikimedia\TestingAccessWrapper;
+use Psr\Log\NullLogger;
 
 /**
  * @covers ApiStashEdit
+ * @covers \MediaWiki\Storage\PageEditStash
  * @group API
  * @group medium
  * @group Database
@@ -11,12 +15,21 @@ use Wikimedia\TestingAccessWrapper;
 class ApiStashEditTest extends ApiTestCase {
        public function setUp() {
                parent::setUp();
-
-               // We need caching here, but note that the cache gets cleared in between tests, so it
-               // doesn't work with @depends
+               $this->setService( 'PageEditStash', new PageEditStash(
+                       new HashBagOStuff( [] ),
+                       MediaWikiServices::getInstance()->getDBLoadBalancer(),
+                       new NullLogger(),
+                       new NullStatsdDataFactory(),
+                       PageEditStash::INITIATOR_USER
+               ) );
+               // Clear rate-limiting cache between tests
                $this->setMwGlobals( 'wgMainCacheType', 'hash' );
        }
 
+       public function tearDown() {
+               parent::tearDown();
+       }
+
        /**
         * Make a stashedit API call with suitable default parameters
         *
@@ -24,6 +37,7 @@ class ApiStashEditTest extends ApiTestCase {
         *   sensible defaults filled in.  To make a parameter actually not passed, set to null.
         * @param User $user User to do the request
         * @param string $expectedResult 'stashed', 'editconflict'
+        * @return array
         */
        protected function doStash(
                array $params = [], User $user = null, $expectedResult = 'stashed'
@@ -88,13 +102,11 @@ class ApiStashEditTest extends ApiTestCase {
         * @return string
         */
        protected function getStashedText( $hash ) {
-               $cache = ObjectCache::getLocalClusterInstance();
-               $key = $cache->makeKey( 'stashedit', 'text', $hash );
-               return $cache->get( $key );
+               return MediaWikiServices::getInstance()->getPageEditStash()->fetchInputText( $hash );
        }
 
        /**
-        * Return a key that can be passed to the cache to obtain a PreparedEdit object.
+        * Return a key that can be passed to the cache to obtain a stashed edit object.
         *
         * @param string $title Title of page
         * @param string Content $text Content of edit
@@ -107,8 +119,10 @@ class ApiStashEditTest extends ApiTestCase {
                if ( !$user ) {
                        $user = $this->getTestSysop()->getUser();
                }
-               $wrapper = TestingAccessWrapper::newFromClass( ApiStashEdit::class );
-               return $wrapper->getStashKey( $titleObj, $wrapper->getContentHash( $content ), $user );
+               $editStash = TestingAccessWrapper::newFromObject(
+                       MediaWikiServices::getInstance()->getPageEditStash() );
+
+               return $editStash->getStashKey( $titleObj, $editStash->getContentHash( $content ), $user );
        }
 
        public function testBasicEdit() {
@@ -263,15 +277,15 @@ class ApiStashEditTest extends ApiTestCase {
        }
 
        /**
-        * Shortcut for calling ApiStashEdit::checkCache() without having to create Titles and Contents
-        * in every test.
+        * Shortcut for calling PageStashEdit::checkCache() without
+        * having to create Titles and Contents in every test.
         *
         * @param User $user
         * @param string $text The text of the article
-        * @return stdClass|bool Return value of ApiStashEdit::checkCache(), false if not in cache
+        * @return stdClass|bool Return value of PageStashEdit::checkCache(), false if not in cache
         */
        protected function doCheckCache( User $user, $text = 'Content' ) {
-               return ApiStashEdit::checkCache(
+               return MediaWikiServices::getInstance()->getPageEditStash()->checkCache(
                        Title::newFromText( __CLASS__ ),
                        new WikitextContent( $text ),
                        $user
@@ -305,11 +319,11 @@ class ApiStashEditTest extends ApiTestCase {
        }
 
        public function testCheckCacheAnon() {
-               $user = new User();
+               $user = User::newFromName( '174.5.4.6', false );
 
                $this->doStash( [], $user );
 
-               $this->assertInstanceOf( stdClass::class, $this->docheckCache( $user ) );
+               $this->assertInstanceOf( stdClass::class, $this->doCheckCache( $user ) );
        }
 
        /**
@@ -320,7 +334,7 @@ class ApiStashEditTest extends ApiTestCase {
         * @param int $howOld How many seconds is "old" (we actually set it one second before this)
         */
        protected function doStashOld(
-               User $user, $text = 'Content', $howOld = ApiStashEdit::PRESUME_FRESH_TTL_SEC
+               User $user, $text = 'Content', $howOld = PageEditStash::PRESUME_FRESH_TTL_SEC
        ) {
                $this->doStash( [ 'text' => $text ], $user );
 
@@ -328,7 +342,9 @@ class ApiStashEditTest extends ApiTestCase {
                // fake the time?
                $key = $this->getStashKey( __CLASS__, $text, $user );
 
-               $cache = ObjectCache::getLocalClusterInstance();
+               $editStash = TestingAccessWrapper::newFromObject(
+                       MediaWikiServices::getInstance()->getPageEditStash() );
+               $cache = $editStash->cache;
 
                $editInfo = $cache->get( $key );
                $outputKey = $cache->makeKey( 'stashed-edit-output', $editInfo->outputID );
@@ -350,7 +366,7 @@ class ApiStashEditTest extends ApiTestCase {
 
        public function testCheckCacheOldNoEditsAnon() {
                // Specify a made-up IP address to make sure no edits are lying around
-               $user = User::newFromName( '192.0.2.77', false );
+               $user = User::newFromName( '172.0.2.77', false );
 
                $this->doStashOld( $user );
 
@@ -379,7 +395,9 @@ class ApiStashEditTest extends ApiTestCase {
        public function testSignatureTtl( $text, $ttl ) {
                $this->doStash( [ 'text' => $text ] );
 
-               $cache = ObjectCache::getLocalClusterInstance();
+               $editStash = TestingAccessWrapper::newFromObject(
+                       MediaWikiServices::getInstance()->getPageEditStash() );
+               $cache = $editStash->cache;
                $key = $this->getStashKey( __CLASS__, $text );
 
                $wrapper = TestingAccessWrapper::newFromObject( $cache );
@@ -389,9 +407,9 @@ class ApiStashEditTest extends ApiTestCase {
 
        public function signatureProvider() {
                return [
-                       '~~~' => [ '~~~', ApiStashEdit::MAX_SIGNATURE_TTL ],
-                       '~~~~' => [ '~~~~', ApiStashEdit::MAX_SIGNATURE_TTL ],
-                       '~~~~~' => [ '~~~~~', ApiStashEdit::MAX_SIGNATURE_TTL ],
+                       '~~~' => [ '~~~', PageEditStash::MAX_SIGNATURE_TTL ],
+                       '~~~~' => [ '~~~~', PageEditStash::MAX_SIGNATURE_TTL ],
+                       '~~~~~' => [ '~~~~~', PageEditStash::MAX_SIGNATURE_TTL ],
                ];
        }
 
index e102b9b..cf0b650 100644 (file)
@@ -1,4 +1,5 @@
 <?php
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Revision\MutableRevisionRecord;
 use MediaWiki\Revision\RevisionStore;
 use MediaWiki\Revision\SlotRecord;
@@ -26,13 +27,12 @@ class ParserMethodsTest extends MediaWikiLangTestCase {
         * @dataProvider providePreSaveTransform
         */
        public function testPreSaveTransform( $text, $expected ) {
-               global $wgParser;
-
                $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
                $user = new User();
                $user->setName( "127.0.0.1" );
                $popts = ParserOptions::newFromUser( $user );
-               $text = $wgParser->preSaveTransform( $text, $title, $user, $popts );
+               $text = MediaWikiServices::getInstance()->getParser()
+                       ->preSaveTransform( $text, $title, $user, $popts );
 
                $this->assertEquals( $expected, $text );
        }
@@ -78,11 +78,11 @@ class ParserMethodsTest extends MediaWikiLangTestCase {
         *  Did you call Parser::parse recursively?
         */
        public function testRecursiveParse() {
-               global $wgParser;
                $title = Title::newFromText( 'foo' );
+               $parser = MediaWikiServices::getInstance()->getParser();
                $po = new ParserOptions;
-               $wgParser->setHook( 'recursivecallparser', [ $this, 'helperParserFunc' ] );
-               $wgParser->parse( '<recursivecallparser>baz</recursivecallparser>', $title, $po );
+               $parser->setHook( 'recursivecallparser', [ $this, 'helperParserFunc' ] );
+               $parser->parse( '<recursivecallparser>baz</recursivecallparser>', $title, $po );
        }
 
        public function helperParserFunc( $input, $args, $parser ) {
@@ -93,16 +93,15 @@ class ParserMethodsTest extends MediaWikiLangTestCase {
        }
 
        public function testCallParserFunction() {
-               global $wgParser;
-
                // Normal parses test passing PPNodes. Test passing an array.
                $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
-               $wgParser->startExternalParse( $title, new ParserOptions(), Parser::OT_HTML );
-               $frame = $wgParser->getPreprocessor()->newFrame();
-               $ret = $wgParser->callParserFunction( $frame, '#tag',
+               $parser = MediaWikiServices::getInstance()->getParser();
+               $parser->startExternalParse( $title, new ParserOptions(), Parser::OT_HTML );
+               $frame = $parser->getPreprocessor()->newFrame();
+               $ret = $parser->callParserFunction( $frame, '#tag',
                        [ 'pre', 'foo', 'style' => 'margin-left: 1.6em' ]
                );
-               $ret['text'] = $wgParser->mStripState->unstripBoth( $ret['text'] );
+               $ret['text'] = $parser->mStripState->unstripBoth( $ret['text'] );
                $this->assertSame( [
                        'found' => true,
                        'text' => '<pre style="margin-left: 1.6em">foo</pre>',
@@ -114,10 +113,9 @@ class ParserMethodsTest extends MediaWikiLangTestCase {
         * @covers ParserOutput::getSections
         */
        public function testGetSections() {
-               global $wgParser;
-
                $title = Title::newFromText( str_replace( '::', '__', __METHOD__ ) );
-               $out = $wgParser->parse( "==foo==\n<h2>bar</h2>\n==baz==\n", $title, new ParserOptions() );
+               $out = MediaWikiServices::getInstance()->getParser()
+                       ->parse( "==foo==\n<h2>bar</h2>\n==baz==\n", $title, new ParserOptions() );
                $this->assertSame( [
                        [
                                'toclevel' => 1,
@@ -195,11 +193,11 @@ class ParserMethodsTest extends MediaWikiLangTestCase {
        }
 
        public function testWrapOutput() {
-               global $wgParser;
                $title = Title::newFromText( 'foo' );
                $po = new ParserOptions();
-               $wgParser->parse( 'Hello World', $title, $po );
-               $text = $wgParser->getOutput()->getText();
+               $parser = MediaWikiServices::getInstance()->getParser();
+               $parser->parse( 'Hello World', $title, $po );
+               $text = $parser->getOutput()->getText();
 
                $this->assertContains( 'Hello World', $text );
                $this->assertContains( '<div', $text );
@@ -330,8 +328,6 @@ class ParserMethodsTest extends MediaWikiLangTestCase {
                $expectedInHtml,
                $expectedInPst = null
        ) {
-               global $wgParser;
-
                $title = $this->getMockTitle( 'ParserRevisionAccessTest' );
 
                $po->enableLimitReport( false );
@@ -369,13 +365,14 @@ class ParserMethodsTest extends MediaWikiLangTestCase {
 
                $this->setService( 'RevisionStore', $revisionStore );
 
-               $wgParser->parse( $text, $title, $po, true, true, $revId );
-               $html = $wgParser->getOutput()->getText();
+               $parser = MediaWikiServices::getInstance()->getParser();
+               $parser->parse( $text, $title, $po, true, true, $revId );
+               $html = $parser->getOutput()->getText();
 
                $this->assertContains( $expectedInHtml, $html, 'In HTML' );
 
                if ( $expectedInPst !== null ) {
-                       $pst = $wgParser->preSaveTransform( $text, $title, $po->getUser(), $po );
+                       $pst = $parser->preSaveTransform( $text, $title, $po->getUser(), $po );
                        $this->assertContains( $expectedInPst, $pst, 'After Pre-Safe Transform' );
                }
        }
@@ -390,8 +387,8 @@ class ParserMethodsTest extends MediaWikiLangTestCase {
        /** @dataProvider provideGuessSectionNameFromWikiText */
        public function testGuessSectionNameFromWikiText( $input, $mode, $expected ) {
                $this->setMwGlobals( [ 'wgFragmentMode' => [ $mode ] ] );
-               global $wgParser;
-               $result = $wgParser->guessSectionNameFromWikiText( $input );
+               $result = MediaWikiServices::getInstance()->getParser()
+                       ->guessSectionNameFromWikiText( $input );
                $this->assertEquals( $result, $expected );
        }
 
index 7239afc..85a47de 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 
 use Wikimedia\TestingAccessWrapper;
+use MediaWiki\MediaWikiServices;
 
 class ResourceLoaderTest extends ResourceLoaderTestCase {
 
@@ -14,25 +15,23 @@ class ResourceLoaderTest extends ResourceLoaderTestCase {
 
        /**
         * Ensure the ResourceLoaderRegisterModules hook is called.
-        *
-        * @covers ResourceLoader::__construct
+        * @coversNothing
         */
-       public function testConstructRegistrationHook() {
-               $resourceLoaderRegisterModulesHook = false;
+       public function testServiceWiring() {
+               $this->overrideMwServices();
 
+               $ranHook = 0;
                $this->setMwGlobals( 'wgHooks', [
                        'ResourceLoaderRegisterModules' => [
-                               function ( &$resourceLoader ) use ( &$resourceLoaderRegisterModulesHook ) {
-                                       $resourceLoaderRegisterModulesHook = true;
+                               function ( &$resourceLoader ) use ( &$ranHook ) {
+                                       $ranHook++;
                                }
                        ]
                ] );
 
-               $unused = new ResourceLoader();
-               $this->assertTrue(
-                       $resourceLoaderRegisterModulesHook,
-                       'Hook ResourceLoaderRegisterModules called'
-               );
+               MediaWikiServices::getInstance()->getResourceLoader();
+
+               $this->assertSame( 1, $ranHook, 'Hook was called' );
        }
 
        /**
index dca1363..050f07d 100644 (file)
@@ -1909,4 +1909,27 @@ class LanguageTest extends LanguageClassesTestCase {
                $ar2 = new LanguageAr();
                $this->assertTrue( $ar1->equals( $ar2 ), 'ar equals ar' );
        }
+
+       /**
+        * @dataProvider provideUcfirst
+        * @covers Language::ucfirst
+        */
+       public function testUcfirst( $orig, $expected, $desc, $overrides = false ) {
+               $lang = new Language();
+               if ( is_array( $overrides ) ) {
+                       $this->setMwGlobals( [ 'wgOverrideUcfirstCharacters' => $overrides ] );
+               }
+               $this->assertSame( $lang->ucfirst( $orig ), $expected, $desc );
+       }
+
+       public static function provideUcfirst() {
+               return [
+                       [ 'alice', 'Alice', 'simple ASCII string', false ],
+                       [ 'århus',  'Århus', 'unicode string', false ],
+                       //overrides do not affect ASCII characters
+                       [ 'foo', 'Foo', 'ASCII is not overriden', [ 'f' => 'b' ] ],
+                       // but they do affect non-ascii ones
+                       [ 'èl', 'Ll' , 'Non-ASCII is overridden', [ 'è' => 'L' ] ],
+               ];
+       }
 }
index 556c754..00d607f 100644 (file)
@@ -16,8 +16,7 @@ class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite {
        }
 
        protected function setUp() {
-               global $wgParser, $wgParserConf, $IP, $messageMemc, $wgMemc, $wgUser,
-                       $wgLang, $wgOut, $wgRequest, $wgStyleDirectory,
+               global $IP, $messageMemc, $wgMemc, $wgUser, $wgLang, $wgOut, $wgRequest, $wgStyleDirectory,
                        $wgParserCacheType, $wgNamespaceAliases, $wgNamespaceProtection;
 
                $tmpDir = $this->getNewTempDirectory();
@@ -66,7 +65,6 @@ class UploadFromUrlTestSuite extends PHPUnit_Framework_TestSuite {
                $wgUser = new User;
                $wgLang = $context->getLanguage();
                $wgOut = $context->getOutput();
-               $wgParser = new StubObject( 'wgParser', $wgParserConf['class'], [ $wgParserConf ] );
                $wgRequest = $context->getRequest();
 
                if ( $wgStyleDirectory === false ) {