Merge "A few more DBLockManager fixes and cleanups"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 24 Aug 2016 03:15:31 +0000 (03:15 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 24 Aug 2016 03:15:31 +0000 (03:15 +0000)
43 files changed:
RELEASE-NOTES-1.28
autoload.php
composer.json
docs/hooks.txt
includes/DefaultSettings.php
includes/EditPage.php
includes/MediaWiki.php
includes/OutputPage.php
includes/Pingback.php
includes/collation/Collation.php
includes/collation/NumericUppercaseCollation.php [new file with mode: 0644]
includes/content/JsonContent.php
includes/content/TextContent.php
includes/content/WikitextContent.php
includes/db/CloneDatabase.php
includes/deferred/DataUpdate.php
includes/deferred/EnqueueableDataUpdate.php [new file with mode: 0644]
includes/filebackend/lockmanager/RedisLockManager.php
includes/htmlform/HTMLForm.php
includes/htmlform/HTMLFormField.php
includes/htmlform/fields/HTMLMultiSelectField.php
includes/jobqueue/JobQueueDB.php
includes/jobqueue/JobRunner.php
includes/objectcache/SqlBagOStuff.php
includes/page/WikiPage.php
includes/parser/Parser.php
includes/specialpage/LoginSignupSpecialPage.php
includes/specials/SpecialMyLanguage.php
includes/specials/SpecialRunJobs.php
includes/user/User.php
languages/Language.php
languages/i18n/en.json
languages/i18n/qqq.json
languages/messages/MessagesSk.php
maintenance/validateRegistrationFile.php
resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js
resources/src/mediawiki/htmlform/multiselect.js
tests/phpunit/includes/content/TextContentTest.php
tests/phpunit/includes/libs/objectcache/CachedBagOStuffTest.php
tests/phpunit/includes/libs/objectcache/HashBagOStuffTest.php
tests/phpunit/includes/libs/objectcache/MultiWriteBagOStuffTest.php
tests/phpunit/includes/user/UserTest.php
tests/phpunit/structure/ExtensionJsonValidationTest.php

index 5d88fbf..92ac869 100644 (file)
@@ -39,7 +39,8 @@ production.
 * (T141604) Extensions can now provide a better error message when their
   maintenance scripts are run without the extension being installed.
 * (T8948) Numeric sorting in categories is now supported by setting $wgCategoryCollation
-  to uca-default-u-kn or uca-<langcode>-u-kn. If migrating from another
+  to 'uca-default-u-kn' or 'uca-<langcode>-u-kn'. If you can't use UCA collations,
+  a 'numeric' collation is also available. If migrating from another
   collation, you will need to run the updateCollation.php maintenance script.
 
 === External library changes in 1.28 ===
@@ -52,6 +53,16 @@ production.
 ==== Removed and replaced external libraries ====
 
 === Bug fixes in 1.28 ===
+* (T137264) SECURITY: XSS in unclosed internal links
+* (T133147) SECURITY: Escape '<' and ']]>' in inline <style> blocks
+* (T133147) SECURITY: Require login to preview user CSS pages
+* (T132926) SECURITY: Do not allow undeleting a revision deleted file if it is
+  the top file
+* (T129738) SECURITY: Make $wgBlockDisablesLogin also restrict logged in
+  permissions
+* (T129738) SECURITY: Make blocks log users out if $wgBlockDisablesLogin is true
+* (T139670) Move 'UserGetRights' call before application of
+  Session::getAllowedUserRights()
 
 === Action API changes in 1.28 ===
 * Added 'maxarticlesize' property to action=query&meta=siteinfo which contains
@@ -72,6 +83,8 @@ production.
 === Action API internal changes in 1.28 ===
 * Added a new hook, 'ApiMakeParserOptions', to allow extensions to better
   interact with ApiParse and ApiExpandTemplates.
+* (T139565) SECURITY: API: Generate head items in the context of the given title
+* (T115333) SECURITY: Check read permission when loading page content in ApiParse
 
 === Languages updated in 1.28 ===
 
index 5457d2a..39102fd 100644 (file)
@@ -408,7 +408,7 @@ $wgAutoloadLocalClasses = [
        'EnhancedChangesList' => __DIR__ . '/includes/changes/EnhancedChangesList.php',
        'EnotifNotifyJob' => __DIR__ . '/includes/jobqueue/jobs/EnotifNotifyJob.php',
        'EnqueueJob' => __DIR__ . '/includes/jobqueue/jobs/EnqueueJob.php',
-       'EnqueueableDataUpdate' => __DIR__ . '/includes/deferred/DataUpdate.php',
+       'EnqueueableDataUpdate' => __DIR__ . '/includes/deferred/EnqueueableDataUpdate.php',
        'EraseArchivedFile' => __DIR__ . '/maintenance/eraseArchivedFile.php',
        'ErrorPageError' => __DIR__ . '/includes/exception/ErrorPageError.php',
        'EventRelayer' => __DIR__ . '/includes/libs/eventrelayer/EventRelayer.php',
@@ -975,6 +975,7 @@ $wgAutoloadLocalClasses = [
        'NullLockManager' => __DIR__ . '/includes/filebackend/lockmanager/LockManager.php',
        'NullRepo' => __DIR__ . '/includes/filerepo/NullRepo.php',
        'NullStatsdDataFactory' => __DIR__ . '/includes/libs/stats/NullStatsdDataFactory.php',
+       'NumericUppercaseCollation' => __DIR__ . '/includes/collation/NumericUppercaseCollation.php',
        'OOUIHTMLForm' => __DIR__ . '/includes/htmlform/OOUIHTMLForm.php',
        'ORAField' => __DIR__ . '/includes/db/DatabaseOracle.php',
        'ORAResult' => __DIR__ . '/includes/db/DatabaseOracle.php',
index 956739d..2243b7c 100644 (file)
@@ -45,7 +45,7 @@
        },
        "require-dev": {
                "jakub-onderka/php-parallel-lint": "0.9.2",
-               "justinrainbow/json-schema": "~1.6",
+               "justinrainbow/json-schema": "~3.0",
                "mediawiki/mediawiki-codesniffer": "0.7.2",
                "monolog/monolog": "~1.18.2",
                "nikic/php-parser": "2.1.0",
index 5cf8ffe..a0ee8bd 100644 (file)
@@ -297,16 +297,6 @@ After a user account is created.
 $user: the User object that was created. (Parameter added in 1.7)
 $byEmail: true when account was created "by email" (added in 1.12)
 
-'AddNewAccountApiForm': Allow modifying internal login form when creating an
-account via API.
-$apiModule: the ApiCreateAccount module calling
-$loginForm: the LoginForm used
-
-'AddNewAccountApiResult': Modify API output when creating a new account via API.
-$apiModule: the ApiCreateAccount module calling
-$loginForm: the LoginForm used
-&$result: associative array for API result data
-
 'AfterBuildFeedLinks': Executed in OutputPage.php after all feed links (atom, rss,...)
 are created. Can be used to omit specific feeds from being outputted. You must not use
 this hook to add feeds, use OutputPage::addFeedLink() instead.
@@ -2069,13 +2059,6 @@ $e: The exception (in case of a plain old PHP error, a wrapping ErrorException)
 $suppressed: true if the error was suppressed via
   error_reporting()/wfSuppressWarnings()
 
-'LoginAuthenticateAudit': A login attempt for a valid user account either
-succeeded or failed. No return data is accepted; this hook is for auditing only.
-$user: the User object being authenticated against
-$password: the password being submitted and found wanting
-$retval: a LoginForm class constant with authenticateUserData() return
-  value (SUCCESS, WRONG_PASS, etc.).
-
 'LoginFormValidErrorMessages': Called in LoginForm when a function gets valid
 error messages. Allows to add additional error messages (except messages already
 in LoginForm::$validErrorMessages).
index 87003f0..e15c988 100644 (file)
@@ -3193,6 +3193,15 @@ $wgHTMLFormAllowTableFormat = true;
  */
 $wgUseMediaWikiUIEverywhere = false;
 
+/**
+ * Whether to label the store-to-database-and-show-to-others button in the editor
+ * as "Save page"/"Save changes" if false (the default) or, if true, instead as
+ * "Publish page"/"Publish changes".
+ *
+ * @since 1.28
+ */
+$wgEditButtonPublishNotSave = false;
+
 /**
  * Permit other namespaces in addition to the w3.org default.
  *
@@ -8033,9 +8042,13 @@ $wgJobRunRate = 1;
  * When $wgJobRunRate > 0, try to run jobs asynchronously, spawning a new process
  * to handle the job execution, instead of blocking the request until the job
  * execution finishes.
+ *
  * @since 1.23
  */
-$wgRunJobsAsync = true;
+$wgRunJobsAsync = (
+       !function_exists( 'register_postsend_function' ) &&
+       !function_exists( 'fastcgi_finish_request' )
+);
 
 /**
  * Number of rows to update per job
@@ -8342,7 +8355,7 @@ $wgEventRelayerConfig = [
  * PHP version, and chosen database backend. The Wikimedia Foundation shares this data with
  * MediaWiki developers to help guide future development efforts.
  *
- * For details about what data is sent, see: https://www.mediawiki.org/wiki/Pingback
+ * For details about what data is sent, see: https://www.mediawiki.org/wiki/Manual:$wgPingback
  *
  * @var bool
  * @since 1.28
index 9b862b9..738eaec 100644 (file)
@@ -401,6 +401,11 @@ class EditPage {
         */
        private $enableApiEditOverride = false;
 
+       /**
+        * @var IContextSource
+        */
+       protected $context;
+
        /**
         * @param Article $article
         */
@@ -408,6 +413,7 @@ class EditPage {
                $this->mArticle = $article;
                $this->page = $article->getPage(); // model object
                $this->mTitle = $article->getTitle();
+               $this->context = $article->getContext();
 
                $this->contentModel = $this->mTitle->getContentModel();
 
@@ -422,6 +428,14 @@ class EditPage {
                return $this->mArticle;
        }
 
+       /**
+        * @since 1.28
+        * @return IContextSource
+        */
+       public function getContext() {
+               return $this->context;
+       }
+
        /**
         * @since 1.19
         * @return Title
@@ -493,7 +507,6 @@ class EditPage {
         * the newly-edited page.
         */
        function edit() {
-               global $wgOut, $wgRequest, $wgUser;
                // Allow extensions to modify/prevent this form or submission
                if ( !Hooks::run( 'AlternateEdit', [ $this ] ) ) {
                        return;
@@ -501,13 +514,15 @@ class EditPage {
 
                wfDebug( __METHOD__ . ": enter\n" );
 
+               $request = $this->context->getRequest();
+               $out = $this->context->getOutput();
                // If they used redlink=1 and the page exists, redirect to the main article
-               if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
-                       $wgOut->redirect( $this->mTitle->getFullURL() );
+               if ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
+                       $out->redirect( $this->mTitle->getFullURL() );
                        return;
                }
 
-               $this->importFormData( $wgRequest );
+               $this->importFormData( $request );
                $this->firsttime = false;
 
                if ( wfReadOnly() && $this->save ) {
@@ -536,7 +551,7 @@ class EditPage {
                        wfDebug( __METHOD__ . ": User can't edit\n" );
                        // Auto-block user's IP if the account was "hard" blocked
                        if ( !wfReadOnly() ) {
-                               $user = $wgUser;
+                               $user = $this->context->getUser();
                                DeferredUpdates::addCallableUpdate( function () use ( $user ) {
                                        $user->spreadAnyEditBlock();
                                } );
@@ -610,15 +625,14 @@ class EditPage {
         * @return array
         */
        protected function getEditPermissionErrors( $rigor = 'secure' ) {
-               global $wgUser;
-
-               $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser, $rigor );
+               $user = $this->context->getUser();
+               $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
                # Can this title be created?
                if ( !$this->mTitle->exists() ) {
                        $permErrors = array_merge(
                                $permErrors,
                                wfArrayDiff2(
-                                       $this->mTitle->getUserPermissionsErrors( 'create', $wgUser, $rigor ),
+                                       $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
                                        $permErrors
                                )
                        );
@@ -651,13 +665,12 @@ class EditPage {
         * @throws PermissionsError
         */
        protected function displayPermissionsError( array $permErrors ) {
-               global $wgRequest, $wgOut;
-
-               if ( $wgRequest->getBool( 'redlink' ) ) {
+               $out = $this->context->getOutput();
+               if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
                        // The edit page was reached via a red link.
                        // Redirect to the article page and let them click the edit tab if
                        // they really want a permission error.
-                       $wgOut->redirect( $this->mTitle->getFullURL() );
+                       $out->redirect( $this->mTitle->getFullURL() );
                        return;
                }
 
@@ -672,7 +685,7 @@ class EditPage {
 
                $this->displayViewSourcePage(
                        $content,
-                       $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
+                       $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
                );
        }
 
@@ -682,29 +695,28 @@ class EditPage {
         * @param string $errorMessage additional wikitext error message to display
         */
        protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
-               global $wgOut;
+               $out = $this->context->getOutput();
+               Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
 
-               Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] );
-
-               $wgOut->setRobotPolicy( 'noindex,nofollow' );
-               $wgOut->setPageTitle( wfMessage(
+               $out->setRobotPolicy( 'noindex,nofollow' );
+               $out->setPageTitle( wfMessage(
                        'viewsource-title',
                        $this->getContextTitle()->getPrefixedText()
                ) );
-               $wgOut->addBacklinkSubtitle( $this->getContextTitle() );
-               $wgOut->addHTML( $this->editFormPageTop );
-               $wgOut->addHTML( $this->editFormTextTop );
+               $out->addBacklinkSubtitle( $this->getContextTitle() );
+               $out->addHTML( $this->editFormPageTop );
+               $out->addHTML( $this->editFormTextTop );
 
                if ( $errorMessage !== '' ) {
-                       $wgOut->addWikiText( $errorMessage );
-                       $wgOut->addHTML( "<hr />\n" );
+                       $out->addWikiText( $errorMessage );
+                       $out->addHTML( "<hr />\n" );
                }
 
                # If the user made changes, preserve them when showing the markup
                # (This happens when a user is blocked during edit, for instance)
                if ( !$this->firsttime ) {
                        $text = $this->textbox1;
-                       $wgOut->addWikiMsg( 'viewyourtext' );
+                       $out->addWikiMsg( 'viewyourtext' );
                } else {
                        try {
                                $text = $this->toEditText( $content );
@@ -713,21 +725,21 @@ class EditPage {
                                # (e.g. for an old revision with a different model)
                                $text = $content->serialize();
                        }
-                       $wgOut->addWikiMsg( 'viewsourcetext' );
+                       $out->addWikiMsg( 'viewsourcetext' );
                }
 
-               $wgOut->addHTML( $this->editFormTextBeforeContent );
+               $out->addHTML( $this->editFormTextBeforeContent );
                $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
-               $wgOut->addHTML( $this->editFormTextAfterContent );
+               $out->addHTML( $this->editFormTextAfterContent );
 
-               $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
+               $out->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
                        Linker::formatTemplates( $this->getTemplates() ) ) );
 
-               $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+               $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
 
-               $wgOut->addHTML( $this->editFormTextBottom );
+               $out->addHTML( $this->editFormTextBottom );
                if ( $this->mTitle->exists() ) {
-                       $wgOut->returnToMain( null, $this->mTitle );
+                       $out->returnToMain( null, $this->mTitle );
                }
        }
 
@@ -737,18 +749,19 @@ class EditPage {
         * @return bool
         */
        protected function previewOnOpen() {
-               global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
-               if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
+               global $wgPreviewOnOpenNamespaces;
+               $request = $this->context->getRequest();
+               if ( $request->getVal( 'preview' ) == 'yes' ) {
                        // Explicit override from request
                        return true;
-               } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
+               } elseif ( $request->getVal( 'preview' ) == 'no' ) {
                        // Explicit override from request
                        return false;
                } elseif ( $this->section == 'new' ) {
                        // Nothing *to* preview for new sections
                        return false;
-               } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() )
-                       && $wgUser->getOption( 'previewonfirst' )
+               } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() )
+                       && $this->context->getUser()->getOption( 'previewonfirst' )
                ) {
                        // Standard preference behavior
                        return true;
@@ -801,7 +814,7 @@ class EditPage {
         * @throws ErrorPageError
         */
        function importFormData( &$request ) {
-               global $wgContLang, $wgUser;
+               global $wgContLang;
 
                # Section edit can come from either the form or a link
                $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
@@ -913,13 +926,14 @@ class EditPage {
                        $this->watchthis = $request->getCheck( 'wpWatchthis' );
 
                        # Don't force edit summaries when a user is editing their own user or talk page
+                       $user = $this->context->getUser();
                        if ( ( $this->mTitle->mNamespace == NS_USER || $this->mTitle->mNamespace == NS_USER_TALK )
-                               && $this->mTitle->getText() == $wgUser->getName()
+                               && $this->mTitle->getText() == $user->getName()
                        ) {
                                $this->allowBlankSummary = true;
                        } else {
                                $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
-                                       || !$wgUser->getOption( 'forceeditsummary' );
+                                       || !$user->getOption( 'forceeditsummary' );
                        }
 
                        $this->autoSumm = $request->getText( 'wpAutoSummary' );
@@ -1025,7 +1039,6 @@ class EditPage {
         * @return bool If the requested section is valid
         */
        function initialiseForm() {
-               global $wgUser;
                $this->edittime = $this->page->getTimestamp();
                $this->editRevId = $this->page->getLatest();
 
@@ -1034,20 +1047,21 @@ class EditPage {
                        return false;
                }
                $this->textbox1 = $this->toEditText( $content );
+               $user = $this->context->getUser();
 
                // activate checkboxes if user wants them to be always active
                # Sort out the "watch" checkbox
-               if ( $wgUser->getOption( 'watchdefault' ) ) {
+               if ( $user->getOption( 'watchdefault' ) ) {
                        # Watch all edits
                        $this->watchthis = true;
-               } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
+               } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
                        # Watch creations
                        $this->watchthis = true;
-               } elseif ( $wgUser->isWatched( $this->mTitle ) ) {
+               } elseif ( $user->isWatched( $this->mTitle ) ) {
                        # Already watched
                        $this->watchthis = true;
                }
-               if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
+               if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
                        $this->minoredit = true;
                }
                if ( $this->textbox1 === false ) {
@@ -1064,9 +1078,11 @@ class EditPage {
         * @since 1.21
         */
        protected function getContentObject( $def_content = null ) {
-               global $wgOut, $wgRequest, $wgUser, $wgContLang;
+               global $wgContLang;
 
                $content = false;
+               $request = $this->context->getRequest();
+               $user = $this->context->getUser();
 
                // For message page not locally set, use the i18n message.
                // For other non-existent articles, use preload text if any.
@@ -1079,10 +1095,10 @@ class EditPage {
                        }
                        if ( $content === false ) {
                                # If requested, preload some text.
-                               $preload = $wgRequest->getVal( 'preload',
+                               $preload = $request->getVal( 'preload',
                                        // Custom preload text for new sections
                                        $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
-                               $params = $wgRequest->getArray( 'preloadparams', [] );
+                               $params = $request->getArray( 'preloadparams', [] );
 
                                $content = $this->getPreloadedContent( $preload, $params );
                        }
@@ -1090,15 +1106,15 @@ class EditPage {
                } else {
                        if ( $this->section != '' ) {
                                // Get section edit text (returns $def_text for invalid sections)
-                               $orig = $this->getOriginalContent( $wgUser );
+                               $orig = $this->getOriginalContent( $user );
                                $content = $orig ? $orig->getSection( $this->section ) : null;
 
                                if ( !$content ) {
                                        $content = $def_content;
                                }
                        } else {
-                               $undoafter = $wgRequest->getInt( 'undoafter' );
-                               $undo = $wgRequest->getInt( 'undo' );
+                               $undoafter = $request->getInt( 'undoafter' );
+                               $undo = $request->getInt( 'undo' );
 
                                if ( $undo > 0 && $undoafter > 0 ) {
                                        $undorev = Revision::newFromId( $undo );
@@ -1118,8 +1134,8 @@ class EditPage {
                                                        $undoMsg = 'failure';
                                                } else {
                                                        $oldContent = $this->page->getContent( Revision::RAW );
-                                                       $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
-                                                       $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
+                                                       $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
+                                                       $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
 
                                                        if ( $newContent->equals( $oldContent ) ) {
                                                                # Tell the user that the undo results in no change,
@@ -1166,12 +1182,13 @@ class EditPage {
 
                                        // Messages: undo-success, undo-failure, undo-norev, undo-nochange
                                        $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
-                                       $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
+                                       $this->editFormPageTop .= $this->context->getOutput()->parse(
+                                               "<div class=\"{$class}\">" .
                                                wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
                                }
 
                                if ( $content === false ) {
-                                       $content = $this->getOriginalContent( $wgUser );
+                                       $content = $this->getOriginalContent( $user );
                                }
                        }
                }
@@ -1368,10 +1385,10 @@ class EditPage {
         * @private
         */
        function tokenOk( &$request ) {
-               global $wgUser;
                $token = $request->getVal( 'wpEditToken' );
-               $this->mTokenOk = $wgUser->matchEditToken( $token );
-               $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
+               $user = $this->context->getUser();
+               $this->mTokenOk = $user->matchEditToken( $token );
+               $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
                return $this->mTokenOk;
        }
 
@@ -1402,7 +1419,7 @@ class EditPage {
                        $val = 'restored';
                }
 
-               $response = RequestContext::getMain()->getRequest()->response();
+               $response = $this->context->getRequest()->response();
                $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
                        'httpOnly' => false,
                ] );
@@ -1410,15 +1427,13 @@ class EditPage {
 
        /**
         * Attempt submission
-        * @param array $resultDetails See docs for $result in internalAttemptSave
+        * @param array|bool $resultDetails See docs for $result in internalAttemptSave
         * @throws UserBlockedError|ReadOnlyError|ThrottledError|PermissionsError
         * @return Status The resulting status object.
         */
        public function attemptSave( &$resultDetails = false ) {
-               global $wgUser;
-
                # Allow bots to exempt some edits from bot flagging
-               $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
+               $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
                $status = $this->internalAttemptSave( $resultDetails, $bot );
 
                Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
@@ -1436,8 +1451,6 @@ class EditPage {
         * @return bool False, if output is done, true if rest of the form should be displayed
         */
        private function handleStatus( Status $status, $resultDetails ) {
-               global $wgUser, $wgOut;
-
                /**
                 * @todo FIXME: once the interface for internalAttemptSave() is made
                 *   nicer, this should use the message in $status
@@ -1451,9 +1464,11 @@ class EditPage {
                        }
                }
 
+               $out = $this->context->getOutput();
+
                // "wpExtraQueryRedirect" is a hidden input to modify
                // after save URL and is not used by actual edit form
-               $request = RequestContext::getMain()->getRequest();
+               $request = $this->context->getRequest();
                $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
 
                switch ( $status->value ) {
@@ -1474,7 +1489,7 @@ class EditPage {
 
                        case self::AS_CANNOT_USE_CUSTOM_MODEL:
                        case self::AS_PARSE_ERROR:
-                               $wgOut->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
+                               $out->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
                                return true;
 
                        case self::AS_SUCCESS_NEW_ARTICLE:
@@ -1487,7 +1502,7 @@ class EditPage {
                                        }
                                }
                                $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
-                               $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
+                               $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
                                return false;
 
                        case self::AS_SUCCESS_UPDATE:
@@ -1515,7 +1530,7 @@ class EditPage {
                                        }
                                }
 
-                               $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
+                               $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
                                return false;
 
                        case self::AS_SPAM_ERROR:
@@ -1523,7 +1538,7 @@ class EditPage {
                                return false;
 
                        case self::AS_BLOCKED_PAGE_FOR_USER:
-                               throw new UserBlockedError( $wgUser->getBlock() );
+                               throw new UserBlockedError( $this->context->getUser()->getBlock() );
 
                        case self::AS_IMAGE_REDIRECT_ANON:
                        case self::AS_IMAGE_REDIRECT_LOGGED:
@@ -1584,7 +1599,7 @@ class EditPage {
 
                // Run new style post-section-merge edit filter
                if ( !Hooks::run( 'EditFilterMergedContent',
-                               [ $this->mArticle->getContext(), $content, $status, $this->summary,
+                               [ $this->context, $content, $status, $this->summary,
                                $user, $this->minoredit ] )
                ) {
                        # Error messages etc. could be handled within the hook...
@@ -1669,10 +1684,11 @@ class EditPage {
         * time.
         */
        function internalAttemptSave( &$result, $bot = false ) {
-               global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
-               global $wgContentHandlerUseDB;
+               global $wgParser, $wgMaxArticleSize, $wgContentHandlerUseDB;
 
                $status = Status::newGood();
+               $user = $this->context->getUser();
+               $request = $this->context->getRequest();
 
                if ( !Hooks::run( 'EditPage::attemptSave', [ $this ] ) ) {
                        wfDebug( "Hook 'EditPage::attemptSave' aborted article saving\n" );
@@ -1681,11 +1697,11 @@ class EditPage {
                        return $status;
                }
 
-               $spam = $wgRequest->getText( 'wpAntispam' );
+               $spam = $request->getText( 'wpAntispam' );
                if ( $spam !== '' ) {
                        wfDebugLog(
                                'SimpleAntiSpam',
-                               $wgUser->getName() .
+                               $user->getName() .
                                ' editing "' .
                                $this->mTitle->getPrefixedText() .
                                '" submitted bogus field "' .
@@ -1714,9 +1730,9 @@ class EditPage {
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
                        $textbox_content->isRedirect() &&
-                       !$wgUser->isAllowed( 'upload' )
+                       !$user->isAllowed( 'upload' )
                ) {
-                               $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
+                               $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
 
                                return $status;
@@ -1741,7 +1757,7 @@ class EditPage {
                }
                if ( $match !== false ) {
                        $result['spam'] = $match;
-                       $ip = $wgRequest->getIP();
+                       $ip = $request->getIP();
                        $pdbk = $this->mTitle->getPrefixedDBkey();
                        $match = str_replace( "\n", '', $match );
                        wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
@@ -1764,10 +1780,10 @@ class EditPage {
                        return $status;
                }
 
-               if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
+               if ( $user->isBlockedFrom( $this->mTitle, false ) ) {
                        // Auto-block user's IP if the account was "hard" blocked
                        if ( !wfReadOnly() ) {
-                               $wgUser->spreadAnyEditBlock();
+                               $user->spreadAnyEditBlock();
                        }
                        # Check block state against master, thus 'false'.
                        $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
@@ -1782,8 +1798,8 @@ class EditPage {
                        return $status;
                }
 
-               if ( !$wgUser->isAllowed( 'edit' ) ) {
-                       if ( $wgUser->isAnon() ) {
+               if ( !$user->isAllowed( 'edit' ) ) {
+                       if ( $user->isAnon() ) {
                                $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
                                return $status;
                        } else {
@@ -1799,7 +1815,7 @@ class EditPage {
                                $status->fatal( 'editpage-cannot-use-custom-model' );
                                $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
                                return $status;
-                       } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
+                       } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) {
                                $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
                                return $status;
 
@@ -1810,7 +1826,7 @@ class EditPage {
 
                if ( $this->changeTags ) {
                        $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
-                               $this->changeTags, $wgUser );
+                               $this->changeTags, $user );
                        if ( !$changeTagsStatus->isOK() ) {
                                $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
                                return $changeTagsStatus;
@@ -1822,7 +1838,7 @@ class EditPage {
                        $status->value = self::AS_READ_ONLY_PAGE;
                        return $status;
                }
-               if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
+               if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 ) ) {
                        $status->fatal( 'actionthrottledtext' );
                        $status->value = self::AS_RATE_LIMITED;
                        return $status;
@@ -1842,7 +1858,7 @@ class EditPage {
 
                if ( $new ) {
                        // Late check for create permission, just in case *PARANOIA*
-                       if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
+                       if ( !$this->mTitle->userCan( 'create', $user ) ) {
                                $status->fatal( 'nocreatetext' );
                                $status->value = self::AS_NO_CREATE_PERMISSION;
                                wfDebug( __METHOD__ . ": no create permission\n" );
@@ -1866,7 +1882,7 @@ class EditPage {
                                return $status;
                        }
 
-                       if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) {
+                       if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
                                return $status;
                        }
 
@@ -1902,7 +1918,7 @@ class EditPage {
                        ) {
                                $this->isConflict = true;
                                if ( $this->section == 'new' ) {
-                                       if ( $this->page->getUserText() == $wgUser->getName() &&
+                                       if ( $this->page->getUserText() == $user->getName() &&
                                                $this->page->getComment() == $this->newSectionSummary()
                                        ) {
                                                // Probably a duplicate submission of a new comment.
@@ -1918,7 +1934,7 @@ class EditPage {
                                } elseif ( $this->section == ''
                                        && Revision::userWasLastToEdit(
                                                DB_MASTER, $this->mTitle->getArticleID(),
-                                               $wgUser->getId(), $this->edittime
+                                               $user->getId(), $this->edittime
                                        )
                                ) {
                                        # Suppress edit conflict with self, except for section edits where merging is required.
@@ -1988,7 +2004,7 @@ class EditPage {
                                return $status;
                        }
 
-                       if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
+                       if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
                                return $status;
                        }
 
@@ -2009,7 +2025,7 @@ class EditPage {
                                        return $status;
                                }
                        } elseif ( !$this->allowBlankSummary
-                               && !$content->equals( $this->getOriginalContent( $wgUser ) )
+                               && !$content->equals( $this->getOriginalContent( $user ) )
                                && !$content->isRedirect()
                                && md5( $this->summary ) == $this->autoSumm
                        ) {
@@ -2079,7 +2095,7 @@ class EditPage {
                        $this->summary,
                        $flags,
                        false,
-                       $wgUser,
+                       $user,
                        $content->getDefaultFormat(),
                        $this->changeTags
                );
@@ -2102,7 +2118,7 @@ class EditPage {
                $result['nullEdit'] = $doEditStatus->hasMessage( 'edit-no-change' );
                if ( $result['nullEdit'] ) {
                        // We don't know if it was a null edit until now, so increment here
-                       $wgUser->pingLimiter( 'linkpurge' );
+                       $user->pingLimiter( 'linkpurge' );
                }
                $result['redirect'] = $content->isRedirect();
 
@@ -2111,7 +2127,7 @@ class EditPage {
                // If the content model changed, add a log entry
                if ( $changingContentModel ) {
                        $this->addContentModelChangeLogEntry(
-                               $wgUser,
+                               $user,
                                $new ? false : $oldContentModel,
                                $this->contentModel,
                                $this->summary
@@ -2145,13 +2161,12 @@ class EditPage {
         * Register the change of watch status
         */
        protected function updateWatchlist() {
-               global $wgUser;
+               $user = $this->context->getUser();
 
-               if ( !$wgUser->isLoggedIn() ) {
+               if ( !$user->isLoggedIn() ) {
                        return;
                }
 
-               $user = $wgUser;
                $title = $this->mTitle;
                $watch = $this->watchthis;
                // Do this in its own transaction to reduce contention...
@@ -2266,29 +2281,32 @@ class EditPage {
        }
 
        function setHeaders() {
-               global $wgOut, $wgUser, $wgAjaxEditStash;
+               global $wgAjaxEditStash;
+
+               $out = $this->context->getOutput();
+               $user = $this->context->getUser();
 
-               $wgOut->addModules( 'mediawiki.action.edit' );
-               $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
+               $out->addModules( 'mediawiki.action.edit' );
+               $out->addModuleStyles( 'mediawiki.action.edit.styles' );
 
-               if ( $wgUser->getOption( 'showtoolbar' ) ) {
+               if ( $user->getOption( 'showtoolbar' ) ) {
                        // The addition of default buttons is handled by getEditToolbar() which
                        // has its own dependency on this module. The call here ensures the module
                        // is loaded in time (it has position "top") for other modules to register
                        // buttons (e.g. extensions, gadgets, user scripts).
-                       $wgOut->addModules( 'mediawiki.toolbar' );
+                       $out->addModules( 'mediawiki.toolbar' );
                }
 
-               if ( $wgUser->getOption( 'uselivepreview' ) ) {
-                       $wgOut->addModules( 'mediawiki.action.edit.preview' );
+               if ( $user->getOption( 'uselivepreview' ) ) {
+                       $out->addModules( 'mediawiki.action.edit.preview' );
                }
 
-               if ( $wgUser->getOption( 'useeditwarning' ) ) {
-                       $wgOut->addModules( 'mediawiki.action.edit.editWarning' );
+               if ( $user->getOption( 'useeditwarning' ) ) {
+                       $out->addModules( 'mediawiki.action.edit.editWarning' );
                }
 
                # Enabled article-related sidebar, toplinks, etc.
-               $wgOut->setArticleRelated( true );
+               $out->setArticleRelated( true );
 
                $contextTitle = $this->getContextTitle();
                if ( $this->isConflict ) {
@@ -2311,10 +2329,10 @@ class EditPage {
                if ( $displayTitle === false ) {
                        $displayTitle = $contextTitle->getPrefixedText();
                }
-               $wgOut->setPageTitle( wfMessage( $msg, $displayTitle ) );
+               $out->setPageTitle( wfMessage( $msg, $displayTitle ) );
                # Transmit the name of the message to JavaScript for live preview
                # Keep Resources.php/mediawiki.action.edit.preview in sync with the possible keys
-               $wgOut->addJsConfigVars( [
+               $out->addJsConfigVars( [
                        'wgEditMessage' => $msg,
                        'wgAjaxEditStash' => $wgAjaxEditStash,
                ] );
@@ -2324,16 +2342,16 @@ class EditPage {
         * Show all applicable editing introductions
         */
        protected function showIntro() {
-               global $wgOut, $wgUser;
                if ( $this->suppressIntro ) {
                        return;
                }
 
+               $out = $this->context->getOutput();
                $namespace = $this->mTitle->getNamespace();
 
                if ( $namespace == NS_MEDIAWIKI ) {
                        # Show a warning if editing an interface message
-                       $wgOut->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
+                       $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
                        # If this is a default message (but not css or js),
                        # show a hint that it is translatable on translatewiki.net
                        if ( !$this->mTitle->hasContentModel( CONTENT_MODEL_CSS )
@@ -2341,7 +2359,7 @@ class EditPage {
                        ) {
                                $defaultMessageText = $this->mTitle->getDefaultMessageText();
                                if ( $defaultMessageText !== false ) {
-                                       $wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
+                                       $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
                                                'translateinterface' );
                                }
                        }
@@ -2353,11 +2371,11 @@ class EditPage {
                                # there must be a description url to show a hint to shared repo
                                if ( $descUrl ) {
                                        if ( !$this->mTitle->exists() ) {
-                                               $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
+                                               $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
                                                                        'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
                                                ] );
                                        } else {
-                                               $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
+                                               $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
                                                                        'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
                                                ] );
                                        }
@@ -2373,12 +2391,12 @@ class EditPage {
                        $ip = User::isIP( $username );
                        $block = Block::newFromTarget( $user, $user );
                        if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
-                               $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
+                               $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
                                        [ 'userpage-userdoesnotexist', wfEscapeWikiText( $username ) ] );
                        } elseif ( !is_null( $block ) && $block->getType() != Block::TYPE_AUTO ) {
                                # Show log extract if the user is currently blocked
                                LogEventsList::showLogExtract(
-                                       $wgOut,
+                                       $out,
                                        'block',
                                        MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
                                        '',
@@ -2398,8 +2416,8 @@ class EditPage {
                        $helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
                                wfMessage( 'helppage' )->inContentLanguage()->text()
                        ) );
-                       if ( $wgUser->isLoggedIn() ) {
-                               $wgOut->wrapWikiMsg(
+                       if ( $this->context->getUser()->isLoggedIn() ) {
+                               $out->wrapWikiMsg(
                                        // Suppress the external link icon, consider the help url an internal one
                                        "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
                                        [
@@ -2408,7 +2426,7 @@ class EditPage {
                                        ]
                                );
                        } else {
-                               $wgOut->wrapWikiMsg(
+                               $out->wrapWikiMsg(
                                        // Suppress the external link icon, consider the help url an internal one
                                        "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
                                        [
@@ -2420,7 +2438,7 @@ class EditPage {
                }
                # Give a notice if the user is editing a deleted/moved page...
                if ( !$this->mTitle->exists() ) {
-                       LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
+                       LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
                                '',
                                [
                                        'lim' => 10,
@@ -2441,9 +2459,8 @@ class EditPage {
                if ( $this->editintro ) {
                        $title = Title::newFromText( $this->editintro );
                        if ( $title instanceof Title && $title->exists() && $title->userCan( 'read' ) ) {
-                               global $wgOut;
                                // Added using template syntax, to take <noinclude>'s into account.
-                               $wgOut->addWikiTextTitleTidy(
+                               $this->context->getOutput()->addWikiTextTitleTidy(
                                        '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
                                        $this->mTitle
                                );
@@ -2493,7 +2510,7 @@ class EditPage {
         * content.
         *
         * @param string|null|bool $text Text to unserialize
-        * @return Content The content object created from $text. If $text was false
+        * @return Content|bool|null The content object created from $text. If $text was false
         *   or null, false resp. null will be  returned instead.
         *
         * @throws MWException If unserializing the text results in a Content
@@ -2525,8 +2542,6 @@ class EditPage {
         * use the EditPage::showEditForm:fields hook instead.
         */
        function showEditForm( $formCallback = null ) {
-               global $wgOut, $wgUser;
-
                # need to parse the preview early so that we know which templates are used,
                # otherwise users with "show preview after edit box" will get a blank list
                # we parse this near the beginning so that setHeaders can do the title
@@ -2536,7 +2551,8 @@ class EditPage {
                        $previewOutput = $this->getPreviewText();
                }
 
-               Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$wgOut ] );
+               $out = $this->context->getOutput();
+               Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$out ] );
 
                $this->setHeaders();
 
@@ -2544,13 +2560,14 @@ class EditPage {
                        return;
                }
 
-               $wgOut->addHTML( $this->editFormPageTop );
+               $out->addHTML( $this->editFormPageTop );
 
-               if ( $wgUser->getOption( 'previewontop' ) ) {
+               $user = $this->context->getUser();
+               if ( $user->getOption( 'previewontop' ) ) {
                        $this->displayPreviewArea( $previewOutput, true );
                }
 
-               $wgOut->addHTML( $this->editFormTextTop );
+               $out->addHTML( $this->editFormTextTop );
 
                $showToolbar = true;
                if ( $this->wasDeletedSinceLastEdit() ) {
@@ -2559,14 +2576,14 @@ class EditPage {
                                // Add an confirmation checkbox and explanation.
                                $showToolbar = false;
                        } else {
-                               $wgOut->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
+                               $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
                                        'deletedwhileediting' );
                        }
                }
 
                // @todo add EditForm plugin interface and use it here!
                //       search for textarea1 and textares2, and allow EditForm to override all uses.
-               $wgOut->addHTML( Html::openElement(
+               $out->addHTML( Html::openElement(
                        'form',
                        [
                                'id' => self::EDITFORM_ID,
@@ -2579,11 +2596,11 @@ class EditPage {
 
                if ( is_callable( $formCallback ) ) {
                        wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
-                       call_user_func_array( $formCallback, [ &$wgOut ] );
+                       call_user_func_array( $formCallback, [ &$out ] );
                }
 
                // Add an empty field to trip up spambots
-               $wgOut->addHTML(
+               $out->addHTML(
                        Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
                        . Html::rawElement(
                                'label',
@@ -2602,7 +2619,7 @@ class EditPage {
                        . Xml::closeElement( 'div' )
                );
 
-               Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$wgOut ] );
+               Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$out ] );
 
                // Put these up at the top to ensure they aren't lost on early form submission
                $this->showFormBeforeText();
@@ -2616,7 +2633,7 @@ class EditPage {
                        $key = $comment === ''
                                ? 'confirmrecreate-noreason'
                                : 'confirmrecreate';
-                       $wgOut->addHTML(
+                       $out->addHTML(
                                '<div class="mw-confirm-recreate">' .
                                        wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
                                Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
@@ -2628,7 +2645,7 @@ class EditPage {
 
                # When the summary is hidden, also hide them on preview/show changes
                if ( $this->nosummary ) {
-                       $wgOut->addHTML( Html::hidden( 'nosummary', true ) );
+                       $out->addHTML( Html::hidden( 'nosummary', true ) );
                }
 
                # If a blank edit summary was previously provided, and the appropriate
@@ -2639,15 +2656,15 @@ class EditPage {
                # For a bit more sophisticated detection of blank summaries, hash the
                # automatic one and pass that in the hidden field wpAutoSummary.
                if ( $this->missingSummary || ( $this->section == 'new' && $this->nosummary ) ) {
-                       $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
+                       $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
                }
 
                if ( $this->undidRev ) {
-                       $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
+                       $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
                }
 
                if ( $this->selfRedirect ) {
-                       $wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
+                       $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
                }
 
                if ( $this->hasPresetSummary ) {
@@ -2658,27 +2675,27 @@ class EditPage {
                }
 
                $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
-               $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
+               $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
 
-               $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
-               $wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
+               $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
+               $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
 
-               $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
-               $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
+               $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
+               $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
 
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
-                       $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
+                       $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
                }
 
-               $wgOut->addHTML( $this->editFormTextBeforeContent );
+               $out->addHTML( $this->editFormTextBeforeContent );
 
-               if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
-                       $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
+               if ( !$this->isCssJsSubpage && $showToolbar && $user->getOption( 'showtoolbar' ) ) {
+                       $out->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
                }
 
                if ( $this->blankArticle ) {
-                       $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
+                       $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
                }
 
                if ( $this->isConflict ) {
@@ -2696,7 +2713,7 @@ class EditPage {
                        $this->showContentForm();
                }
 
-               $wgOut->addHTML( $this->editFormTextAfterContent );
+               $out->addHTML( $this->editFormTextAfterContent );
 
                $this->showStandardInputs();
 
@@ -2706,19 +2723,19 @@ class EditPage {
 
                $this->showEditTools();
 
-               $wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
+               $out->addHTML( $this->editFormTextAfterTools . "\n" );
 
-               $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
+               $out->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
                        Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
 
-               $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
+               $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
                        Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
 
                if ( $this->mParserOutput ) {
-                       $wgOut->setLimitReportData( $this->mParserOutput->getLimitReportData() );
+                       $out->setLimitReportData( $this->mParserOutput->getLimitReportData() );
                }
 
-               $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+               $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
 
                if ( $this->isConflict ) {
                        try {
@@ -2731,7 +2748,7 @@ class EditPage {
                                        $this->contentFormat,
                                        $ex->getMessage()
                                );
-                               $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+                               $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
                        }
                }
 
@@ -2745,14 +2762,14 @@ class EditPage {
                } else {
                        $mode = 'text';
                }
-               $wgOut->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
+               $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
 
                // Marker for detecting truncated form data.  This must be the last
                // parameter sent in order to be of use, so do not move me.
-               $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
-               $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
+               $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
+               $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
 
-               if ( !$wgUser->getOption( 'previewontop' ) ) {
+               if ( !$user->getOption( 'previewontop' ) ) {
                        $this->displayPreviewArea( $previewOutput, false );
                }
 
@@ -2778,21 +2795,23 @@ class EditPage {
         * @return bool
         */
        protected function showHeader() {
-               global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang;
-               global $wgAllowUserCss, $wgAllowUserJs;
+               global $wgMaxArticleSize, $wgAllowUserCss, $wgAllowUserJs;
+
+               $out = $this->context->getOutput();
+               $user = $this->context->getUser();
 
                if ( $this->mTitle->isTalkPage() ) {
-                       $wgOut->addWikiMsg( 'talkpagetext' );
+                       $out->addWikiMsg( 'talkpagetext' );
                }
 
                // Add edit notices
                $editNotices = $this->mTitle->getEditNotices( $this->oldid );
                if ( count( $editNotices ) ) {
-                       $wgOut->addHTML( implode( "\n", $editNotices ) );
+                       $out->addHTML( implode( "\n", $editNotices ) );
                } else {
                        $msg = wfMessage( 'editnotice-notext' );
                        if ( !$msg->isDisabled() ) {
-                               $wgOut->addHTML(
+                               $out->addHTML(
                                        '<div class="mw-editnotice-notext">'
                                        . $msg->parseAsBlock()
                                        . '</div>'
@@ -2801,14 +2820,14 @@ class EditPage {
                }
 
                if ( $this->isConflict ) {
-                       $wgOut->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
+                       $out->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
                        $this->editRevId = $this->page->getLatest();
                } else {
                        if ( $this->section != '' && !$this->isSectionEditSupported() ) {
                                // We use $this->section to much before this and getVal('wgSection') directly in other places
                                // at this point we can't reset $this->section to '' to fallback to non-section editing.
                                // Someone is welcome to try refactoring though
-                               $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
+                               $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
                                return false;
                        }
 
@@ -2822,31 +2841,31 @@ class EditPage {
                        }
 
                        if ( $this->missingComment ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
+                               $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
                        }
 
                        if ( $this->missingSummary && $this->section != 'new' ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
+                               $out->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
                        }
 
                        if ( $this->missingSummary && $this->section == 'new' ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
+                               $out->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
                        }
 
                        if ( $this->blankArticle ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
+                               $out->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
                        }
 
                        if ( $this->selfRedirect ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
+                               $out->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
                        }
 
                        if ( $this->hookError !== '' ) {
-                               $wgOut->addWikiText( $this->hookError );
+                               $out->addWikiText( $this->hookError );
                        }
 
                        if ( !$this->checkUnicodeCompliantBrowser() ) {
-                               $wgOut->addWikiMsg( 'nonunicodebrowser' );
+                               $out->addWikiMsg( 'nonunicodebrowser' );
                        }
 
                        if ( $this->section != 'new' ) {
@@ -2854,13 +2873,13 @@ class EditPage {
                                if ( $revision ) {
                                        // Let sysop know that this will make private content public if saved
 
-                                       if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
-                                               $wgOut->wrapWikiMsg(
+                                       if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) {
+                                               $out->wrapWikiMsg(
                                                        "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
                                                        'rev-deleted-text-permission'
                                                );
                                        } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
-                                               $wgOut->wrapWikiMsg(
+                                               $out->wrapWikiMsg(
                                                        "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
                                                        'rev-deleted-text-view'
                                                );
@@ -2868,25 +2887,25 @@ class EditPage {
 
                                        if ( !$revision->isCurrent() ) {
                                                $this->mArticle->setOldSubtitle( $revision->getId() );
-                                               $wgOut->addWikiMsg( 'editingold' );
+                                               $out->addWikiMsg( 'editingold' );
                                        }
                                } elseif ( $this->mTitle->exists() ) {
                                        // Something went wrong
 
-                                       $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
+                                       $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
                                                [ 'missing-revision', $this->oldid ] );
                                }
                        }
                }
 
                if ( wfReadOnly() ) {
-                       $wgOut->wrapWikiMsg(
+                       $out->wrapWikiMsg(
                                "<div id=\"mw-read-only-warning\">\n$1\n</div>",
                                [ 'readonlywarning', wfReadOnlyReason() ]
                        );
-               } elseif ( $wgUser->isAnon() ) {
+               } elseif ( $user->isAnon() ) {
                        if ( $this->formtype != 'preview' ) {
-                               $wgOut->wrapWikiMsg(
+                               $out->wrapWikiMsg(
                                        "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
                                        [ 'anoneditwarning',
                                                // Log-in link
@@ -2900,7 +2919,7 @@ class EditPage {
                                        ]
                                );
                        } else {
-                               $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
+                               $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
                                        'anonpreviewwarning'
                                );
                        }
@@ -2908,22 +2927,25 @@ class EditPage {
                        if ( $this->isCssJsSubpage ) {
                                # Check the skin exists
                                if ( $this->isWrongCaseCssJsPage ) {
-                                       $wgOut->wrapWikiMsg(
+                                       $out->wrapWikiMsg(
                                                "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
                                                [ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
                                        );
                                }
-                               if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
+                               if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
+                                       $out->wrapWikiMsg( '<div class="mw-usercssjspublic">$1</div>',
+                                               $this->isCssSubpage ? 'usercssispublic' : 'userjsispublic'
+                                       );
                                        if ( $this->formtype !== 'preview' ) {
                                                if ( $this->isCssSubpage && $wgAllowUserCss ) {
-                                                       $wgOut->wrapWikiMsg(
+                                                       $out->wrapWikiMsg(
                                                                "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
                                                                [ 'usercssyoucanpreview' ]
                                                        );
                                                }
 
                                                if ( $this->isJsSubpage && $wgAllowUserJs ) {
-                                                       $wgOut->wrapWikiMsg(
+                                                       $out->wrapWikiMsg(
                                                                "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
                                                                [ 'userjsyoucanpreview' ]
                                                        );
@@ -2943,7 +2965,7 @@ class EditPage {
                                # Then it must be protected based on static groups (regular)
                                $noticeMsg = 'protectedpagewarning';
                        }
-                       LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
+                       LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
                                [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
                }
                if ( $this->mTitle->isCascadeProtected() ) {
@@ -2959,10 +2981,10 @@ class EditPage {
                                }
                        }
                        $notice .= '</div>';
-                       $wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
+                       $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
                }
                if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
-                       LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
+                       LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
                                [ 'lim' => 1,
                                        'showIfEmpty' => false,
                                        'msgKey' => [ 'titleprotectedwarning' ],
@@ -2973,20 +2995,21 @@ class EditPage {
                        $this->contentLength = strlen( $this->textbox1 );
                }
 
+               $lang = $this->context->getLanguage();
                if ( $this->tooBig || $this->contentLength > $wgMaxArticleSize * 1024 ) {
-                       $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
+                       $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
                                [
                                        'longpageerror',
-                                       $wgLang->formatNum( round( $this->contentLength / 1024, 3 ) ),
-                                       $wgLang->formatNum( $wgMaxArticleSize )
+                                       $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
+                                       $lang->formatNum( $wgMaxArticleSize )
                                ]
                        );
                } else {
                        if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
-                               $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
+                               $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
                                        [
                                                'longpage-hint',
-                                               $wgLang->formatSize( strlen( $this->textbox1 ) ),
+                                               $lang->formatSize( strlen( $this->textbox1 ) ),
                                                strlen( $this->textbox1 )
                                        ]
                                );
@@ -3051,7 +3074,6 @@ class EditPage {
         * @param string $summary The text of the summary to display
         */
        protected function showSummaryInput( $isSubjectPreview, $summary = "" ) {
-               global $wgOut;
                # Add a class if 'missingsummary' is triggered to allow styling of the summary line
                $summaryClass = $this->missingSummary ? 'mw-summarymissed' : 'mw-summary';
                if ( $isSubjectPreview ) {
@@ -3070,7 +3092,7 @@ class EditPage {
                        [ 'class' => $summaryClass ],
                        []
                );
-               $wgOut->addHTML( "{$label} {$input}" );
+               $this->context->getOutput()->addHTML( "{$label} {$input}" );
        }
 
        /**
@@ -3102,9 +3124,9 @@ class EditPage {
        }
 
        protected function showFormBeforeText() {
-               global $wgOut;
                $section = htmlspecialchars( $this->section );
-               $wgOut->addHTML( <<<HTML
+               $out = $this->context->getOutput();
+               $out->addHTML( <<<HTML
 <input type='hidden' value="{$section}" name="wpSection"/>
 <input type='hidden' value="{$this->starttime}" name="wpStarttime" />
 <input type='hidden' value="{$this->edittime}" name="wpEdittime" />
@@ -3114,12 +3136,11 @@ class EditPage {
 HTML
                );
                if ( !$this->checkUnicodeCompliantBrowser() ) {
-                       $wgOut->addHTML( Html::hidden( 'safemode', '1' ) );
+                       $out->addHTML( Html::hidden( 'safemode', '1' ) );
                }
        }
 
        protected function showFormAfterText() {
-               global $wgOut, $wgUser;
                /**
                 * To make it harder for someone to slip a user a page
                 * which submits an edit form to the wiki without their
@@ -3132,7 +3153,10 @@ HTML
                 * include the constant suffix to prevent editing from
                 * broken text-mangling proxies.
                 */
-               $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
+               $token = $this->context->getUser()->getEditToken();
+               $this->context->getOutput()->addHTML(
+                       "\n" . Html::hidden( "wpEditToken", $token ) . "\n"
+               );
        }
 
        /**
@@ -3202,8 +3226,6 @@ HTML
        }
 
        protected function showTextbox( $text, $name, $customAttribs = [] ) {
-               global $wgOut, $wgUser;
-
                $wikitext = $this->safeUnicodeOutput( $text );
                if ( strval( $wikitext ) !== '' ) {
                        // Ensure there's a newline at the end, otherwise adding lines
@@ -3213,11 +3235,12 @@ HTML
                        $wikitext .= "\n";
                }
 
+               $user = $this->context->getUser();
                $attribs = $customAttribs + [
                        'accesskey' => ',',
                        'id' => $name,
-                       'cols' => $wgUser->getIntOption( 'cols' ),
-                       'rows' => $wgUser->getIntOption( 'rows' ),
+                       'cols' => $user->getIntOption( 'cols' ),
+                       'rows' => $user->getIntOption( 'rows' ),
                        // Avoid PHP notices when appending preferences
                        // (appending allows customAttribs['style'] to still work).
                        'style' => ''
@@ -3227,11 +3250,10 @@ HTML
                $attribs['lang'] = $pageLang->getHtmlCode();
                $attribs['dir'] = $pageLang->getDir();
 
-               $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
+               $this->context->getOutput()->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
        }
 
        protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
-               global $wgOut;
                $classes = [];
                if ( $isOnTop ) {
                        $classes[] = 'ontop';
@@ -3243,7 +3265,8 @@ HTML
                        $attribs['style'] = 'display: none;';
                }
 
-               $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
+               $out = $this->context->getOutput();
+               $out->addHTML( Xml::openElement( 'div', $attribs ) );
 
                if ( $this->formtype == 'preview' ) {
                        $this->showPreview( $previewOutput );
@@ -3252,10 +3275,10 @@ HTML
                        $pageViewLang = $this->mTitle->getPageViewLanguage();
                        $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
                                'class' => 'mw-content-' . $pageViewLang->getDir() ];
-                       $wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
+                       $out->addHTML( Html::rawElement( 'div', $attribs ) );
                }
 
-               $wgOut->addHTML( '</div>' );
+               $out->addHTML( '</div>' );
 
                if ( $this->formtype == 'diff' ) {
                        try {
@@ -3267,7 +3290,7 @@ HTML
                                        $this->contentFormat,
                                        $ex->getMessage()
                                );
-                               $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+                               $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
                        }
                }
        }
@@ -3279,14 +3302,14 @@ HTML
         * @param string $text The HTML to be output for the preview.
         */
        protected function showPreview( $text ) {
-               global $wgOut;
                if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
                        $this->mArticle->openShowCategory();
                }
+               $out = $this->context->getOutput();
                # This hook seems slightly odd here, but makes things more
                # consistent for extensions.
-               Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
-               $wgOut->addHTML( $text );
+               Hooks::run( 'OutputPageBeforeHTML', [ &$out, &$text ] );
+               $out->addHTML( $text );
                if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
                        $this->mArticle->closeShowCategory();
                }
@@ -3300,7 +3323,7 @@ HTML
         * save and then make a comparison.
         */
        function showDiff() {
-               global $wgUser, $wgContLang, $wgOut;
+               global $wgContLang;
 
                $oldtitlemsg = 'currentrev';
                # if message does not exist, show diff against the preloaded default
@@ -3331,8 +3354,9 @@ HTML
                        ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
                        Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
 
-                       $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
-                       $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
+                       $user = $this->context->getUser();
+                       $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
+                       $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
                }
 
                if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
@@ -3356,7 +3380,7 @@ HTML
                        $difftext = '';
                }
 
-               $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
+               $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
        }
 
        /**
@@ -3365,8 +3389,7 @@ HTML
        protected function showHeaderCopyrightWarning() {
                $msg = 'editpage-head-copy-warn';
                if ( !wfMessage( $msg )->isDisabled() ) {
-                       global $wgOut;
-                       $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
+                       $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
                                'editpage-head-copy-warn' );
                }
        }
@@ -3383,16 +3406,15 @@ HTML
                $msg = 'editpage-tos-summary';
                Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
                if ( !wfMessage( $msg )->isDisabled() ) {
-                       global $wgOut;
-                       $wgOut->addHTML( '<div class="mw-tos-summary">' );
-                       $wgOut->addWikiMsg( $msg );
-                       $wgOut->addHTML( '</div>' );
+                       $out = $this->context->getOutput();
+                       $out->addHTML( '<div class="mw-tos-summary">' );
+                       $out->addWikiMsg( $msg );
+                       $out->addHTML( '</div>' );
                }
        }
 
        protected function showEditTools() {
-               global $wgOut;
-               $wgOut->addHTML( '<div class="mw-editTools">' .
+               $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
                        wfMessage( 'edittools' )->inContentLanguage()->parse() .
                        '</div>' );
        }
@@ -3452,24 +3474,24 @@ HTML
        }
 
        protected function showStandardInputs( &$tabindex = 2 ) {
-               global $wgOut;
-               $wgOut->addHTML( "<div class='editOptions'>\n" );
+               $out = $this->context->getOutput();
+               $out->addHTML( "<div class='editOptions'>\n" );
 
                if ( $this->section != 'new' ) {
                        $this->showSummaryInput( false, $this->summary );
-                       $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
+                       $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
                }
 
                $checkboxes = $this->getCheckboxes( $tabindex,
                        [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
-               $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
+               $out->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
 
                // Show copyright warning.
-               $wgOut->addWikiText( $this->getCopywarn() );
-               $wgOut->addHTML( $this->editFormTextAfterWarn );
+               $out->addWikiText( $this->getCopywarn() );
+               $out->addHTML( $this->editFormTextAfterWarn );
 
-               $wgOut->addHTML( "<div class='editButtons'>\n" );
-               $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
+               $out->addHTML( "<div class='editButtons'>\n" );
+               $out->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
 
                $cancel = $this->getCancelLink();
                if ( $cancel !== '' ) {
@@ -3489,13 +3511,13 @@ HTML
                        wfMessage( 'word-separator' )->escaped() .
                        wfMessage( 'newwindow' )->parse();
 
-               $wgOut->addHTML( "      <span class='cancelLink'>{$cancel}</span>\n" );
-               $wgOut->addHTML( "      <span class='editHelp'>{$edithelp}</span>\n" );
-               $wgOut->addHTML( "</div><!-- editButtons -->\n" );
+               $out->addHTML( "        <span class='cancelLink'>{$cancel}</span>\n" );
+               $out->addHTML( "        <span class='editHelp'>{$edithelp}</span>\n" );
+               $out->addHTML( "</div><!-- editButtons -->\n" );
 
-               Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
+               Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
 
-               $wgOut->addHTML( "</div><!-- editOptions -->\n" );
+               $out->addHTML( "</div><!-- editOptions -->\n" );
        }
 
        /**
@@ -3503,10 +3525,9 @@ HTML
         * If you want to use another entry point to this function, be careful.
         */
        protected function showConflict() {
-               global $wgOut;
-
-               if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
-                       $stats = $wgOut->getContext()->getStats();
+               $out = $this->context->getOutput();
+               if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$out ] ) ) {
+                       $stats = $this->context->getStats();
                        $stats->increment( 'edit.failures.conflict' );
                        // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
                        if (
@@ -3516,7 +3537,7 @@ HTML
                                $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
                        }
 
-                       $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
+                       $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
 
                        $content1 = $this->toEditContent( $this->textbox1 );
                        $content2 = $this->toEditContent( $this->textbox2 );
@@ -3529,7 +3550,7 @@ HTML
                                wfMessage( 'storedversion' )->text()
                        );
 
-                       $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
+                       $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
                        $this->showTextbox2();
                }
        }
@@ -3642,10 +3663,10 @@ HTML
         * @return string
         */
        function getPreviewText() {
-               global $wgOut, $wgRawHtml, $wgLang;
-               global $wgAllowUserCss, $wgAllowUserJs;
+               global $wgRawHtml, $wgAllowUserCss, $wgAllowUserJs;
 
-               $stats = $wgOut->getContext()->getStats();
+               $stats = $this->context->getStats();
+               $out = $this->context->getOutput();
 
                if ( $wgRawHtml && !$this->mTokenOk ) {
                        // Could be an offsite preview attempt. This is very unsafe if
@@ -3655,7 +3676,7 @@ HTML
                                // Do not put big scary notice, if previewing the empty
                                // string, which happens when you initially edit
                                // a category page, due to automatic preview-on-open.
-                               $parsedNote = $wgOut->parse( "<div class='previewnote'>" .
+                               $parsedNote = $out->parse( "<div class='previewnote'>" .
                                        wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
                        }
                        $stats->increment( 'edit.failures.session_loss' );
@@ -3677,7 +3698,7 @@ HTML
 
                        # provide a anchor link to the editform
                        $continueEditing = '<span class="mw-continue-editing">' .
-                               '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
+                               '[[#' . self::EDITFORM_ID . '|' . $this->context->getLanguage()->getArrow() . ' ' .
                                wfMessage( 'continue-editing' )->text() . ']]</span>';
                        if ( $this->mTriedSave && !$this->mTokenOk ) {
                                if ( $this->mTokenOkExceptSuffix ) {
@@ -3743,7 +3764,7 @@ HTML
                        $parserOutput = $parserResult['parserOutput'];
                        $previewHTML = $parserResult['html'];
                        $this->mParserOutput = $parserOutput;
-                       $wgOut->addParserOutputMetadata( $parserOutput );
+                       $out->addParserOutputMetadata( $parserOutput );
 
                        if ( count( $parserOutput->getWarnings() ) ) {
                                $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
@@ -3769,7 +3790,7 @@ HTML
 
                $previewhead = "<div class='previewnote'>\n" .
                        '<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
-                       $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
+                       $out->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
 
                $pageViewLang = $this->mTitle->getPageViewLanguage();
                $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
@@ -3784,7 +3805,7 @@ HTML
         * @return ParserOptions
         */
        protected function getPreviewParserOptions() {
-               $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
+               $parserOptions = $this->page->makeParserOptions( $this->context );
                $parserOptions->setIsPreview( true );
                $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
                $parserOptions->enableLimitReport();
@@ -3795,17 +3816,17 @@ HTML
         * Parse the page for a preview. Subclasses may override this class, in order
         * to parse with different options, or to otherwise modify the preview HTML.
         *
-        * @param Content @content The page content
-        * @return Associative array with keys:
+        * @param Content $content The page content
+        * @return array with keys:
         *   - parserOutput: The ParserOutput object
         *   - html: The HTML to be displayed
         */
        protected function doPreviewParse( Content $content ) {
-               global $wgUser;
+               $user = $this->context->getUser();
                $parserOptions = $this->getPreviewParserOptions();
-               $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
+               $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
                $scopedCallback = $parserOptions->setupFakeRevision(
-                       $this->mTitle, $pstContent, $wgUser );
+                       $this->mTitle, $pstContent, $user );
                $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
                ScopedCallback::consume( $scopedCallback );
                $parserOutput->setEditSectionTokens( false ); // no section edit links
@@ -3981,15 +4002,16 @@ HTML
         * @return array
         */
        public function getCheckboxes( &$tabindex, $checked ) {
-               global $wgUser, $wgUseMediaWikiUIEverywhere;
+               global $wgUseMediaWikiUIEverywhere;
 
                $checkboxes = [];
+               $user = $this->context->getUser();
 
                // don't show the minor edit checkbox if it's a new page or section
                if ( !$this->isNew ) {
                        $checkboxes['minor'] = '';
                        $minorLabel = wfMessage( 'minoredit' )->parse();
-                       if ( $wgUser->isAllowed( 'minoredit' ) ) {
+                       if ( $user->isAllowed( 'minoredit' ) ) {
                                $attribs = [
                                        'tabindex' => ++$tabindex,
                                        'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
@@ -4013,7 +4035,7 @@ HTML
 
                $watchLabel = wfMessage( 'watchthis' )->parse();
                $checkboxes['watch'] = '';
-               if ( $wgUser->isLoggedIn() ) {
+               if ( $user->isLoggedIn() ) {
                        $attribs = [
                                'tabindex' => ++$tabindex,
                                'accesskey' => wfMessage( 'accesskey-watch' )->text(),
@@ -4047,13 +4069,19 @@ HTML
        public function getEditButtons( &$tabindex ) {
                $buttons = [];
 
+               $labelAsPublish = $this->mArticle->getContext()->getConfig()->get( 'EditButtonPublishNotSave' );
+               if ( $labelAsPublish ) {
+                       $buttonLabelKey = $this->isNew ? 'publishpage' : 'publishchanges';
+               } else {
+                       $buttonLabelKey = $this->isNew ? 'savearticle' : 'savechanges';
+               }
+               $buttonLabel = wfMessage( $buttonLabelKey )->text();
                $attribs = [
                        'id' => 'wpSave',
                        'name' => 'wpSave',
                        'tabindex' => ++$tabindex,
                ] + Linker::tooltipAndAccesskeyAttribs( 'save' );
-               $buttons['save'] = Html::submitButton( wfMessage( 'savearticle' )->text(),
-                       $attribs, [ 'mw-ui-constructive' ] );
+               $buttons['save'] = Html::submitButton( $buttonLabel, $attribs, [ 'mw-ui-constructive' ] );
 
                ++$tabindex; // use the same for preview and live preview
                $attribs = [
@@ -4082,15 +4110,14 @@ HTML
         * they have attempted to edit a nonexistent section.
         */
        function noSuchSectionPage() {
-               global $wgOut;
-
-               $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
+               $out = $this->context->getOutput();
+               $out->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
 
                $res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
                Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
-               $wgOut->addHTML( $res );
+               $out->addHTML( $res );
 
-               $wgOut->returnToMain( false, $this->mTitle );
+               $out->returnToMain( false, $this->mTitle );
        }
 
        /**
@@ -4099,28 +4126,28 @@ HTML
         * @param string|array|bool $match Text (or array of texts) which triggered one or more filters
         */
        public function spamPageWithContent( $match = false ) {
-               global $wgOut, $wgLang;
                $this->textbox2 = $this->textbox1;
 
                if ( is_array( $match ) ) {
-                       $match = $wgLang->listToText( $match );
+                       $match = $this->context->getLanguage()->listToText( $match );
                }
-               $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
+               $out = $this->context->getOutput();
+               $out->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
 
-               $wgOut->addHTML( '<div id="spamprotected">' );
-               $wgOut->addWikiMsg( 'spamprotectiontext' );
+               $out->addHTML( '<div id="spamprotected">' );
+               $out->addWikiMsg( 'spamprotectiontext' );
                if ( $match ) {
-                       $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
+                       $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
                }
-               $wgOut->addHTML( '</div>' );
+               $out->addHTML( '</div>' );
 
-               $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
+               $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
                $this->showDiff();
 
-               $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
+               $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
                $this->showTextbox2();
 
-               $wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
+               $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
        }
 
        /**
@@ -4130,9 +4157,9 @@ HTML
         * @return bool
         */
        private function checkUnicodeCompliantBrowser() {
-               global $wgBrowserBlackList, $wgRequest;
+               global $wgBrowserBlackList;
 
-               $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
+               $currentbrowser = $this->context->getRequest()->getHeader( 'User-Agent' );
                if ( $currentbrowser === false ) {
                        // No User-Agent header sent? Trust it by default...
                        return true;
index 7dac0ec..77ac76a 100644 (file)
@@ -803,10 +803,10 @@ class MediaWiki {
         */
        public function triggerJobs() {
                $jobRunRate = $this->config->get( 'JobRunRate' );
-               if ( $jobRunRate <= 0 || wfReadOnly() ) {
-                       return;
-               } elseif ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
+               if ( $this->getTitle()->isSpecial( 'RunJobs' ) ) {
                        return; // recursion guard
+               } elseif ( $jobRunRate <= 0 || wfReadOnly() ) {
+                       return;
                }
 
                if ( $jobRunRate < 1 ) {
@@ -843,7 +843,7 @@ class MediaWiki {
                        $query, $this->config->get( 'SecretKey' ) );
 
                $errno = $errstr = null;
-               $info = wfParseUrl( $this->config->get( 'Server' ) );
+               $info = wfParseUrl( $this->config->get( 'CanonicalServer' ) );
                MediaWiki\suppressWarnings();
                $host = $info['host'];
                $port = 80;
@@ -872,7 +872,8 @@ class MediaWiki {
                        return;
                }
 
-               $url = wfAppendQuery( wfScript( 'index' ), $query );
+               $special = SpecialPageFactory::getPage( 'RunJobs' );
+               $url = $special->getPageTitle()->getCanonicalURL( $query );
                $req = (
                        "POST $url HTTP/1.1\r\n" .
                        "Host: {$info['host']}\r\n" .
@@ -883,7 +884,7 @@ class MediaWiki {
                $runJobsLogger->info( "Running $n job(s) via '$url'" );
                // Send a cron API request to be performed in the background.
                // Give up if this takes too long to send (which should be rare).
-               stream_set_timeout( $sock, 1 );
+               stream_set_timeout( $sock, 2 );
                $bytes = fwrite( $sock, $req );
                if ( $bytes !== strlen( $req ) ) {
                        $runJobsLogger->error( "Failed to start cron API (socket write error)" );
index 8fb3bc2..c7499b1 100644 (file)
@@ -2859,7 +2859,6 @@ class OutputPage extends ContextSource {
 
        private function isUserCssPreview() {
                return $this->getConfig()->get( 'AllowUserCss' )
-                       && $this->getUser()->isLoggedIn()
                        && $this->getTitle()
                        && $this->getTitle()->isCssSubpage()
                        && $this->userCanPreview();
@@ -3097,7 +3096,7 @@ class OutputPage extends ContextSource {
 
                $user = $this->getUser();
 
-               if ( !$this->getUser()->isLoggedIn() ) {
+               if ( !$user->isLoggedIn() ) {
                        // Anons have predictable edit tokens
                        return false;
                }
index dd68102..0039d2b 100644 (file)
@@ -117,6 +117,9 @@ class Pingback {
         *
         * This is public so we can display it in the installer
         *
+        * Developers: If you're adding a new piece of data to this, please ensure
+        * that you update https://www.mediawiki.org/wiki/Manual:$wgPingback
+        *
         * @return array
         */
        public function getSystemInfo() {
index fe254af..881c8c2 100644 (file)
@@ -49,6 +49,8 @@ abstract class Collation {
                switch ( $collationName ) {
                        case 'uppercase':
                                return new UppercaseCollation;
+                       case 'numeric':
+                               return new NumericUppercaseCollation;
                        case 'identity':
                                return new IdentityCollation;
                        case 'uca-default':
diff --git a/includes/collation/NumericUppercaseCollation.php b/includes/collation/NumericUppercaseCollation.php
new file mode 100644 (file)
index 0000000..4bf2f73
--- /dev/null
@@ -0,0 +1,58 @@
+<?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
+ */
+
+/**
+ * Collation that orders text with numbers "naturally", so that 'Foo 1' < 'Foo 2' < 'Foo 12'.
+ *
+ * Note that this only works in terms of sequences of digits, and the behavior for decimal fractions
+ * or pretty-formatted numbers may be unexpected.
+ *
+ * @since 1.28
+ */
+class NumericUppercaseCollation extends UppercaseCollation {
+       public function getSortKey( $string ) {
+               $sortkey = parent::getSortKey( $string );
+
+               // For each sequence of digits, insert the digit '0' and then the length of the sequence
+               // (encoded in two bytes) before it. That's all folks, it sorts correctly now! The '0' ensures
+               // correct position (where digits would normally sort), then the length will be compared putting
+               // shorter numbers before longer ones; if identical, then the characters will be compared, which
+               // generates the correct results for numbers of equal length.
+               $sortkey = preg_replace_callback( '/\d+/', function ( $matches ) {
+                       $len = strlen( $matches[0] );
+                       // This allows sequences of up to 65536 numeric characters to be handled correctly. One byte
+                       // would allow only for 256, which doesn't feel future-proof.
+                       $prefix = chr( floor( $len / 256 ) ) . chr( $len % 256 );
+                       return '0' . $prefix . $matches[0];
+               }, $sortkey );
+
+               return $sortkey;
+       }
+
+       public function getFirstLetter( $string ) {
+               if ( preg_match( '/^\d/', $string ) ) {
+                       // Note that we pass 0 and 9 as normal params, not numParams(). This only works for 0-9
+                       // and not localised digits, so we don't want them to be converted.
+                       return wfMessage( 'category-header-numerals' )->params( 0, 9 )->text();
+               } else {
+                       return parent::getFirstLetter( $string );
+               }
+       }
+}
index 40d9277..14c8182 100644 (file)
@@ -86,7 +86,7 @@ class JsonContent extends TextContent {
                        return $this;
                }
 
-               return new static( $this->beautifyJSON() );
+               return new static( self::normalizeLineEndings( $this->beautifyJSON() ) );
        }
 
        /**
index de650b9..7bb4def 100644 (file)
@@ -147,9 +147,28 @@ class TextContent extends AbstractContent {
                }
        }
 
+       /**
+        * Do a "\r\n" -> "\n" and "\r" -> "\n" transformation
+        * as well as trim trailing whitespace
+        *
+        * This was formerly part of Parser::preSaveTransform, but
+        * for non-wikitext content models they probably still want
+        * to normalize line endings without all of the other PST
+        * changes.
+        *
+        * @since 1.28
+        * @param $text
+        * @return string
+        */
+       public static function normalizeLineEndings( $text ) {
+               return str_replace( [ "\r\n", "\r" ], "\n", rtrim( $text ) );
+       }
+
        /**
         * Returns a Content object with pre-save transformations applied.
-        * This implementation just trims trailing whitespace and normalizes newlines.
+        *
+        * At a minimum, subclasses should make sure to call TextContent::normalizeLineEndings()
+        * either directly or part of Parser::preSaveTransform().
         *
         * @param Title $title
         * @param User $user
@@ -159,8 +178,7 @@ class TextContent extends AbstractContent {
         */
        public function preSaveTransform( Title $title, User $user, ParserOptions $popts ) {
                $text = $this->getNativeData();
-               $pst = rtrim( $text );
-               $pst = str_replace( [ "\r\n", "\r" ], "\n", $pst );
+               $pst = self::normalizeLineEndings( $text );
 
                return ( $text === $pst ) ? $this : new static( $pst, $this->getModel() );
        }
index a63819d..9296728 100644 (file)
@@ -138,7 +138,6 @@ class WikitextContent extends TextContent {
 
                $text = $this->getNativeData();
                $pst = $wgParser->preSaveTransform( $text, $title, $user, $popts );
-               rtrim( $pst );
 
                return ( $text === $pst ) ? $this : new static( $pst );
        }
index 3cd09e2..577c98d 100644 (file)
@@ -129,25 +129,11 @@ class CloneDatabase {
         */
        public static function changePrefix( $prefix ) {
                global $wgDBprefix;
-               wfGetLBFactory()->forEachLB( [ 'CloneDatabase', 'changeLBPrefix' ], [ $prefix ] );
+               wfGetLBFactory()->forEachLB( function( $lb ) use ( $prefix ) {
+                       $lb->forEachOpenConnection( function ( $db ) use ( $prefix ) {
+                               $db->tablePrefix( $prefix );
+                       } );
+               } );
                $wgDBprefix = $prefix;
        }
-
-       /**
-        * @param LoadBalancer $lb
-        * @param string $prefix
-        * @return void
-        */
-       public static function changeLBPrefix( $lb, $prefix ) {
-               $lb->forEachOpenConnection( [ 'CloneDatabase', 'changeDBPrefix' ], [ $prefix ] );
-       }
-
-       /**
-        * @param DatabaseBase $db
-        * @param string $prefix
-        * @return void
-        */
-       public static function changeDBPrefix( $db, $prefix ) {
-               $db->tablePrefix( $prefix );
-       }
 }
index 5b84ca9..281ac24 100644 (file)
@@ -155,18 +155,3 @@ abstract class DataUpdate implements DeferrableUpdate {
                return $remaining;
        }
 }
-
-/**
- * Interface that marks a DataUpdate as enqueuable via the JobQueue
- *
- * Such updates must be representable using IJobSpecification, so that
- * they can be serialized into jobs and enqueued for later execution
- *
- * @since 1.27
- */
-interface EnqueueableDataUpdate {
-       /**
-        * @return array (wiki => wiki ID, job => IJobSpecification)
-        */
-       public function getAsJobSpecification();
-}
diff --git a/includes/deferred/EnqueueableDataUpdate.php b/includes/deferred/EnqueueableDataUpdate.php
new file mode 100644 (file)
index 0000000..ffeb740
--- /dev/null
@@ -0,0 +1,15 @@
+<?php
+/**
+ * Interface that marks a DataUpdate as enqueuable via the JobQueue
+ *
+ * Such updates must be representable using IJobSpecification, so that
+ * they can be serialized into jobs and enqueued for later execution
+ *
+ * @since 1.27
+ */
+interface EnqueueableDataUpdate {
+       /**
+        * @return array (wiki => wiki ID, job => IJobSpecification)
+        */
+       public function getAsJobSpecification();
+}
index 6095aee..4121ecb 100644 (file)
@@ -81,10 +81,12 @@ class RedisLockManager extends QuorumLockManager {
        protected function getLocksOnServer( $lockSrv, array $pathsByType ) {
                $status = Status::newGood();
 
+               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+
                $server = $this->lockServers[$lockSrv];
                $conn = $this->redisPool->getConnection( $server );
                if ( !$conn ) {
-                       foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+                       foreach ( $pathList as $path ) {
                                $status->fatal( 'lockmanager-fail-acquirelock', $path );
                        }
 
@@ -157,7 +159,7 @@ LUA;
                }
 
                if ( $res === false ) {
-                       foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+                       foreach ( $pathList as $path ) {
                                $status->fatal( 'lockmanager-fail-acquirelock', $path );
                        }
                } else {
@@ -172,10 +174,12 @@ LUA;
        protected function freeLocksOnServer( $lockSrv, array $pathsByType ) {
                $status = Status::newGood();
 
+               $pathList = call_user_func_array( 'array_merge', array_values( $pathsByType ) );
+
                $server = $this->lockServers[$lockSrv];
                $conn = $this->redisPool->getConnection( $server );
                if ( !$conn ) {
-                       foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+                       foreach ( $pathList as $path ) {
                                $status->fatal( 'lockmanager-fail-releaselock', $path );
                        }
 
@@ -225,7 +229,7 @@ LUA;
                }
 
                if ( $res === false ) {
-                       foreach ( array_merge( array_values( $pathsByType ) ) as $path ) {
+                       foreach ( $pathList as $path ) {
                                $status->fatal( 'lockmanager-fail-releaselock', $path );
                        }
                } else {
index ff37e24..3c88594 100644 (file)
@@ -354,6 +354,26 @@ class HTMLForm extends ContextSource {
                $this->mFieldTree = $loadedDescriptor;
        }
 
+       /**
+        * @param string $fieldname
+        * @return bool
+        */
+       public function hasField( $fieldname ) {
+               return isset( $this->mFlatFields[$fieldname] );
+       }
+
+       /**
+        * @param string $fieldname
+        * @return HTMLFormField
+        * @throws DomainException on invalid field name
+        */
+       public function getField( $fieldname ) {
+               if ( !$this->hasField( $fieldname ) ) {
+                       throw new DomainException( __METHOD__ . ': no field named ' . $fieldname );
+               }
+               return $this->mFlatFields[$fieldname];
+       }
+
        /**
         * Set format in which to display the form
         *
index dd53f5e..8f42ea2 100644 (file)
@@ -61,7 +61,7 @@ abstract class HTMLFormField {
         * @return bool
         */
        public function canDisplayErrors() {
-               return true;
+               return $this->hasVisibleOutput();
        }
 
        /**
index a231b2f..38d231f 100644 (file)
@@ -4,6 +4,22 @@
  * Multi-select field
  */
 class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable {
+       /**
+        * @param array $params
+        *   In adition to the usual HTMLFormField parameters, this can take the following fields:
+        *   - dropdown: If given, the options will be displayed inside a dropdown with a text field that
+        *     can be used to filter them. This is desirable mostly for very long lists of options.
+        *     This only works for users with JavaScript support and falls back to the list of checkboxes.
+        */
+       public function __construct( $params ) {
+               parent::__construct( $params );
+
+               // For backwards compatibility, also handle the old way with 'cssclass' => 'mw-chosen'
+               if ( isset( $params['dropdown'] ) || strpos( $this->mClass, 'mw-chosen' ) !== false ) {
+                       $this->mClass .= ' mw-htmlform-dropdown';
+               }
+       }
+
        function validate( $value, $alldata ) {
                $p = parent::validate( $value, $alldata );
 
@@ -28,6 +44,10 @@ class HTMLMultiSelectField extends HTMLFormField implements HTMLNestedFilterable
        }
 
        function getInputHTML( $value ) {
+               if ( isset( $this->mParams['dropdown'] ) ) {
+                       $this->mParent->getOutput()->addModules( 'jquery.chosen' );
+               }
+
                $value = HTMLFormField::forceToStringRecursive( $value );
                $html = $this->formatOptions( $this->getOptions(), $value );
 
index b9f4592..3a1da13 100644 (file)
@@ -261,7 +261,6 @@ class JobQueueDB extends JobQueue {
        protected function doPop() {
                $dbw = $this->getMasterDB();
                try {
-                       $dbw->commit( __METHOD__, 'flush' ); // flush existing transaction
                        $autoTrx = $dbw->getFlag( DBO_TRX ); // get current setting
                        $dbw->clearFlag( DBO_TRX ); // make each query its own transaction
                        $scopedReset = new ScopedCallback( function () use ( $dbw, $autoTrx ) {
@@ -457,7 +456,6 @@ class JobQueueDB extends JobQueue {
 
                $dbw = $this->getMasterDB();
                try {
-                       $dbw->commit( __METHOD__, 'flush' ); // flush existing transaction
                        $autoTrx = $dbw->getFlag( DBO_TRX ); // get current setting
                        $dbw->clearFlag( DBO_TRX ); // make each query its own transaction
                        $scopedReset = new ScopedCallback( function () use ( $dbw, $autoTrx ) {
index ad0abf2..5f48dca 100644 (file)
@@ -21,6 +21,7 @@
  * @ingroup JobQueue
  */
 
+use MediaWiki\MediaWikiServices;
 use MediaWiki\Logger\LoggerFactory;
 use Psr\Log\LoggerAwareInterface;
 use Psr\Log\LoggerInterface;
@@ -122,7 +123,8 @@ class JobRunner implements LoggerAwareInterface {
                }
 
                // Flush any pending DB writes for sanity
-               wfGetLBFactory()->commitAll( __METHOD__ );
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+               $lbFactory->commitAll( __METHOD__ );
 
                // Catch huge single updates that lead to slave lag
                $trxProfiler = Profiler::instance()->getTransactionProfiler();
@@ -176,9 +178,11 @@ class JobRunner implements LoggerAwareInterface {
                                        $backoffs = $this->syncBackoffDeltas( $backoffs, $backoffDeltas, $wait );
                                }
 
+                               $lbFactory->commitMasterChanges( __METHOD__ ); // flush any JobQueueDB writes
                                $info = $this->executeJob( $job, $stats, $popTime );
                                if ( $info['status'] !== false || !$job->allowRetries() ) {
                                        $group->ack( $job ); // succeeded or job cannot be retried
+                                       $lbFactory->commitMasterChanges( __METHOD__ ); // flush any JobQueueDB writes
                                }
 
                                // Back off of certain jobs for a while (for throttling and for errors)
@@ -212,7 +216,7 @@ class JobRunner implements LoggerAwareInterface {
                                $timePassed = microtime( true ) - $lastCheckTime;
                                if ( $timePassed >= self::LAG_CHECK_PERIOD || $timePassed < 0 ) {
                                        try {
-                                               wfGetLBFactory()->waitForReplication( [
+                                               $lbFactory->waitForReplication( [
                                                        'ifWritesSince' => $lastCheckTime,
                                                        'timeout' => self::MAX_ALLOWED_LAG
                                                ] );
@@ -257,6 +261,7 @@ class JobRunner implements LoggerAwareInterface {
                $msg = $job->toString() . " STARTING";
                $this->logger->debug( $msg );
                $this->debugCallback( $msg );
+               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
 
                // Run the job...
                $rssStart = $this->getMaxRssKb();
@@ -284,7 +289,7 @@ class JobRunner implements LoggerAwareInterface {
                // Commit all outstanding connections that are in a transaction
                // to get a fresh repeatable read snapshot on every connection.
                // Note that jobs are still responsible for handling slave lag.
-               wfGetLBFactory()->commitAll( __METHOD__ );
+               $lbFactory->commitAll( __METHOD__ );
                // Clear out title cache data from prior snapshots
                LinkCache::singleton()->clear();
                $timeMs = intval( ( microtime( true ) - $jobStartTime ) * 1000 );
index 84936a4..9ab28aa 100644 (file)
@@ -21,6 +21,8 @@
  * @ingroup Cache
  */
 
+use \MediaWiki\MediaWikiServices;
+
 /**
  * Class to store objects in the database
  *
@@ -276,6 +278,7 @@ class SqlBagOStuff extends BagOStuff {
                        if ( isset( $dataRows[$key] ) ) { // HIT?
                                $row = $dataRows[$key];
                                $this->debug( "get: retrieved data; expiry time is " . $row->exptime );
+                               $db = null;
                                try {
                                        $db = $this->getDB( $row->serverIndex );
                                        if ( $this->isExpired( $db, $row->exptime ) ) { // MISS
@@ -284,7 +287,7 @@ class SqlBagOStuff extends BagOStuff {
                                                $values[$key] = $this->unserialize( $db->decodeBlob( $row->value ) );
                                        }
                                } catch ( DBQueryError $e ) {
-                                       $this->handleWriteError( $e, $row->serverIndex );
+                                       $this->handleWriteError( $e, $db, $row->serverIndex );
                                }
                        } else { // MISS
                                $this->debug( 'get: no matching rows' );
@@ -306,10 +309,11 @@ class SqlBagOStuff extends BagOStuff {
                $result = true;
                $exptime = (int)$expiry;
                foreach ( $keysByTable as $serverIndex => $serverKeys ) {
+                       $db = null;
                        try {
                                $db = $this->getDB( $serverIndex );
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $serverIndex );
+                               $this->handleWriteError( $e, $db, $serverIndex );
                                $result = false;
                                continue;
                        }
@@ -342,7 +346,7 @@ class SqlBagOStuff extends BagOStuff {
                                                __METHOD__
                                        );
                                } catch ( DBError $e ) {
-                                       $this->handleWriteError( $e, $serverIndex );
+                                       $this->handleWriteError( $e, $db, $serverIndex );
                                        $result = false;
                                }
 
@@ -364,6 +368,7 @@ class SqlBagOStuff extends BagOStuff {
 
        protected function cas( $casToken, $key, $value, $exptime = 0 ) {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               $db = null;
                try {
                        $db = $this->getDB( $serverIndex );
                        $exptime = intval( $exptime );
@@ -394,7 +399,7 @@ class SqlBagOStuff extends BagOStuff {
                                __METHOD__
                        );
                } catch ( DBQueryError $e ) {
-                       $this->handleWriteError( $e, $serverIndex );
+                       $this->handleWriteError( $e, $db, $serverIndex );
 
                        return false;
                }
@@ -404,6 +409,7 @@ class SqlBagOStuff extends BagOStuff {
 
        public function delete( $key ) {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               $db = null;
                try {
                        $db = $this->getDB( $serverIndex );
                        $db->delete(
@@ -411,7 +417,7 @@ class SqlBagOStuff extends BagOStuff {
                                [ 'keyname' => $key ],
                                __METHOD__ );
                } catch ( DBError $e ) {
-                       $this->handleWriteError( $e, $serverIndex );
+                       $this->handleWriteError( $e, $db, $serverIndex );
                        return false;
                }
 
@@ -420,6 +426,7 @@ class SqlBagOStuff extends BagOStuff {
 
        public function incr( $key, $step = 1 ) {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               $db = null;
                try {
                        $db = $this->getDB( $serverIndex );
                        $step = intval( $step );
@@ -455,7 +462,7 @@ class SqlBagOStuff extends BagOStuff {
                                $newValue = null;
                        }
                } catch ( DBError $e ) {
-                       $this->handleWriteError( $e, $serverIndex );
+                       $this->handleWriteError( $e, $db, $serverIndex );
                        return null;
                }
 
@@ -473,6 +480,7 @@ class SqlBagOStuff extends BagOStuff {
 
        public function changeTTL( $key, $expiry = 0 ) {
                list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               $db = null;
                try {
                        $db = $this->getDB( $serverIndex );
                        $db->update(
@@ -485,7 +493,7 @@ class SqlBagOStuff extends BagOStuff {
                                return false;
                        }
                } catch ( DBError $e ) {
-                       $this->handleWriteError( $e, $serverIndex );
+                       $this->handleWriteError( $e, $db, $serverIndex );
                        return false;
                }
 
@@ -542,6 +550,7 @@ class SqlBagOStuff extends BagOStuff {
         */
        public function deleteObjectsExpiringBefore( $timestamp, $progressCallback = false ) {
                for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+                       $db = null;
                        try {
                                $db = $this->getDB( $serverIndex );
                                $dbTimestamp = $db->timestamp( $timestamp );
@@ -604,7 +613,7 @@ class SqlBagOStuff extends BagOStuff {
                                        }
                                }
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $serverIndex );
+                               $this->handleWriteError( $e, $db, $serverIndex );
                                return false;
                        }
                }
@@ -618,13 +627,14 @@ class SqlBagOStuff extends BagOStuff {
         */
        public function deleteAll() {
                for ( $serverIndex = 0; $serverIndex < $this->numServers; $serverIndex++ ) {
+                       $db = null;
                        try {
                                $db = $this->getDB( $serverIndex );
                                for ( $i = 0; $i < $this->shards; $i++ ) {
                                        $db->delete( $this->getTableNameByShard( $i ), '*', __METHOD__ );
                                }
                        } catch ( DBError $e ) {
-                               $this->handleWriteError( $e, $serverIndex );
+                               $this->handleWriteError( $e, $db, $serverIndex );
                                return false;
                        }
                }
@@ -694,26 +704,19 @@ class SqlBagOStuff extends BagOStuff {
         * Handle a DBQueryError which occurred during a write operation.
         *
         * @param DBError $exception
+        * @param IDatabase|null $db DB handle or null if connection failed
         * @param int $serverIndex
         * @throws Exception
         */
-       protected function handleWriteError( DBError $exception, $serverIndex ) {
-               if ( $exception instanceof DBConnectionError ) {
+       protected function handleWriteError( DBError $exception, IDatabase $db = null, $serverIndex ) {
+               if ( !$db ) {
                        $this->markServerDown( $exception, $serverIndex );
-               }
-               if ( $exception->db && $exception->db->wasReadOnlyError() ) {
-                       if ( $exception->db->trxLevel() ) {
-                               if ( !$this->serverInfos ) {
-                                       // Errors like deadlocks and connection drops already cause rollback.
-                                       // For consistency, we have no choice but to throw an error and trigger
-                                       // complete rollback if the main DB is also being used as the cache DB.
-                                       throw $exception;
-                               }
-
-                               try {
-                                       $exception->db->rollback( __METHOD__ );
-                               } catch ( DBError $e ) {
-                               }
+               } elseif ( $db->wasReadOnlyError() ) {
+                       if ( $db->trxLevel() && $this->usesMainDB() ) {
+                               // Errors like deadlocks and connection drops already cause rollback.
+                               // For consistency, we have no choice but to throw an error and trigger
+                               // complete rollback if the main DB is also being used as the cache DB.
+                               throw $exception;
                        }
                }
 
@@ -733,7 +736,7 @@ class SqlBagOStuff extends BagOStuff {
         * @param DBError $exception
         * @param int $serverIndex
         */
-       protected function markServerDown( $exception, $serverIndex ) {
+       protected function markServerDown( DBError $exception, $serverIndex ) {
                unset( $this->conns[$serverIndex] ); // bug T103435
 
                if ( isset( $this->connFailureTimes[$serverIndex] ) ) {
@@ -770,11 +773,19 @@ class SqlBagOStuff extends BagOStuff {
                }
        }
 
+       /**
+        * @return bool Whether the main DB is used, e.g. wfGetDB( DB_MASTER )
+        */
+       protected function usesMainDB() {
+               return !$this->serverInfos;
+       }
+
        protected function waitForSlaves() {
-               if ( !$this->serverInfos ) {
+               if ( $this->usesMainDB() ) {
                        // Main LB is used; wait for any slaves to catch up
                        try {
-                               wfGetLBFactory()->waitForReplication( [ 'wiki' => wfWikiID() ] );
+                               $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+                               $lbFactory->waitForReplication( [ 'wiki' => wfWikiID() ] );
                                return true;
                        } catch ( DBReplicationWaitError $e ) {
                                return false;
index 033eb67..3851d15 100644 (file)
@@ -2873,6 +2873,10 @@ class WikiPage implements Page, IDBAccessObject {
                        return $status;
                }
 
+               // Given the lock above, we can be confident in the title and page ID values
+               $namespace = $this->getTitle()->getNamespace();
+               $dbKey = $this->getTitle()->getDBkey();
+
                // At this point we are now comitted to returning an OK
                // status unless some DB query error or other exception comes up.
                // This way callers don't have to call rollback() if $status is bad
@@ -2893,54 +2897,50 @@ class WikiPage implements Page, IDBAccessObject {
                        $bitfield = 'rev_deleted';
                }
 
-               /**
-                * For now, shunt the revision data into the archive table.
-                * Text is *not* removed from the text table; bulk storage
-                * is left intact to avoid breaking block-compression or
-                * immutable storage schemes.
-                *
-                * For backwards compatibility, note that some older archive
-                * table entries will have ar_text and ar_flags fields still.
-                *
-                * In the future, we may keep revisions and mark them with
-                * the rev_deleted field, which is reserved for this purpose.
-                */
-
-               $row = [
-                       'ar_namespace'  => 'page_namespace',
-                       'ar_title'      => 'page_title',
-                       'ar_comment'    => 'rev_comment',
-                       'ar_user'       => 'rev_user',
-                       'ar_user_text'  => 'rev_user_text',
-                       'ar_timestamp'  => 'rev_timestamp',
-                       'ar_minor_edit' => 'rev_minor_edit',
-                       'ar_rev_id'     => 'rev_id',
-                       'ar_parent_id'  => 'rev_parent_id',
-                       'ar_text_id'    => 'rev_text_id',
-                       'ar_text'       => '\'\'', // Be explicit to appease
-                       'ar_flags'      => '\'\'', // MySQL's "strict mode"...
-                       'ar_len'        => 'rev_len',
-                       'ar_page_id'    => 'page_id',
-                       'ar_deleted'    => $bitfield,
-                       'ar_sha1'       => 'rev_sha1',
-               ];
+               // For now, shunt the revision data into the archive table.
+               // Text is *not* removed from the text table; bulk storage
+               // is left intact to avoid breaking block-compression or
+               // immutable storage schemes.
+               // In the future, we may keep revisions and mark them with
+               // the rev_deleted field, which is reserved for this purpose.
 
-               if ( $wgContentHandlerUseDB ) {
-                       $row['ar_content_model'] = 'rev_content_model';
-                       $row['ar_content_format'] = 'rev_content_format';
-               }
-
-               // Copy all the page revisions into the archive table
-               $dbw->insertSelect(
-                       'archive',
-                       [ 'page', 'revision' ],
-                       $row,
-                       [
-                               'page_id' => $id,
-                               'page_id = rev_page'
-                       ],
-                       __METHOD__
+               // Get all of the page revisions
+               $res = $dbw->select(
+                       'revision',
+                       Revision::selectFields(),
+                       [ 'rev_page' => $id ],
+                       __METHOD__,
+                       'FOR UPDATE'
                );
+               // Build their equivalent archive rows
+               $rowsInsert = [];
+               foreach ( $res as $row ) {
+                       $rowInsert = [
+                               'ar_namespace'  => $namespace,
+                               'ar_title'      => $dbKey,
+                               'ar_comment'    => $row->rev_comment,
+                               'ar_user'       => $row->rev_user,
+                               'ar_user_text'  => $row->rev_user_text,
+                               'ar_timestamp'  => $row->rev_timestamp,
+                               'ar_minor_edit' => $row->rev_minor_edit,
+                               'ar_rev_id'     => $row->rev_id,
+                               'ar_parent_id'  => $row->rev_parent_id,
+                               'ar_text_id'    => $row->rev_text_id,
+                               'ar_text'       => '',
+                               'ar_flags'      => '',
+                               'ar_len'        => $row->rev_len,
+                               'ar_page_id'    => $id,
+                               'ar_deleted'    => $bitfield,
+                               'ar_sha1'       => $row->rev_sha1,
+                       ];
+                       if ( $wgContentHandlerUseDB ) {
+                               $rowInsert['ar_content_model'] = $row->rev_content_model;
+                               $rowInsert['ar_content_format'] = $row->rev_content_format;
+                       }
+                       $rowsInsert[] = $rowInsert;
+               }
+               // Copy them into the archive table
+               $dbw->insert( 'archive', $rowsInsert, __METHOD__ );
                // Save this so we can pass it to the ArticleDeleteComplete hook.
                $archivedRevisionCount = $dbw->affectedRows();
 
index 38eb621..b116bd4 100644 (file)
@@ -4364,7 +4364,11 @@ class Parser {
                $this->startParse( $title, $options, self::OT_WIKI, $clearState );
                $this->setUser( $user );
 
-               $text = str_replace( [ "\r\n", "\r" ], "\n", $text );
+               // We still normalize line endings for backwards-compatibility
+               // with other code that just calls PST, but this should already
+               // be handled in TextContent subclasses
+               $text = TextContent::normalizeLineEndings( $text );
+
                if ( $options->getPreSaveTransform() ) {
                        $text = $this->pstPass2( $text, $user );
                }
@@ -4442,9 +4446,6 @@ class Parser {
                        $text = preg_replace( $p2, '[[\\1]]', $text );
                }
 
-               # Trim trailing whitespace
-               $text = rtrim( $text );
-
                return $text;
        }
 
index cf1a4d0..22c38cb 100644 (file)
@@ -616,36 +616,6 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                        $form->setId( 'userlogin2' );
                }
 
-               // add pre/post text
-               // header used by ConfirmEdit, CondfirmAccount, Persona, WikimediaIncubator, SemanticSignup
-               // should be above the error message but HTMLForm doesn't support that
-               $form->addHeaderText( $fakeTemplate->get( 'header' ) );
-
-               // FIXME the old form used this for error/warning messages which does not play well with
-               // HTMLForm (maybe it could with a subclass?); for now only display it for signups
-               // (where the JS username validation needs it) and alway empty
-               if ( $this->isSignup() ) {
-                       // used by the mediawiki.special.userlogin.signup.js module
-                       $statusAreaAttribs = [ 'id' => 'mw-createacct-status-area' ];
-                       // $statusAreaAttribs += $msg ? [ 'class' => "{$msgType}box" ] : [ 'style' => 'display: none;' ];
-                       $form->addHeaderText( Html::element( 'div', $statusAreaAttribs ) );
-               }
-
-               // header used by MobileFrontend
-               $form->addHeaderText( $fakeTemplate->get( 'formheader' ) );
-
-               // blank signup footer for site customization
-               if ( $this->isSignup() && $this->showExtraInformation() ) {
-                       // Use signupend-https for HTTPS requests if it's not blank, signupend otherwise
-                       $signupendMsg = $this->msg( 'signupend' );
-                       $signupendHttpsMsg = $this->msg( 'signupend-https' );
-                       if ( !$signupendMsg->isDisabled() ) {
-                               $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
-                                       ? $signupendHttpsMsg ->parse() : $signupendMsg->parse();
-                               $form->addPostText( Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ) );
-                       }
-               }
-
                // warning header for non-standard workflows (e.g. security reauthentication)
                if ( !$this->isSignup() && $this->getUser()->isLoggedIn() ) {
                        $reauthMessage = $this->securityLevel ? 'userlogin-reauth' : 'userlogin-loggedin';
@@ -653,52 +623,6 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                                $this->msg( $reauthMessage )->params( $this->getUser()->getName() )->parse() ) );
                }
 
-               if ( !$this->isSignup() && $this->showExtraInformation() ) {
-                       $passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
-                       if ( $passwordReset->isAllowed( $this->getUser() ) ) {
-                               $form->addFooterText( Html::rawElement(
-                                       'div',
-                                       [ 'class' => 'mw-ui-vform-field mw-form-related-link-container' ],
-                                       Linker::link(
-                                               SpecialPage::getTitleFor( 'PasswordReset' ),
-                                               $this->msg( 'userlogin-resetpassword-link' )->escaped()
-                                       )
-                               ) );
-                       }
-
-                       // Don't show a "create account" link if the user can't.
-                       if ( $this->showCreateAccountLink() ) {
-                               // link to the other action
-                               $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' :'CreateAccount' );
-                               $linkq = $this->getReturnToQueryStringFragment();
-                               // Pass any language selection on to the mode switch link
-                               if ( $wgLoginLanguageSelector && $this->mLanguage ) {
-                                       $linkq .= '&uselang=' . $this->mLanguage;
-                               }
-
-                               $loggedIn = $this->getUser()->isLoggedIn();
-                               $createOrLoginHtml = Html::rawElement( 'div',
-                                       [ 'id' => 'mw-createaccount' . ( !$loggedIn ? '-cta' : '' ),
-                                               'class' => ( $loggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
-                                       ( $loggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
-                                       . Html::element( 'a',
-                                               [
-                                                       'id' => 'mw-createaccount-join' . ( $loggedIn ? '-loggedin' : '' ),
-                                                       'href' => $linkTitle->getLocalURL( $linkq ),
-                                                       'class' => ( $loggedIn ? '' : 'mw-ui-button' ),
-                                                       'tabindex' => 100,
-                                               ],
-                                               $this->msg(
-                                                       ( $this->getUser()->isLoggedIn() ?
-                                                               'userlogin-createanother' :
-                                                               'userlogin-joinproject'
-                                                       ) )->escaped()
-                                       )
-                               );
-                               $form->addFooterText( $createOrLoginHtml );
-                       }
-               }
-
                $form->suppressDefaultSubmit();
 
                $this->authForm = $form;
@@ -837,7 +761,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                array $requests, array $fieldInfo, array &$formDescriptor, $action
        ) {
                $coreFieldDescriptors = $this->getFieldDefinitions( $this->fakeTemplate );
-               $specialFields = array_merge( [ 'extraInput', 'linkcontainer', 'entryError' ],
+               $specialFields = array_merge( [ 'extraInput' ],
                        array_keys( $this->fakeTemplate->getExtraInputDefinitions() ) );
 
                // keep the ordering from getCoreFieldDescriptors() where there is no explicit weight
@@ -846,14 +770,19 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                                $formDescriptor[$fieldName] : [];
 
                        // remove everything that is not in the fieldinfo, is not marked as a supplemental field
-                       // to something in the fieldinfo, and is not a generic or B/C field or a submit button
+                       // to something in the fieldinfo, is not B/C for the pre-AuthManager templates,
+                       // and is not an info field or a submit button
                        if (
                                !isset( $fieldInfo[$fieldName] )
                                && (
                                        !isset( $coreField['baseField'] )
                                        || !isset( $fieldInfo[$coreField['baseField']] )
-                               ) && !in_array( $fieldName, $specialFields, true )
-                               && ( !isset( $coreField['type'] ) || $coreField['type'] !== 'submit' )
+                               )
+                               && !in_array( $fieldName, $specialFields, true )
+                               && (
+                                       !isset( $coreField['type'] )
+                                       || !in_array( $coreField['type'], [ 'submit', 'info' ], true )
+                               )
                        ) {
                                $coreFieldDescriptors[$fieldName] = null;
                                continue;
@@ -892,13 +821,12 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
         * @return array
         */
        protected function getFieldDefinitions( $template ) {
-               global $wgEmailConfirmToEdit;
+               global $wgEmailConfirmToEdit, $wgLoginLanguageSelector;
 
                $isLoggedIn = $this->getUser()->isLoggedIn();
                $continuePart = $this->isContinued() ? 'continue-' : '';
                $anotherPart = $isLoggedIn ? 'another-' : '';
-               $expiration = $this->getRequest()->getSession()->getProvider()
-                       ->getRememberUserDuration();
+               $expiration = $this->getRequest()->getSession()->getProvider()->getRememberUserDuration();
                $expirationDays = ceil( $expiration / ( 3600 * 24 ) );
                $secureLoginLink = '';
                if ( $this->mSecureLoginUrl ) {
@@ -910,6 +838,14 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
 
                if ( $this->isSignup() ) {
                        $fieldDefinitions = [
+                               'statusarea' => [
+                                       // used by the mediawiki.special.userlogin.signup.js module for error display
+                                       // FIXME merge this with HTMLForm's normal status (error) area
+                                       'type' => 'info',
+                                       'raw' => true,
+                                       'default' => Html::element( 'div', [ 'id' => 'mw-createacct-status-area' ] ),
+                                       'weight' => -105,
+                               ],
                                'username' => [
                                        'label-message' => 'userlogin-yourname',
                                        // FIXME help-message does not match old formatting
@@ -1056,6 +992,7 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                                ],
                        ];
                }
+
                $fieldDefinitions['username'] += [
                        'type' => 'text',
                        'name' => 'wpName',
@@ -1072,6 +1009,19 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                        // 'required' => true,
                ];
 
+               if ( $template->get( 'header' ) || $template->get( 'formheader' ) ) {
+                       // B/C for old extensions that haven't been converted to AuthManager (or have been
+                       // but somebody is using the old version) and still use templates via the
+                       // UserCreateForm/UserLoginForm hook.
+                       // 'header' used by ConfirmEdit, CondfirmAccount, Persona, WikimediaIncubator, SemanticSignup
+                       // 'formheader' used by MobileFrontend
+                       $fieldDefinitions['header'] = [
+                               'type' => 'info',
+                               'raw' => true,
+                               'default' => $template->get( 'header' ) ?: $template->get( 'formheader' ),
+                               'weight' => - 110,
+                       ];
+               }
                if ( $this->mEntryError ) {
                        $fieldDefinitions['entryError'] = [
                                'type' => 'info',
@@ -1082,9 +1032,77 @@ abstract class LoginSignupSpecialPage extends AuthManagerSpecialPage {
                                'weight' => -100,
                        ];
                }
-
                if ( !$this->showExtraInformation() ) {
-                       unset( $fieldDefinitions['linkcontainer'] );
+                       unset( $fieldDefinitions['linkcontainer'], $fieldDefinitions['signupend'] );
+               }
+               if ( $this->isSignup() && $this->showExtraInformation() ) {
+                       // blank signup footer for site customization
+                       // uses signupend-https for HTTPS requests if it's not blank, signupend otherwise
+                       $signupendMsg = $this->msg( 'signupend' );
+                       $signupendHttpsMsg = $this->msg( 'signupend-https' );
+                       if ( !$signupendMsg->isDisabled() ) {
+                               $usingHTTPS = $this->getRequest()->getProtocol() === 'https';
+                               $signupendText = ( $usingHTTPS && !$signupendHttpsMsg->isBlank() )
+                                       ? $signupendHttpsMsg ->parse() : $signupendMsg->parse();
+                               $fieldDefinitions['signupend'] = [
+                                       'type' => 'info',
+                                       'raw' => true,
+                                       'default' => Html::rawElement( 'div', [ 'id' => 'signupend' ], $signupendText ),
+                                       'weight' => 225,
+                               ];
+                       }
+               }
+               if ( !$this->isSignup() && $this->showExtraInformation() ) {
+                       $passwordReset = new PasswordReset( $this->getConfig(), AuthManager::singleton() );
+                       if ( $passwordReset->isAllowed( $this->getUser() ) ) {
+                               $fieldDefinitions['passwordReset'] = [
+                                       'type' => 'info',
+                                       'raw' => true,
+                                       'cssclass' => 'mw-form-related-link-container',
+                                       'default' => Linker::link(
+                                               SpecialPage::getTitleFor( 'PasswordReset' ),
+                                               $this->msg( 'userlogin-resetpassword-link' )->escaped()
+                                       ),
+                                       'weight' => 230,
+                               ];
+                       }
+
+                       // Don't show a "create account" link if the user can't.
+                       if ( $this->showCreateAccountLink() ) {
+                               // link to the other action
+                               $linkTitle = $this->getTitleFor( $this->isSignup() ? 'Userlogin' :'CreateAccount' );
+                               $linkq = $this->getReturnToQueryStringFragment();
+                               // Pass any language selection on to the mode switch link
+                               if ( $wgLoginLanguageSelector && $this->mLanguage ) {
+                                       $linkq .= '&uselang=' . $this->mLanguage;
+                               }
+                               $loggedIn = $this->getUser()->isLoggedIn();
+
+                               $fieldDefinitions['createOrLogin'] = [
+                                       'type' => 'info',
+                                       'raw' => true,
+                                       'linkQuery' => $linkq,
+                                       'default' => function ( $params ) use ( $loggedIn, $linkTitle ) {
+                                               return Html::rawElement( 'div',
+                                                       [ 'id' => 'mw-createaccount' . ( !$loggedIn ? '-cta' : '' ),
+                                                               'class' => ( $loggedIn ? 'mw-form-related-link-container' : 'mw-ui-vform-field' ) ],
+                                                       ( $loggedIn ? '' : $this->msg( 'userlogin-noaccount' )->escaped() )
+                                                       . Html::element( 'a',
+                                                               [
+                                                                       'id' => 'mw-createaccount-join' . ( $loggedIn ? '-loggedin' : '' ),
+                                                                       'href' => $linkTitle->getLocalURL( $params['linkQuery'] ),
+                                                                       'class' => ( $loggedIn ? '' : 'mw-ui-button' ),
+                                                                       'tabindex' => 100,
+                                                               ],
+                                                               $this->msg(
+                                                                       $loggedIn ? 'userlogin-createanother' : 'userlogin-joinproject'
+                                                               )->escaped()
+                                                       )
+                                               );
+                                       },
+                                       'weight' => 235,
+                               ];
+                       }
                }
 
                $fieldDefinitions = $this->getBCFieldDefinitions( $fieldDefinitions, $template );
index d11fbe6..9cb6d4b 100644 (file)
@@ -91,7 +91,7 @@ class SpecialMyLanguage extends RedirectSpecialArticle {
 
                $uiCode = $this->getLanguage()->getCode();
                $proposed = $base->getSubpage( $uiCode );
-               if ( $uiCode !== $this->getConfig()->get( 'LanguageCode' ) && $proposed && $proposed->exists() ) {
+               if ( $proposed && $proposed->exists() && $uiCode !== $base->getPageLanguage()->getCode() ) {
                        return $proposed;
                } elseif ( $provided && $provided->exists() ) {
                        return $provided;
index ce5533f..e1e2049 100644 (file)
@@ -40,7 +40,6 @@ class SpecialRunJobs extends UnlistedSpecialPage {
 
        public function execute( $par = '' ) {
                $this->getOutput()->disable();
-
                if ( wfReadOnly() ) {
                        // HTTP 423 Locked
                        HttpStatus::header( 423 );
index a6ab439..a9ccc4e 100644 (file)
@@ -3134,6 +3134,7 @@ class User implements IDBAccessObject {
        public function getRights() {
                if ( is_null( $this->mRights ) ) {
                        $this->mRights = self::getGroupPermissions( $this->getEffectiveGroups() );
+                       Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
 
                        // Deny any rights denied by the user's session, unless this
                        // endpoint has no sessions.
@@ -3144,7 +3145,6 @@ class User implements IDBAccessObject {
                                }
                        }
 
-                       Hooks::run( 'UserGetRights', [ $this, &$this->mRights ] );
                        // Force reindexation of rights when a hook has unset one of them
                        $this->mRights = array_values( array_unique( $this->mRights ) );
 
index 8b39d77..cb3b4b8 100644 (file)
@@ -1036,6 +1036,7 @@ class Language {
         *    xin  n (month number) in Iranian calendar
         *    xiy  y (two digit year) in Iranian calendar
         *    xiY  Y (full year) in Iranian calendar
+        *    xit  t (days in month) in Iranian calendar
         *
         *    xjj  j (day number) in Hebrew calendar
         *    xjF  F (month name) in Hebrew calendar
@@ -1331,6 +1332,13 @@ class Language {
                                        }
                                        $num = substr( $iranian[0], -2 );
                                        break;
+                               case 'xit':
+                                       $usedIranianYear = true;
+                                       if ( !$iranian ) {
+                                               $iranian = self::tsToIranian( $ts );
+                                       }
+                                       $num = self::$IRANIAN_DAYS[$iranian[1] - 1];
+                                       break;
                                case 'a':
                                        $usedAMPM = true;
                                        $s .= intval( substr( $ts, 8, 2 ) ) < 12 ? 'am' : 'pm';
index e1c37c8..892ac2c 100644 (file)
        "linkaccounts-submit": "Link accounts",
        "unlinkaccounts": "Unlink accounts",
        "unlinkaccounts-success": "The account was unlinked.",
-       "authenticationdatachange-ignored": "The authentication data change was not handled. Maybe no provider was configured?"
+       "authenticationdatachange-ignored": "The authentication data change was not handled. Maybe no provider was configured?",
+       "userjsispublic": "Please note: JavaScript subpages should not contain confidential data as they are viewable by other users.",
+       "usercssispublic": "Please note: CSS subpages should not contain confidential data as they are viewable by other users."
 }
index 6a3d604..2c5041c 100644 (file)
        "linkaccounts-submit": "Text of the main submit button on [[Special:LinkAccounts]] (when there is one)",
        "unlinkaccounts": "Title of the special page [[Special:UnlinkAccounts]] which allows the user to remove linked remote accounts.",
        "unlinkaccounts-success": "Account unlinking form success message",
-       "authenticationdatachange-ignored": "Shown when authentication data change was unsuccessful due to configuration problems.\n\nCf. e.g. {{msg-mw|Passwordreset-ignored}}."
+       "authenticationdatachange-ignored": "Shown when authentication data change was unsuccessful due to configuration problems.\n\nCf. e.g. {{msg-mw|Passwordreset-ignored}}.",
+       "userjsispublic": "A reminder to users that Javascript subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .js. See also {{msg-mw|usercssispublic}}.",
+       "usercssispublic": "A reminder to users that CSS subpages are not preferences but normal pages, and thus can be viewed by other users and the general public. This message is shown to a user whenever they are editing a subpage in their own user-space that ends in .css. See also {{msg-mw|userjsispublic}}"
 }
index 1fa701e..2d04f63 100644 (file)
@@ -250,8 +250,8 @@ $namespaceNames = [
        NS_MEDIA            => 'Médiá',
        NS_SPECIAL          => 'Å peciálne',
        NS_TALK             => 'Diskusia',
-       NS_USER             => 'Redaktor',
-       NS_USER_TALK        => 'Diskusia_s_redaktorom',
+       NS_USER             => 'Užívateľ',
+       NS_USER_TALK        => 'Diskusia_s_užívateľom',
        NS_PROJECT_TALK     => 'Diskusia_k_{{GRAMMAR:datív|$1}}',
        NS_FILE             => 'Súbor',
        NS_FILE_TALK        => 'Diskusia_k_súboru',
@@ -267,6 +267,8 @@ $namespaceNames = [
 
 $namespaceAliases = [
        "Komentár"               => NS_TALK,
+       'Redaktor'               => NS_USER,
+       'Diskusia_s_redaktorom'  => NS_USER_TALK,
        "Komentár_k_redaktorovi" => NS_USER_TALK,
        "Komentár_k_Wikipédii"   => NS_PROJECT_TALK,
        'Obrázok' => NS_FILE,
@@ -275,6 +277,11 @@ $namespaceAliases = [
        "Komentár_k_MediaWiki"   => NS_MEDIAWIKI_TALK,
 ];
 
+$namespaceGenderAliases = [
+       NS_USER => [ 'male' => 'Užívateľ', 'female' => 'Užívateľka' ],
+       NS_USER_TALK => [ 'male' => 'Diskusia_s_užívateľom', 'female' => 'Diskusia_s_užívateľkou' ],
+];
+
 $separatorTransformTable = [
        ',' => "\xc2\xa0",
        '.' => ','
index 9cf7b2b..bd34a50 100644 (file)
@@ -8,7 +8,7 @@ class ValidateRegistrationFile extends Maintenance {
                $this->addArg( 'path', 'Path to extension.json/skin.json file.', true );
        }
        public function execute() {
-               if ( !class_exists( 'JsonSchema\Uri\UriRetriever' ) ) {
+               if ( !class_exists( 'JsonSchema\Validato' ) ) {
                        $this->error( 'The JsonSchema library cannot be found, please install it through composer.', 1 );
                }
 
@@ -38,11 +38,8 @@ class ValidateRegistrationFile extends Maintenance {
                        $this->output( "Warning: $path is using a deprecated schema, and should be updated to "
                                . ExtensionRegistry::MANIFEST_VERSION . "\n" );
                }
-               $retriever = new JsonSchema\Uri\UriRetriever();
-               $schema = $retriever->retrieve( 'file://' . $schemaPath );
-
-               $validator = new JsonSchema\Validator();
-               $validator->check( $data, $schema );
+               $validator = new JsonSchema\Validator;
+               $validator->check( $data, (object) [ '$ref' => 'file://' . $schemaPath ] );
                if ( $validator->isValid() ) {
                        $this->output( "$path validates against the version $version schema!\n" );
                } else {
index 0bdf02e..946823d 100644 (file)
                        $.each( response.query.pages, function ( index, page ) {
                                var title = new ForeignTitle( page.title ).getPrefixedText();
                                cache.existenceCache[ title ] = !page.missing;
+                               if ( !queue[ title ] ) {
+                                       // Debugging for T139130
+                                       throw new Error( 'No queue for "' + title + '", requested "' + titles.join( '|' ) + '"' );
+                               }
                                queue[ title ].resolve( cache.existenceCache[ title ] );
                        } );
                } );
index 3cdab3c..a8786ef 100644 (file)
@@ -6,7 +6,7 @@
 
        function addMulti( $oldContainer, $container ) {
                var name = $oldContainer.find( 'input:first-child' ).attr( 'name' ),
-                       oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen)/g, '' ),
+                       oldClass = ( ' ' + $oldContainer.attr( 'class' ) + ' ' ).replace( /(mw-htmlform-field-HTMLMultiSelectField|mw-chosen|mw-htmlform-dropdown)/g, '' ),
                        $select = $( '<select>' ),
                        dataPlaceholder = mw.message( 'htmlform-chosen-placeholder' );
                oldClass = $.trim( oldClass );
@@ -53,9 +53,9 @@
        }
 
        mw.hook( 'htmlform.enhance' ).add( function ( $root ) {
-               if ( $root.find( '.mw-chosen' ).length ) {
+               if ( $root.find( '.mw-htmlform-dropdown' ).length ) {
                        mw.loader.using( 'jquery.chosen', function () {
-                               $root.find( '.mw-chosen' ).each( function () {
+                               $root.find( '.mw-htmlform-dropdown' ).each( function () {
                                        var type = this.nodeName.toLowerCase(),
                                                $converted = convertCheckboxesToMulti( $( this ), type );
                                        $converted.find( '.htmlform-chzn-select' ).chosen( { width: 'auto' } );
index b4ae765..b290f8f 100644 (file)
@@ -459,4 +459,30 @@ class TextContentTest extends MediaWikiLangTestCase {
                        $this->assertEquals( $expectedNative, $converted->getNativeData() );
                }
        }
+
+       /**
+        * @covers TextContent::normalizeLineEndings
+        * @dataProvider provideNormalizeLineEndings
+        */
+       public function testNormalizeLineEndings( $input, $expected ) {
+               $this->assertEquals( $expected, TextContent::normalizeLineEndings( $input ) );
+       }
+
+       public static function provideNormalizeLineEndings() {
+               return [
+                       [
+                               "Foo\r\nbar",
+                               "Foo\nbar"
+                       ],
+                       [
+                               "Foo\rbar",
+                               "Foo\nbar"
+                       ],
+                       [
+                               "Foobar\n  ",
+                               "Foobar"
+                       ]
+               ];
+       }
+
 }
index 7fe8055..a01cc6b 100644 (file)
@@ -5,6 +5,10 @@
  */
 class CachedBagOStuffTest extends PHPUnit_Framework_TestCase {
 
+       /**
+        * @covers CachedBagOStuff::__construct
+        * @covers CachedBagOStuff::doGet
+        */
        public function testGetFromBackend() {
                $backend = new HashBagOStuff;
                $cache = new CachedBagOStuff( $backend );
@@ -16,6 +20,10 @@ class CachedBagOStuffTest extends PHPUnit_Framework_TestCase {
                $this->assertEquals( 'bar', $cache->get( 'foo' ), 'cached' );
        }
 
+       /**
+        * @covers CachedBagOStuff::set
+        * @covers CachedBagOStuff::delete
+        */
        public function testSetAndDelete() {
                $backend = new HashBagOStuff;
                $cache = new CachedBagOStuff( $backend );
@@ -30,6 +38,10 @@ class CachedBagOStuffTest extends PHPUnit_Framework_TestCase {
                }
        }
 
+       /**
+        * @covers CachedBagOStuff::set
+        * @covers CachedBagOStuff::delete
+        */
        public function testWriteCacheOnly() {
                $backend = new HashBagOStuff;
                $cache = new CachedBagOStuff( $backend );
@@ -50,6 +62,9 @@ class CachedBagOStuffTest extends PHPUnit_Framework_TestCase {
                $this->assertEquals( 'old', $cache->get( 'foo' ) ); // Reloaded from backend
        }
 
+       /**
+        * @covers CachedBagOStuff::doGet
+        */
        public function testCacheBackendMisses() {
                $backend = new HashBagOStuff;
                $cache = new CachedBagOStuff( $backend );
index fce09ae..c4db0cf 100644 (file)
@@ -5,6 +5,9 @@
  */
 class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
 
+       /**
+        * @covers HashBagOStuff::delete
+        */
        public function testDelete() {
                $cache = new HashBagOStuff();
                for ( $i = 0; $i < 10; $i++ ) {
@@ -15,6 +18,9 @@ class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
                }
        }
 
+       /**
+        * @covers HashBagOStuff::clear
+        */
        public function testClear() {
                $cache = new HashBagOStuff();
                for ( $i = 0; $i < 10; $i++ ) {
@@ -27,6 +33,10 @@ class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
                }
        }
 
+       /**
+        * @covers HashBagOStuff::doGet
+        * @covers HashBagOStuff::expire
+        */
        public function testExpire() {
                $cache = new HashBagOStuff();
                $cacheInternal = TestingAccessWrapper::newFromObject( $cache );
@@ -45,6 +55,9 @@ class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
 
        /**
         * Ensure maxKeys eviction prefers keeping new keys.
+        *
+        * @covers HashBagOStuff::__construct
+        * @covers HashBagOStuff::set
         */
        public function testEvictionAdd() {
                $cache = new HashBagOStuff( [ 'maxKeys' => 10 ] );
@@ -62,6 +75,9 @@ class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
        /**
         * Ensure maxKeys eviction prefers recently set keys
         * even if the keys pre-exist.
+        *
+        * @covers HashBagOStuff::__construct
+        * @covers HashBagOStuff::set
         */
        public function testEvictionSet() {
                $cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
@@ -85,6 +101,10 @@ class HashBagOStuffTest extends PHPUnit_Framework_TestCase {
 
        /**
         * Ensure maxKeys eviction prefers recently retrieved keys (LRU).
+        *
+        * @covers HashBagOStuff::__construct
+        * @covers HashBagOStuff::doGet
+        * @covers HashBagOStuff::hasKey
         */
        public function testEvictionGet() {
                $cache = new HashBagOStuff( [ 'maxKeys' => 3 ] );
index 6df74d6..38d63e3 100644 (file)
@@ -23,6 +23,10 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                ] );
        }
 
+       /**
+        * @covers MultiWriteBagOStuff::set
+        * @covers MultiWriteBagOStuff::doWrite
+        */
        public function testSetImmediate() {
                $key = wfRandomString();
                $value = wfRandomString();
@@ -34,6 +38,9 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                $this->assertEquals( $value, $this->cache2->get( $key ), 'Written to tier 2' );
        }
 
+       /**
+        * @covers MultiWriteBagOStuff
+        */
        public function testSyncMerge() {
                $key = wfRandomString();
                $value = wfRandomString();
@@ -69,6 +76,9 @@ class MultiWriteBagOStuffTest extends MediaWikiTestCase {
                $dbw->commit();
        }
 
+       /**
+        * @covers MultiWriteBagOStuff::set
+        */
        public function testSetDelayed() {
                $key = wfRandomString();
                $value = wfRandomString();
index bd076ba..985554b 100644 (file)
@@ -92,6 +92,57 @@ class UserTest extends MediaWikiTestCase {
                $this->assertNotContains( 'nukeworld', $rights );
        }
 
+       /**
+        * @covers User::getRights
+        */
+       public function testUserGetRightsHooks() {
+               $user = new User;
+               $user->addGroup( 'unittesters' );
+               $user->addGroup( 'testwriters' );
+               $userWrapper = TestingAccessWrapper::newFromObject( $user );
+
+               $rights = $user->getRights();
+               $this->assertContains( 'test', $rights, 'sanity check' );
+               $this->assertContains( 'runtest', $rights, 'sanity check' );
+               $this->assertContains( 'writetest', $rights, 'sanity check' );
+               $this->assertNotContains( 'nukeworld', $rights, 'sanity check' );
+
+               // Add a hook manipluating the rights
+               $this->mergeMwGlobalArrayValue( 'wgHooks', [ 'UserGetRights' => [ function ( $user, &$rights ) {
+                       $rights[] = 'nukeworld';
+                       $rights = array_diff( $rights, [ 'writetest' ] );
+               } ] ] );
+
+               $userWrapper->mRights = null;
+               $rights = $user->getRights();
+               $this->assertContains( 'test', $rights );
+               $this->assertContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertContains( 'nukeworld', $rights );
+
+               // Add a Session that limits rights
+               $mock = $this->getMockBuilder( stdclass::class )
+                       ->setMethods( [ 'getAllowedUserRights', 'deregisterSession', 'getSessionId' ] )
+                       ->getMock();
+               $mock->method( 'getAllowedUserRights' )->willReturn( [ 'test', 'writetest' ] );
+               $mock->method( 'getSessionId' )->willReturn(
+                       new MediaWiki\Session\SessionId( str_repeat( 'X', 32 ) )
+               );
+               $session = MediaWiki\Session\TestUtils::getDummySession( $mock );
+               $mockRequest = $this->getMockBuilder( FauxRequest::class )
+                       ->setMethods( [ 'getSession' ] )
+                       ->getMock();
+               $mockRequest->method( 'getSession' )->willReturn( $session );
+               $userWrapper->mRequest = $mockRequest;
+
+               $userWrapper->mRights = null;
+               $rights = $user->getRights();
+               $this->assertContains( 'test', $rights );
+               $this->assertNotContains( 'runtest', $rights );
+               $this->assertNotContains( 'writetest', $rights );
+               $this->assertNotContains( 'nukeworld', $rights );
+       }
+
        /**
         * @dataProvider provideGetGroupsWithPermission
         * @covers User::getGroupsWithPermission
index 275c0d1..711eab6 100644 (file)
@@ -74,11 +74,9 @@ class ExtensionJsonValidationTest extends PHPUnit_Framework_TestCase {
                        $version <= ExtensionRegistry::MANIFEST_VERSION,
                        "$path is using a non-supported schema version"
                );
-               $retriever = new JsonSchema\Uri\UriRetriever();
-               $schema = $retriever->retrieve( 'file://' . $schemaPath );
 
-               $validator = new JsonSchema\Validator();
-               $validator->check( $data, $schema );
+               $validator = new JsonSchema\Validator;
+               $validator->check( $data, (object) [ '$ref' => 'file://' . $schemaPath ] );
                if ( $validator->isValid() ) {
                        // All good.
                        $this->assertTrue( true );