Merge "mw.loader: Make 'mwLoadEnd' less expensive with a single using()"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 31 Aug 2016 01:52:31 +0000 (01:52 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 31 Aug 2016 01:52:31 +0000 (01:52 +0000)
62 files changed:
RELEASE-NOTES-1.28
includes/EditPage.php
includes/MediaWiki.php
includes/MediaWikiServices.php
includes/WebRequest.php
includes/api/ApiBase.php
includes/api/ApiPageSet.php
includes/api/i18n/de.json
includes/api/i18n/diq.json
includes/api/i18n/en.json
includes/api/i18n/gl.json
includes/api/i18n/he.json
includes/api/i18n/it.json
includes/api/i18n/ja.json
includes/api/i18n/oc.json
includes/api/i18n/zh-hans.json
includes/auth/AuthManager.php
includes/auth/AuthenticationProvider.php
includes/auth/AuthenticationRequest.php
includes/auth/AuthenticationResponse.php
includes/auth/PreAuthenticationProvider.php
includes/auth/PrimaryAuthenticationProvider.php
includes/auth/SecondaryAuthenticationProvider.php
includes/db/DBConnRef.php
includes/db/Database.php
includes/db/IDatabase.php
includes/db/loadbalancer/LoadBalancer.php
includes/installer/i18n/diq.json
includes/jobqueue/JobRunner.php
includes/objectcache/SqlBagOStuff.php
includes/session/Session.php
includes/session/SessionBackend.php
includes/session/SessionInfo.php
includes/session/SessionManager.php
includes/session/SessionManagerInterface.php
includes/session/SessionProvider.php
includes/specials/SpecialPageLanguage.php
languages/i18n/ang.json
languages/i18n/ar.json
languages/i18n/bn.json
languages/i18n/diq.json
languages/i18n/dty.json
languages/i18n/eo.json
languages/i18n/jv.json
languages/i18n/kk-cyrl.json
languages/i18n/sv.json
languages/i18n/ur.json
languages/i18n/vi.json
resources/src/mediawiki.special/mediawiki.special.apisandbox.js
resources/src/mediawiki.widgets/mw.widgets.CategoryCapsuleItemWidget.js
resources/src/mediawiki/api.js
resources/src/mediawiki/api/messages.js
resources/src/mediawiki/api/options.js
resources/src/mediawiki/mediawiki.Title.js
resources/src/mediawiki/mediawiki.toc.js
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/api/ApiBaseTest.php
tests/phpunit/includes/api/ApiPageSetTest.php
tests/phpunit/includes/api/MockApi.php
tests/phpunit/includes/api/query/ApiQueryTest.php
tests/qunit/suites/resources/mediawiki.api/mediawiki.api.options.test.js
tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js

index 0b7e68b..6639a95 100644 (file)
@@ -47,6 +47,9 @@ production.
   collation, you will need to run the updateCollation.php maintenance script.
 * Two new codes have been added to #time parser function: "xit" for days in current
   month, and "xiz" for days passed in the year, both in Iranian calendar.
+* mw.Api has a new option, useUS, to use U+001F (Unit Separator) when
+  appropriate for sending multi-valued parameters. This defaults to true when
+  the mw.Api instance seems to be for the local wiki.
 
 === External library changes in 1.28 ===
 
@@ -84,6 +87,15 @@ production.
   action=createaccount, action=linkaccount, and action=changeauthenticationdata
   in the query string is now deprecated and outputs a warning. They should be
   submitted in the POST body instead.
+* (T141960) Multi-valued parameters may now be separated using U+001F (Unit Separator)
+  instead of the pipe character. This will be useful if some of the multiple
+  values need to contain pipes, e.g. for action=options.
+* The API will now warn if input is not NFC-normalized Unicode or if it
+  contains invalid characters.
+* The 'normalized' list output by action=query and other modules that use
+  ApiPageSet may contain entries where the 'from' value is percent-encoded as
+  the raw value cannot be represented in a valid API response. These are
+  indicated by a 'fromencoded' boolean alongside the existing 'from' parameter.
 
 === Action API internal changes in 1.28 ===
 * Added a new hook, 'ApiMakeParserOptions', to allow extensions to better
index d0d41ac..e0080fa 100644 (file)
@@ -507,6 +507,7 @@ 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;
@@ -514,15 +515,13 @@ 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 ( $request->getBool( 'redlink' ) && $this->mTitle->exists() ) {
-                       $out->redirect( $this->mTitle->getFullURL() );
+               if ( $wgRequest->getBool( 'redlink' ) && $this->mTitle->exists() ) {
+                       $wgOut->redirect( $this->mTitle->getFullURL() );
                        return;
                }
 
-               $this->importFormData( $request );
+               $this->importFormData( $wgRequest );
                $this->firsttime = false;
 
                if ( wfReadOnly() && $this->save ) {
@@ -551,7 +550,7 @@ class EditPage {
                        wfDebug( __METHOD__ . ": User can't edit\n" );
                        // Auto-block user's IP if the account was "hard" blocked
                        if ( !wfReadOnly() ) {
-                               $user = $this->context->getUser();
+                               $user = $wgUser;
                                DeferredUpdates::addCallableUpdate( function () use ( $user ) {
                                        $user->spreadAnyEditBlock();
                                } );
@@ -625,14 +624,15 @@ class EditPage {
         * @return array
         */
        protected function getEditPermissionErrors( $rigor = 'secure' ) {
-               $user = $this->context->getUser();
-               $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user, $rigor );
+               global $wgUser;
+
+               $permErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser, $rigor );
                # Can this title be created?
                if ( !$this->mTitle->exists() ) {
                        $permErrors = array_merge(
                                $permErrors,
                                wfArrayDiff2(
-                                       $this->mTitle->getUserPermissionsErrors( 'create', $user, $rigor ),
+                                       $this->mTitle->getUserPermissionsErrors( 'create', $wgUser, $rigor ),
                                        $permErrors
                                )
                        );
@@ -665,12 +665,13 @@ class EditPage {
         * @throws PermissionsError
         */
        protected function displayPermissionsError( array $permErrors ) {
-               $out = $this->context->getOutput();
-               if ( $this->context->getRequest()->getBool( 'redlink' ) ) {
+               global $wgRequest, $wgOut;
+
+               if ( $wgRequest->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.
-                       $out->redirect( $this->mTitle->getFullURL() );
+                       $wgOut->redirect( $this->mTitle->getFullURL() );
                        return;
                }
 
@@ -685,7 +686,7 @@ class EditPage {
 
                $this->displayViewSourcePage(
                        $content,
-                       $out->formatPermissionsErrorMessage( $permErrors, 'edit' )
+                       $wgOut->formatPermissionsErrorMessage( $permErrors, 'edit' )
                );
        }
 
@@ -695,28 +696,29 @@ class EditPage {
         * @param string $errorMessage additional wikitext error message to display
         */
        protected function displayViewSourcePage( Content $content, $errorMessage = '' ) {
-               $out = $this->context->getOutput();
-               Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$out ] );
+               global $wgOut;
 
-               $out->setRobotPolicy( 'noindex,nofollow' );
-               $out->setPageTitle( wfMessage(
+               Hooks::run( 'EditPage::showReadOnlyForm:initial', [ $this, &$wgOut ] );
+
+               $wgOut->setRobotPolicy( 'noindex,nofollow' );
+               $wgOut->setPageTitle( wfMessage(
                        'viewsource-title',
                        $this->getContextTitle()->getPrefixedText()
                ) );
-               $out->addBacklinkSubtitle( $this->getContextTitle() );
-               $out->addHTML( $this->editFormPageTop );
-               $out->addHTML( $this->editFormTextTop );
+               $wgOut->addBacklinkSubtitle( $this->getContextTitle() );
+               $wgOut->addHTML( $this->editFormPageTop );
+               $wgOut->addHTML( $this->editFormTextTop );
 
                if ( $errorMessage !== '' ) {
-                       $out->addWikiText( $errorMessage );
-                       $out->addHTML( "<hr />\n" );
+                       $wgOut->addWikiText( $errorMessage );
+                       $wgOut->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;
-                       $out->addWikiMsg( 'viewyourtext' );
+                       $wgOut->addWikiMsg( 'viewyourtext' );
                } else {
                        try {
                                $text = $this->toEditText( $content );
@@ -725,21 +727,21 @@ class EditPage {
                                # (e.g. for an old revision with a different model)
                                $text = $content->serialize();
                        }
-                       $out->addWikiMsg( 'viewsourcetext' );
+                       $wgOut->addWikiMsg( 'viewsourcetext' );
                }
 
-               $out->addHTML( $this->editFormTextBeforeContent );
+               $wgOut->addHTML( $this->editFormTextBeforeContent );
                $this->showTextbox( $text, 'wpTextbox1', [ 'readonly' ] );
-               $out->addHTML( $this->editFormTextAfterContent );
+               $wgOut->addHTML( $this->editFormTextAfterContent );
 
-               $out->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
+               $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
                        Linker::formatTemplates( $this->getTemplates() ) ) );
 
-               $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+               $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
 
-               $out->addHTML( $this->editFormTextBottom );
+               $wgOut->addHTML( $this->editFormTextBottom );
                if ( $this->mTitle->exists() ) {
-                       $out->returnToMain( null, $this->mTitle );
+                       $wgOut->returnToMain( null, $this->mTitle );
                }
        }
 
@@ -749,19 +751,18 @@ class EditPage {
         * @return bool
         */
        protected function previewOnOpen() {
-               global $wgPreviewOnOpenNamespaces;
-               $request = $this->context->getRequest();
-               if ( $request->getVal( 'preview' ) == 'yes' ) {
+               global $wgRequest, $wgUser, $wgPreviewOnOpenNamespaces;
+               if ( $wgRequest->getVal( 'preview' ) == 'yes' ) {
                        // Explicit override from request
                        return true;
-               } elseif ( $request->getVal( 'preview' ) == 'no' ) {
+               } elseif ( $wgRequest->getVal( 'preview' ) == 'no' ) {
                        // Explicit override from request
                        return false;
                } elseif ( $this->section == 'new' ) {
                        // Nothing *to* preview for new sections
                        return false;
-               } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() )
-                       && $this->context->getUser()->getOption( 'previewonfirst' )
+               } elseif ( ( $wgRequest->getVal( 'preload' ) !== null || $this->mTitle->exists() )
+                       && $wgUser->getOption( 'previewonfirst' )
                ) {
                        // Standard preference behavior
                        return true;
@@ -814,7 +815,7 @@ class EditPage {
         * @throws ErrorPageError
         */
        function importFormData( &$request ) {
-               global $wgContLang;
+               global $wgContLang, $wgUser;
 
                # Section edit can come from either the form or a link
                $this->section = $request->getVal( 'wpSection', $request->getVal( 'section' ) );
@@ -926,14 +927,13 @@ 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() == $user->getName()
+                               && $this->mTitle->getText() == $wgUser->getName()
                        ) {
                                $this->allowBlankSummary = true;
                        } else {
                                $this->allowBlankSummary = $request->getBool( 'wpIgnoreBlankSummary' )
-                                       || !$user->getOption( 'forceeditsummary' );
+                                       || !$wgUser->getOption( 'forceeditsummary' );
                        }
 
                        $this->autoSumm = $request->getText( 'wpAutoSummary' );
@@ -1039,6 +1039,7 @@ class EditPage {
         * @return bool If the requested section is valid
         */
        function initialiseForm() {
+               global $wgUser;
                $this->edittime = $this->page->getTimestamp();
                $this->editRevId = $this->page->getLatest();
 
@@ -1047,21 +1048,20 @@ 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 ( $user->getOption( 'watchdefault' ) ) {
+               if ( $wgUser->getOption( 'watchdefault' ) ) {
                        # Watch all edits
                        $this->watchthis = true;
-               } elseif ( $user->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
+               } elseif ( $wgUser->getOption( 'watchcreations' ) && !$this->mTitle->exists() ) {
                        # Watch creations
                        $this->watchthis = true;
-               } elseif ( $user->isWatched( $this->mTitle ) ) {
+               } elseif ( $wgUser->isWatched( $this->mTitle ) ) {
                        # Already watched
                        $this->watchthis = true;
                }
-               if ( $user->getOption( 'minordefault' ) && !$this->isNew ) {
+               if ( $wgUser->getOption( 'minordefault' ) && !$this->isNew ) {
                        $this->minoredit = true;
                }
                if ( $this->textbox1 === false ) {
@@ -1078,11 +1078,9 @@ class EditPage {
         * @since 1.21
         */
        protected function getContentObject( $def_content = null ) {
-               global $wgContLang;
+               global $wgOut, $wgRequest, $wgUser, $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.
@@ -1095,10 +1093,10 @@ class EditPage {
                        }
                        if ( $content === false ) {
                                # If requested, preload some text.
-                               $preload = $request->getVal( 'preload',
+                               $preload = $wgRequest->getVal( 'preload',
                                        // Custom preload text for new sections
                                        $this->section === 'new' ? 'MediaWiki:addsection-preload' : '' );
-                               $params = $request->getArray( 'preloadparams', [] );
+                               $params = $wgRequest->getArray( 'preloadparams', [] );
 
                                $content = $this->getPreloadedContent( $preload, $params );
                        }
@@ -1106,15 +1104,15 @@ class EditPage {
                } else {
                        if ( $this->section != '' ) {
                                // Get section edit text (returns $def_text for invalid sections)
-                               $orig = $this->getOriginalContent( $user );
+                               $orig = $this->getOriginalContent( $wgUser );
                                $content = $orig ? $orig->getSection( $this->section ) : null;
 
                                if ( !$content ) {
                                        $content = $def_content;
                                }
                        } else {
-                               $undoafter = $request->getInt( 'undoafter' );
-                               $undo = $request->getInt( 'undo' );
+                               $undoafter = $wgRequest->getInt( 'undoafter' );
+                               $undo = $wgRequest->getInt( 'undo' );
 
                                if ( $undo > 0 && $undoafter > 0 ) {
                                        $undorev = Revision::newFromId( $undo );
@@ -1134,8 +1132,8 @@ class EditPage {
                                                        $undoMsg = 'failure';
                                                } else {
                                                        $oldContent = $this->page->getContent( Revision::RAW );
-                                                       $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
-                                                       $newContent = $content->preSaveTransform( $this->mTitle, $user, $popts );
+                                                       $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
+                                                       $newContent = $content->preSaveTransform( $this->mTitle, $wgUser, $popts );
 
                                                        if ( $newContent->equals( $oldContent ) ) {
                                                                # Tell the user that the undo results in no change,
@@ -1182,13 +1180,12 @@ class EditPage {
 
                                        // Messages: undo-success, undo-failure, undo-norev, undo-nochange
                                        $class = ( $undoMsg == 'success' ? '' : 'error ' ) . "mw-undo-{$undoMsg}";
-                                       $this->editFormPageTop .= $this->context->getOutput()->parse(
-                                               "<div class=\"{$class}\">" .
+                                       $this->editFormPageTop .= $wgOut->parse( "<div class=\"{$class}\">" .
                                                wfMessage( 'undo-' . $undoMsg )->plain() . '</div>', true, /* interface */true );
                                }
 
                                if ( $content === false ) {
-                                       $content = $this->getOriginalContent( $user );
+                                       $content = $this->getOriginalContent( $wgUser );
                                }
                        }
                }
@@ -1385,10 +1382,10 @@ class EditPage {
         * @private
         */
        function tokenOk( &$request ) {
+               global $wgUser;
                $token = $request->getVal( 'wpEditToken' );
-               $user = $this->context->getUser();
-               $this->mTokenOk = $user->matchEditToken( $token );
-               $this->mTokenOkExceptSuffix = $user->matchEditTokenNoSuffix( $token );
+               $this->mTokenOk = $wgUser->matchEditToken( $token );
+               $this->mTokenOkExceptSuffix = $wgUser->matchEditTokenNoSuffix( $token );
                return $this->mTokenOk;
        }
 
@@ -1419,7 +1416,7 @@ class EditPage {
                        $val = 'restored';
                }
 
-               $response = $this->context->getRequest()->response();
+               $response = RequestContext::getMain()->getRequest()->response();
                $response->setCookie( $postEditKey, $val, time() + self::POST_EDIT_COOKIE_DURATION, [
                        'httpOnly' => false,
                ] );
@@ -1432,8 +1429,10 @@ class EditPage {
         * @return Status The resulting status object.
         */
        public function attemptSave( &$resultDetails = false ) {
+               global $wgUser;
+
                # Allow bots to exempt some edits from bot flagging
-               $bot = $this->context->getUser()->isAllowed( 'bot' ) && $this->bot;
+               $bot = $wgUser->isAllowed( 'bot' ) && $this->bot;
                $status = $this->internalAttemptSave( $resultDetails, $bot );
 
                Hooks::run( 'EditPage::attemptSave:after', [ $this, $status, $resultDetails ] );
@@ -1451,6 +1450,8 @@ 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
@@ -1464,11 +1465,9 @@ 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 = $this->context->getRequest();
+               $request = RequestContext::getMain()->getRequest();
                $extraQueryRedirect = $request->getVal( 'wpExtraQueryRedirect' );
 
                switch ( $status->value ) {
@@ -1489,7 +1488,7 @@ class EditPage {
 
                        case self::AS_CANNOT_USE_CUSTOM_MODEL:
                        case self::AS_PARSE_ERROR:
-                               $out->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
+                               $wgOut->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
                                return true;
 
                        case self::AS_SUCCESS_NEW_ARTICLE:
@@ -1502,7 +1501,7 @@ class EditPage {
                                        }
                                }
                                $anchor = isset( $resultDetails['sectionanchor'] ) ? $resultDetails['sectionanchor'] : '';
-                               $out->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
+                               $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $anchor );
                                return false;
 
                        case self::AS_SUCCESS_UPDATE:
@@ -1530,7 +1529,7 @@ class EditPage {
                                        }
                                }
 
-                               $out->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
+                               $wgOut->redirect( $this->mTitle->getFullURL( $extraQuery ) . $sectionanchor );
                                return false;
 
                        case self::AS_SPAM_ERROR:
@@ -1538,7 +1537,7 @@ class EditPage {
                                return false;
 
                        case self::AS_BLOCKED_PAGE_FOR_USER:
-                               throw new UserBlockedError( $this->context->getUser()->getBlock() );
+                               throw new UserBlockedError( $wgUser->getBlock() );
 
                        case self::AS_IMAGE_REDIRECT_ANON:
                        case self::AS_IMAGE_REDIRECT_LOGGED:
@@ -1599,7 +1598,7 @@ class EditPage {
 
                // Run new style post-section-merge edit filter
                if ( !Hooks::run( 'EditFilterMergedContent',
-                               [ $this->context, $content, $status, $this->summary,
+                               [ $this->mArticle->getContext(), $content, $status, $this->summary,
                                $user, $this->minoredit ] )
                ) {
                        # Error messages etc. could be handled within the hook...
@@ -1684,11 +1683,10 @@ class EditPage {
         * time.
         */
        function internalAttemptSave( &$result, $bot = false ) {
-               global $wgParser, $wgMaxArticleSize, $wgContentHandlerUseDB;
+               global $wgUser, $wgRequest, $wgParser, $wgMaxArticleSize;
+               global $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" );
@@ -1697,11 +1695,11 @@ class EditPage {
                        return $status;
                }
 
-               $spam = $request->getText( 'wpAntispam' );
+               $spam = $wgRequest->getText( 'wpAntispam' );
                if ( $spam !== '' ) {
                        wfDebugLog(
                                'SimpleAntiSpam',
-                               $user->getName() .
+                               $wgUser->getName() .
                                ' editing "' .
                                $this->mTitle->getPrefixedText() .
                                '" submitted bogus field "' .
@@ -1730,9 +1728,9 @@ class EditPage {
                # Check image redirect
                if ( $this->mTitle->getNamespace() == NS_FILE &&
                        $textbox_content->isRedirect() &&
-                       !$user->isAllowed( 'upload' )
+                       !$wgUser->isAllowed( 'upload' )
                ) {
-                               $code = $user->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
+                               $code = $wgUser->isAnon() ? self::AS_IMAGE_REDIRECT_ANON : self::AS_IMAGE_REDIRECT_LOGGED;
                                $status->setResult( false, $code );
 
                                return $status;
@@ -1757,7 +1755,7 @@ class EditPage {
                }
                if ( $match !== false ) {
                        $result['spam'] = $match;
-                       $ip = $request->getIP();
+                       $ip = $wgRequest->getIP();
                        $pdbk = $this->mTitle->getPrefixedDBkey();
                        $match = str_replace( "\n", '', $match );
                        wfDebugLog( 'SpamRegex', "$ip spam regex hit [[$pdbk]]: \"$match\"" );
@@ -1780,10 +1778,10 @@ class EditPage {
                        return $status;
                }
 
-               if ( $user->isBlockedFrom( $this->mTitle, false ) ) {
+               if ( $wgUser->isBlockedFrom( $this->mTitle, false ) ) {
                        // Auto-block user's IP if the account was "hard" blocked
                        if ( !wfReadOnly() ) {
-                               $user->spreadAnyEditBlock();
+                               $wgUser->spreadAnyEditBlock();
                        }
                        # Check block state against master, thus 'false'.
                        $status->setResult( false, self::AS_BLOCKED_PAGE_FOR_USER );
@@ -1798,8 +1796,8 @@ class EditPage {
                        return $status;
                }
 
-               if ( !$user->isAllowed( 'edit' ) ) {
-                       if ( $user->isAnon() ) {
+               if ( !$wgUser->isAllowed( 'edit' ) ) {
+                       if ( $wgUser->isAnon() ) {
                                $status->setResult( false, self::AS_READ_ONLY_PAGE_ANON );
                                return $status;
                        } else {
@@ -1815,7 +1813,7 @@ class EditPage {
                                $status->fatal( 'editpage-cannot-use-custom-model' );
                                $status->value = self::AS_CANNOT_USE_CUSTOM_MODEL;
                                return $status;
-                       } elseif ( !$user->isAllowed( 'editcontentmodel' ) ) {
+                       } elseif ( !$wgUser->isAllowed( 'editcontentmodel' ) ) {
                                $status->setResult( false, self::AS_NO_CHANGE_CONTENT_MODEL );
                                return $status;
 
@@ -1826,7 +1824,7 @@ class EditPage {
 
                if ( $this->changeTags ) {
                        $changeTagsStatus = ChangeTags::canAddTagsAccompanyingChange(
-                               $this->changeTags, $user );
+                               $this->changeTags, $wgUser );
                        if ( !$changeTagsStatus->isOK() ) {
                                $changeTagsStatus->value = self::AS_CHANGE_TAG_ERROR;
                                return $changeTagsStatus;
@@ -1838,7 +1836,7 @@ class EditPage {
                        $status->value = self::AS_READ_ONLY_PAGE;
                        return $status;
                }
-               if ( $user->pingLimiter() || $user->pingLimiter( 'linkpurge', 0 ) ) {
+               if ( $wgUser->pingLimiter() || $wgUser->pingLimiter( 'linkpurge', 0 ) ) {
                        $status->fatal( 'actionthrottledtext' );
                        $status->value = self::AS_RATE_LIMITED;
                        return $status;
@@ -1858,7 +1856,7 @@ class EditPage {
 
                if ( $new ) {
                        // Late check for create permission, just in case *PARANOIA*
-                       if ( !$this->mTitle->userCan( 'create', $user ) ) {
+                       if ( !$this->mTitle->userCan( 'create', $wgUser ) ) {
                                $status->fatal( 'nocreatetext' );
                                $status->value = self::AS_NO_CREATE_PERMISSION;
                                wfDebug( __METHOD__ . ": no create permission\n" );
@@ -1882,7 +1880,7 @@ class EditPage {
                                return $status;
                        }
 
-                       if ( !$this->runPostMergeFilters( $textbox_content, $status, $user ) ) {
+                       if ( !$this->runPostMergeFilters( $textbox_content, $status, $wgUser ) ) {
                                return $status;
                        }
 
@@ -1918,7 +1916,7 @@ class EditPage {
                        ) {
                                $this->isConflict = true;
                                if ( $this->section == 'new' ) {
-                                       if ( $this->page->getUserText() == $user->getName() &&
+                                       if ( $this->page->getUserText() == $wgUser->getName() &&
                                                $this->page->getComment() == $this->newSectionSummary()
                                        ) {
                                                // Probably a duplicate submission of a new comment.
@@ -1934,7 +1932,7 @@ class EditPage {
                                } elseif ( $this->section == ''
                                        && Revision::userWasLastToEdit(
                                                DB_MASTER, $this->mTitle->getArticleID(),
-                                               $user->getId(), $this->edittime
+                                               $wgUser->getId(), $this->edittime
                                        )
                                ) {
                                        # Suppress edit conflict with self, except for section edits where merging is required.
@@ -2004,7 +2002,7 @@ class EditPage {
                                return $status;
                        }
 
-                       if ( !$this->runPostMergeFilters( $content, $status, $user ) ) {
+                       if ( !$this->runPostMergeFilters( $content, $status, $wgUser ) ) {
                                return $status;
                        }
 
@@ -2025,7 +2023,7 @@ class EditPage {
                                        return $status;
                                }
                        } elseif ( !$this->allowBlankSummary
-                               && !$content->equals( $this->getOriginalContent( $user ) )
+                               && !$content->equals( $this->getOriginalContent( $wgUser ) )
                                && !$content->isRedirect()
                                && md5( $this->summary ) == $this->autoSumm
                        ) {
@@ -2095,7 +2093,7 @@ class EditPage {
                        $this->summary,
                        $flags,
                        false,
-                       $user,
+                       $wgUser,
                        $content->getDefaultFormat(),
                        $this->changeTags
                );
@@ -2118,7 +2116,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
-                       $user->pingLimiter( 'linkpurge' );
+                       $wgUser->pingLimiter( 'linkpurge' );
                }
                $result['redirect'] = $content->isRedirect();
 
@@ -2127,7 +2125,7 @@ class EditPage {
                // If the content model changed, add a log entry
                if ( $changingContentModel ) {
                        $this->addContentModelChangeLogEntry(
-                               $user,
+                               $wgUser,
                                $new ? false : $oldContentModel,
                                $this->contentModel,
                                $this->summary
@@ -2161,12 +2159,13 @@ class EditPage {
         * Register the change of watch status
         */
        protected function updateWatchlist() {
-               $user = $this->context->getUser();
+               global $wgUser;
 
-               if ( !$user->isLoggedIn() ) {
+               if ( !$wgUser->isLoggedIn() ) {
                        return;
                }
 
+               $user = $wgUser;
                $title = $this->mTitle;
                $watch = $this->watchthis;
                // Do this in its own transaction to reduce contention...
@@ -2281,32 +2280,29 @@ class EditPage {
        }
 
        function setHeaders() {
-               global $wgAjaxEditStash;
-
-               $out = $this->context->getOutput();
-               $user = $this->context->getUser();
+               global $wgOut, $wgUser, $wgAjaxEditStash;
 
-               $out->addModules( 'mediawiki.action.edit' );
-               $out->addModuleStyles( 'mediawiki.action.edit.styles' );
+               $wgOut->addModules( 'mediawiki.action.edit' );
+               $wgOut->addModuleStyles( 'mediawiki.action.edit.styles' );
 
-               if ( $user->getOption( 'showtoolbar' ) ) {
+               if ( $wgUser->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).
-                       $out->addModules( 'mediawiki.toolbar' );
+                       $wgOut->addModules( 'mediawiki.toolbar' );
                }
 
-               if ( $user->getOption( 'uselivepreview' ) ) {
-                       $out->addModules( 'mediawiki.action.edit.preview' );
+               if ( $wgUser->getOption( 'uselivepreview' ) ) {
+                       $wgOut->addModules( 'mediawiki.action.edit.preview' );
                }
 
-               if ( $user->getOption( 'useeditwarning' ) ) {
-                       $out->addModules( 'mediawiki.action.edit.editWarning' );
+               if ( $wgUser->getOption( 'useeditwarning' ) ) {
+                       $wgOut->addModules( 'mediawiki.action.edit.editWarning' );
                }
 
                # Enabled article-related sidebar, toplinks, etc.
-               $out->setArticleRelated( true );
+               $wgOut->setArticleRelated( true );
 
                $contextTitle = $this->getContextTitle();
                if ( $this->isConflict ) {
@@ -2329,10 +2325,10 @@ class EditPage {
                if ( $displayTitle === false ) {
                        $displayTitle = $contextTitle->getPrefixedText();
                }
-               $out->setPageTitle( wfMessage( $msg, $displayTitle ) );
+               $wgOut->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
-               $out->addJsConfigVars( [
+               $wgOut->addJsConfigVars( [
                        'wgEditMessage' => $msg,
                        'wgAjaxEditStash' => $wgAjaxEditStash,
                ] );
@@ -2342,16 +2338,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
-                       $out->wrapWikiMsg( "<div class='mw-editinginterface'>\n$1\n</div>", 'editinginterface' );
+                       $wgOut->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 )
@@ -2359,7 +2355,7 @@ class EditPage {
                        ) {
                                $defaultMessageText = $this->mTitle->getDefaultMessageText();
                                if ( $defaultMessageText !== false ) {
-                                       $out->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
+                                       $wgOut->wrapWikiMsg( "<div class='mw-translateinterface'>\n$1\n</div>",
                                                'translateinterface' );
                                }
                        }
@@ -2371,11 +2367,11 @@ class EditPage {
                                # there must be a description url to show a hint to shared repo
                                if ( $descUrl ) {
                                        if ( !$this->mTitle->exists() ) {
-                                               $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
+                                               $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-create\">\n$1\n</div>", [
                                                                        'sharedupload-desc-create', $file->getRepo()->getDisplayName(), $descUrl
                                                ] );
                                        } else {
-                                               $out->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
+                                               $wgOut->wrapWikiMsg( "<div class=\"mw-sharedupload-desc-edit\">\n$1\n</div>", [
                                                                        'sharedupload-desc-edit', $file->getRepo()->getDisplayName(), $descUrl
                                                ] );
                                        }
@@ -2391,12 +2387,12 @@ class EditPage {
                        $ip = User::isIP( $username );
                        $block = Block::newFromTarget( $user, $user );
                        if ( !( $user && $user->isLoggedIn() ) && !$ip ) { # User does not exist
-                               $out->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
+                               $wgOut->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(
-                                       $out,
+                                       $wgOut,
                                        'block',
                                        MWNamespace::getCanonicalName( NS_USER ) . ':' . $block->getTarget(),
                                        '',
@@ -2416,8 +2412,8 @@ class EditPage {
                        $helpLink = wfExpandUrl( Skin::makeInternalOrExternalUrl(
                                wfMessage( 'helppage' )->inContentLanguage()->text()
                        ) );
-                       if ( $this->context->getUser()->isLoggedIn() ) {
-                               $out->wrapWikiMsg(
+                       if ( $wgUser->isLoggedIn() ) {
+                               $wgOut->wrapWikiMsg(
                                        // Suppress the external link icon, consider the help url an internal one
                                        "<div class=\"mw-newarticletext plainlinks\">\n$1\n</div>",
                                        [
@@ -2426,7 +2422,7 @@ class EditPage {
                                        ]
                                );
                        } else {
-                               $out->wrapWikiMsg(
+                               $wgOut->wrapWikiMsg(
                                        // Suppress the external link icon, consider the help url an internal one
                                        "<div class=\"mw-newarticletextanon plainlinks\">\n$1\n</div>",
                                        [
@@ -2438,7 +2434,7 @@ class EditPage {
                }
                # Give a notice if the user is editing a deleted/moved page...
                if ( !$this->mTitle->exists() ) {
-                       LogEventsList::showLogExtract( $out, [ 'delete', 'move' ], $this->mTitle,
+                       LogEventsList::showLogExtract( $wgOut, [ 'delete', 'move' ], $this->mTitle,
                                '',
                                [
                                        'lim' => 10,
@@ -2459,8 +2455,9 @@ 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.
-                               $this->context->getOutput()->addWikiTextTitleTidy(
+                               $wgOut->addWikiTextTitleTidy(
                                        '<div class="mw-editintro">{{:' . $title->getFullText() . '}}</div>',
                                        $this->mTitle
                                );
@@ -2540,6 +2537,8 @@ 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
@@ -2549,8 +2548,7 @@ class EditPage {
                        $previewOutput = $this->getPreviewText();
                }
 
-               $out = $this->context->getOutput();
-               Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$out ] );
+               Hooks::run( 'EditPage::showEditForm:initial', [ &$this, &$wgOut ] );
 
                $this->setHeaders();
 
@@ -2558,14 +2556,13 @@ class EditPage {
                        return;
                }
 
-               $out->addHTML( $this->editFormPageTop );
+               $wgOut->addHTML( $this->editFormPageTop );
 
-               $user = $this->context->getUser();
-               if ( $user->getOption( 'previewontop' ) ) {
+               if ( $wgUser->getOption( 'previewontop' ) ) {
                        $this->displayPreviewArea( $previewOutput, true );
                }
 
-               $out->addHTML( $this->editFormTextTop );
+               $wgOut->addHTML( $this->editFormTextTop );
 
                $showToolbar = true;
                if ( $this->wasDeletedSinceLastEdit() ) {
@@ -2574,14 +2571,14 @@ class EditPage {
                                // Add an confirmation checkbox and explanation.
                                $showToolbar = false;
                        } else {
-                               $out->wrapWikiMsg( "<div class='error mw-deleted-while-editing'>\n$1\n</div>",
+                               $wgOut->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.
-               $out->addHTML( Html::openElement(
+               $wgOut->addHTML( Html::openElement(
                        'form',
                        [
                                'id' => self::EDITFORM_ID,
@@ -2594,11 +2591,11 @@ class EditPage {
 
                if ( is_callable( $formCallback ) ) {
                        wfWarn( 'The $formCallback parameter to ' . __METHOD__ . 'is deprecated' );
-                       call_user_func_array( $formCallback, [ &$out ] );
+                       call_user_func_array( $formCallback, [ &$wgOut ] );
                }
 
                // Add an empty field to trip up spambots
-               $out->addHTML(
+               $wgOut->addHTML(
                        Xml::openElement( 'div', [ 'id' => 'antispam-container', 'style' => 'display: none;' ] )
                        . Html::rawElement(
                                'label',
@@ -2617,7 +2614,7 @@ class EditPage {
                        . Xml::closeElement( 'div' )
                );
 
-               Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$out ] );
+               Hooks::run( 'EditPage::showEditForm:fields', [ &$this, &$wgOut ] );
 
                // Put these up at the top to ensure they aren't lost on early form submission
                $this->showFormBeforeText();
@@ -2631,7 +2628,7 @@ class EditPage {
                        $key = $comment === ''
                                ? 'confirmrecreate-noreason'
                                : 'confirmrecreate';
-                       $out->addHTML(
+                       $wgOut->addHTML(
                                '<div class="mw-confirm-recreate">' .
                                        wfMessage( $key, $username, "<nowiki>$comment</nowiki>" )->parse() .
                                Xml::checkLabel( wfMessage( 'recreate' )->text(), 'wpRecreate', 'wpRecreate', false,
@@ -2643,7 +2640,7 @@ class EditPage {
 
                # When the summary is hidden, also hide them on preview/show changes
                if ( $this->nosummary ) {
-                       $out->addHTML( Html::hidden( 'nosummary', true ) );
+                       $wgOut->addHTML( Html::hidden( 'nosummary', true ) );
                }
 
                # If a blank edit summary was previously provided, and the appropriate
@@ -2654,15 +2651,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 ) ) {
-                       $out->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
+                       $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankSummary', true ) );
                }
 
                if ( $this->undidRev ) {
-                       $out->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
+                       $wgOut->addHTML( Html::hidden( 'wpUndidRevision', $this->undidRev ) );
                }
 
                if ( $this->selfRedirect ) {
-                       $out->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
+                       $wgOut->addHTML( Html::hidden( 'wpIgnoreSelfRedirect', true ) );
                }
 
                if ( $this->hasPresetSummary ) {
@@ -2673,27 +2670,27 @@ class EditPage {
                }
 
                $autosumm = $this->autoSumm ? $this->autoSumm : md5( $this->summary );
-               $out->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
+               $wgOut->addHTML( Html::hidden( 'wpAutoSummary', $autosumm ) );
 
-               $out->addHTML( Html::hidden( 'oldid', $this->oldid ) );
-               $out->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
+               $wgOut->addHTML( Html::hidden( 'oldid', $this->oldid ) );
+               $wgOut->addHTML( Html::hidden( 'parentRevId', $this->getParentRevId() ) );
 
-               $out->addHTML( Html::hidden( 'format', $this->contentFormat ) );
-               $out->addHTML( Html::hidden( 'model', $this->contentModel ) );
+               $wgOut->addHTML( Html::hidden( 'format', $this->contentFormat ) );
+               $wgOut->addHTML( Html::hidden( 'model', $this->contentModel ) );
 
                if ( $this->section == 'new' ) {
                        $this->showSummaryInput( true, $this->summary );
-                       $out->addHTML( $this->getSummaryPreview( true, $this->summary ) );
+                       $wgOut->addHTML( $this->getSummaryPreview( true, $this->summary ) );
                }
 
-               $out->addHTML( $this->editFormTextBeforeContent );
+               $wgOut->addHTML( $this->editFormTextBeforeContent );
 
-               if ( !$this->isCssJsSubpage && $showToolbar && $user->getOption( 'showtoolbar' ) ) {
-                       $out->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
+               if ( !$this->isCssJsSubpage && $showToolbar && $wgUser->getOption( 'showtoolbar' ) ) {
+                       $wgOut->addHTML( EditPage::getEditToolbar( $this->mTitle ) );
                }
 
                if ( $this->blankArticle ) {
-                       $out->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
+                       $wgOut->addHTML( Html::hidden( 'wpIgnoreBlankArticle', true ) );
                }
 
                if ( $this->isConflict ) {
@@ -2711,7 +2708,7 @@ class EditPage {
                        $this->showContentForm();
                }
 
-               $out->addHTML( $this->editFormTextAfterContent );
+               $wgOut->addHTML( $this->editFormTextAfterContent );
 
                $this->showStandardInputs();
 
@@ -2721,19 +2718,19 @@ class EditPage {
 
                $this->showEditTools();
 
-               $out->addHTML( $this->editFormTextAfterTools . "\n" );
+               $wgOut->addHTML( $this->editFormTextAfterTools . "\n" );
 
-               $out->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
+               $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'templatesUsed' ],
                        Linker::formatTemplates( $this->getTemplates(), $this->preview, $this->section != '' ) ) );
 
-               $out->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
+               $wgOut->addHTML( Html::rawElement( 'div', [ 'class' => 'hiddencats' ],
                        Linker::formatHiddenCategories( $this->page->getHiddenCategories() ) ) );
 
                if ( $this->mParserOutput ) {
-                       $out->setLimitReportData( $this->mParserOutput->getLimitReportData() );
+                       $wgOut->setLimitReportData( $this->mParserOutput->getLimitReportData() );
                }
 
-               $out->addModules( 'mediawiki.action.edit.collapsibleFooter' );
+               $wgOut->addModules( 'mediawiki.action.edit.collapsibleFooter' );
 
                if ( $this->isConflict ) {
                        try {
@@ -2746,7 +2743,7 @@ class EditPage {
                                        $this->contentFormat,
                                        $ex->getMessage()
                                );
-                               $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+                               $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
                        }
                }
 
@@ -2760,14 +2757,14 @@ class EditPage {
                } else {
                        $mode = 'text';
                }
-               $out->addHTML( Html::hidden( 'mode', $mode, [ 'id' => 'mw-edit-mode' ] ) );
+               $wgOut->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.
-               $out->addHTML( Html::hidden( 'wpUltimateParam', true ) );
-               $out->addHTML( $this->editFormTextBottom . "\n</form>\n" );
+               $wgOut->addHTML( Html::hidden( 'wpUltimateParam', true ) );
+               $wgOut->addHTML( $this->editFormTextBottom . "\n</form>\n" );
 
-               if ( !$user->getOption( 'previewontop' ) ) {
+               if ( !$wgUser->getOption( 'previewontop' ) ) {
                        $this->displayPreviewArea( $previewOutput, false );
                }
 
@@ -2793,23 +2790,21 @@ class EditPage {
         * @return bool
         */
        protected function showHeader() {
-               global $wgMaxArticleSize, $wgAllowUserCss, $wgAllowUserJs;
-
-               $out = $this->context->getOutput();
-               $user = $this->context->getUser();
+               global $wgOut, $wgUser, $wgMaxArticleSize, $wgLang;
+               global $wgAllowUserCss, $wgAllowUserJs;
 
                if ( $this->mTitle->isTalkPage() ) {
-                       $out->addWikiMsg( 'talkpagetext' );
+                       $wgOut->addWikiMsg( 'talkpagetext' );
                }
 
                // Add edit notices
                $editNotices = $this->mTitle->getEditNotices( $this->oldid );
                if ( count( $editNotices ) ) {
-                       $out->addHTML( implode( "\n", $editNotices ) );
+                       $wgOut->addHTML( implode( "\n", $editNotices ) );
                } else {
                        $msg = wfMessage( 'editnotice-notext' );
                        if ( !$msg->isDisabled() ) {
-                               $out->addHTML(
+                               $wgOut->addHTML(
                                        '<div class="mw-editnotice-notext">'
                                        . $msg->parseAsBlock()
                                        . '</div>'
@@ -2818,14 +2813,14 @@ class EditPage {
                }
 
                if ( $this->isConflict ) {
-                       $out->wrapWikiMsg( "<div class='mw-explainconflict'>\n$1\n</div>", 'explainconflict' );
+                       $wgOut->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
-                               $out->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
+                               $wgOut->showErrorPage( 'sectioneditnotsupported-title', 'sectioneditnotsupported-text' );
                                return false;
                        }
 
@@ -2839,31 +2834,31 @@ class EditPage {
                        }
 
                        if ( $this->missingComment ) {
-                               $out->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
+                               $wgOut->wrapWikiMsg( "<div id='mw-missingcommenttext'>\n$1\n</div>", 'missingcommenttext' );
                        }
 
                        if ( $this->missingSummary && $this->section != 'new' ) {
-                               $out->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
+                               $wgOut->wrapWikiMsg( "<div id='mw-missingsummary'>\n$1\n</div>", 'missingsummary' );
                        }
 
                        if ( $this->missingSummary && $this->section == 'new' ) {
-                               $out->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
+                               $wgOut->wrapWikiMsg( "<div id='mw-missingcommentheader'>\n$1\n</div>", 'missingcommentheader' );
                        }
 
                        if ( $this->blankArticle ) {
-                               $out->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
+                               $wgOut->wrapWikiMsg( "<div id='mw-blankarticle'>\n$1\n</div>", 'blankarticle' );
                        }
 
                        if ( $this->selfRedirect ) {
-                               $out->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
+                               $wgOut->wrapWikiMsg( "<div id='mw-selfredirect'>\n$1\n</div>", 'selfredirect' );
                        }
 
                        if ( $this->hookError !== '' ) {
-                               $out->addWikiText( $this->hookError );
+                               $wgOut->addWikiText( $this->hookError );
                        }
 
                        if ( !$this->checkUnicodeCompliantBrowser() ) {
-                               $out->addWikiMsg( 'nonunicodebrowser' );
+                               $wgOut->addWikiMsg( 'nonunicodebrowser' );
                        }
 
                        if ( $this->section != 'new' ) {
@@ -2871,13 +2866,13 @@ class EditPage {
                                if ( $revision ) {
                                        // Let sysop know that this will make private content public if saved
 
-                                       if ( !$revision->userCan( Revision::DELETED_TEXT, $user ) ) {
-                                               $out->wrapWikiMsg(
+                                       if ( !$revision->userCan( Revision::DELETED_TEXT, $wgUser ) ) {
+                                               $wgOut->wrapWikiMsg(
                                                        "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
                                                        'rev-deleted-text-permission'
                                                );
                                        } elseif ( $revision->isDeleted( Revision::DELETED_TEXT ) ) {
-                                               $out->wrapWikiMsg(
+                                               $wgOut->wrapWikiMsg(
                                                        "<div class='mw-warning plainlinks'>\n$1\n</div>\n",
                                                        'rev-deleted-text-view'
                                                );
@@ -2885,25 +2880,25 @@ class EditPage {
 
                                        if ( !$revision->isCurrent() ) {
                                                $this->mArticle->setOldSubtitle( $revision->getId() );
-                                               $out->addWikiMsg( 'editingold' );
+                                               $wgOut->addWikiMsg( 'editingold' );
                                        }
                                } elseif ( $this->mTitle->exists() ) {
                                        // Something went wrong
 
-                                       $out->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
+                                       $wgOut->wrapWikiMsg( "<div class='errorbox'>\n$1\n</div>\n",
                                                [ 'missing-revision', $this->oldid ] );
                                }
                        }
                }
 
                if ( wfReadOnly() ) {
-                       $out->wrapWikiMsg(
+                       $wgOut->wrapWikiMsg(
                                "<div id=\"mw-read-only-warning\">\n$1\n</div>",
                                [ 'readonlywarning', wfReadOnlyReason() ]
                        );
-               } elseif ( $user->isAnon() ) {
+               } elseif ( $wgUser->isAnon() ) {
                        if ( $this->formtype != 'preview' ) {
-                               $out->wrapWikiMsg(
+                               $wgOut->wrapWikiMsg(
                                        "<div id='mw-anon-edit-warning' class='warningbox'>\n$1\n</div>",
                                        [ 'anoneditwarning',
                                                // Log-in link
@@ -2917,7 +2912,7 @@ class EditPage {
                                        ]
                                );
                        } else {
-                               $out->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
+                               $wgOut->wrapWikiMsg( "<div id=\"mw-anon-preview-warning\" class=\"warningbox\">\n$1</div>",
                                        'anonpreviewwarning'
                                );
                        }
@@ -2925,25 +2920,25 @@ class EditPage {
                        if ( $this->isCssJsSubpage ) {
                                # Check the skin exists
                                if ( $this->isWrongCaseCssJsPage ) {
-                                       $out->wrapWikiMsg(
+                                       $wgOut->wrapWikiMsg(
                                                "<div class='error' id='mw-userinvalidcssjstitle'>\n$1\n</div>",
                                                [ 'userinvalidcssjstitle', $this->mTitle->getSkinFromCssJsSubpage() ]
                                        );
                                }
-                               if ( $this->getTitle()->isSubpageOf( $user->getUserPage() ) ) {
-                                       $out->wrapWikiMsg( '<div class="mw-usercssjspublic">$1</div>',
+                               if ( $this->getTitle()->isSubpageOf( $wgUser->getUserPage() ) ) {
+                                       $wgOut->wrapWikiMsg( '<div class="mw-usercssjspublic">$1</div>',
                                                $this->isCssSubpage ? 'usercssispublic' : 'userjsispublic'
                                        );
                                        if ( $this->formtype !== 'preview' ) {
                                                if ( $this->isCssSubpage && $wgAllowUserCss ) {
-                                                       $out->wrapWikiMsg(
+                                                       $wgOut->wrapWikiMsg(
                                                                "<div id='mw-usercssyoucanpreview'>\n$1\n</div>",
                                                                [ 'usercssyoucanpreview' ]
                                                        );
                                                }
 
                                                if ( $this->isJsSubpage && $wgAllowUserJs ) {
-                                                       $out->wrapWikiMsg(
+                                                       $wgOut->wrapWikiMsg(
                                                                "<div id='mw-userjsyoucanpreview'>\n$1\n</div>",
                                                                [ 'userjsyoucanpreview' ]
                                                        );
@@ -2963,7 +2958,7 @@ class EditPage {
                                # Then it must be protected based on static groups (regular)
                                $noticeMsg = 'protectedpagewarning';
                        }
-                       LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
+                       LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
                                [ 'lim' => 1, 'msgKey' => [ $noticeMsg ] ] );
                }
                if ( $this->mTitle->isCascadeProtected() ) {
@@ -2979,10 +2974,10 @@ class EditPage {
                                }
                        }
                        $notice .= '</div>';
-                       $out->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
+                       $wgOut->wrapWikiMsg( $notice, [ 'cascadeprotectedwarning', $cascadeSourcesCount ] );
                }
                if ( !$this->mTitle->exists() && $this->mTitle->getRestrictions( 'create' ) ) {
-                       LogEventsList::showLogExtract( $out, 'protect', $this->mTitle, '',
+                       LogEventsList::showLogExtract( $wgOut, 'protect', $this->mTitle, '',
                                [ 'lim' => 1,
                                        'showIfEmpty' => false,
                                        'msgKey' => [ 'titleprotectedwarning' ],
@@ -2993,21 +2988,20 @@ class EditPage {
                        $this->contentLength = strlen( $this->textbox1 );
                }
 
-               $lang = $this->context->getLanguage();
                if ( $this->tooBig || $this->contentLength > $wgMaxArticleSize * 1024 ) {
-                       $out->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
+                       $wgOut->wrapWikiMsg( "<div class='error' id='mw-edit-longpageerror'>\n$1\n</div>",
                                [
                                        'longpageerror',
-                                       $lang->formatNum( round( $this->contentLength / 1024, 3 ) ),
-                                       $lang->formatNum( $wgMaxArticleSize )
+                                       $wgLang->formatNum( round( $this->contentLength / 1024, 3 ) ),
+                                       $wgLang->formatNum( $wgMaxArticleSize )
                                ]
                        );
                } else {
                        if ( !wfMessage( 'longpage-hint' )->isDisabled() ) {
-                               $out->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
+                               $wgOut->wrapWikiMsg( "<div id='mw-edit-longpage-hint'>\n$1\n</div>",
                                        [
                                                'longpage-hint',
-                                               $lang->formatSize( strlen( $this->textbox1 ) ),
+                                               $wgLang->formatSize( strlen( $this->textbox1 ) ),
                                                strlen( $this->textbox1 )
                                        ]
                                );
@@ -3072,6 +3066,7 @@ 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 ) {
@@ -3090,7 +3085,7 @@ class EditPage {
                        [ 'class' => $summaryClass ],
                        []
                );
-               $this->context->getOutput()->addHTML( "{$label} {$input}" );
+               $wgOut->addHTML( "{$label} {$input}" );
        }
 
        /**
@@ -3122,9 +3117,9 @@ class EditPage {
        }
 
        protected function showFormBeforeText() {
+               global $wgOut;
                $section = htmlspecialchars( $this->section );
-               $out = $this->context->getOutput();
-               $out->addHTML( <<<HTML
+               $wgOut->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" />
@@ -3134,11 +3129,12 @@ class EditPage {
 HTML
                );
                if ( !$this->checkUnicodeCompliantBrowser() ) {
-                       $out->addHTML( Html::hidden( 'safemode', '1' ) );
+                       $wgOut->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
@@ -3151,10 +3147,7 @@ HTML
                 * include the constant suffix to prevent editing from
                 * broken text-mangling proxies.
                 */
-               $token = $this->context->getUser()->getEditToken();
-               $this->context->getOutput()->addHTML(
-                       "\n" . Html::hidden( "wpEditToken", $token ) . "\n"
-               );
+               $wgOut->addHTML( "\n" . Html::hidden( "wpEditToken", $wgUser->getEditToken() ) . "\n" );
        }
 
        /**
@@ -3224,6 +3217,8 @@ 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
@@ -3233,12 +3228,11 @@ HTML
                        $wikitext .= "\n";
                }
 
-               $user = $this->context->getUser();
                $attribs = $customAttribs + [
                        'accesskey' => ',',
                        'id' => $name,
-                       'cols' => $user->getIntOption( 'cols' ),
-                       'rows' => $user->getIntOption( 'rows' ),
+                       'cols' => $wgUser->getIntOption( 'cols' ),
+                       'rows' => $wgUser->getIntOption( 'rows' ),
                        // Avoid PHP notices when appending preferences
                        // (appending allows customAttribs['style'] to still work).
                        'style' => ''
@@ -3248,10 +3242,11 @@ HTML
                $attribs['lang'] = $pageLang->getHtmlCode();
                $attribs['dir'] = $pageLang->getDir();
 
-               $this->context->getOutput()->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
+               $wgOut->addHTML( Html::textarea( $name, $wikitext, $attribs ) );
        }
 
        protected function displayPreviewArea( $previewOutput, $isOnTop = false ) {
+               global $wgOut;
                $classes = [];
                if ( $isOnTop ) {
                        $classes[] = 'ontop';
@@ -3263,8 +3258,7 @@ HTML
                        $attribs['style'] = 'display: none;';
                }
 
-               $out = $this->context->getOutput();
-               $out->addHTML( Xml::openElement( 'div', $attribs ) );
+               $wgOut->addHTML( Xml::openElement( 'div', $attribs ) );
 
                if ( $this->formtype == 'preview' ) {
                        $this->showPreview( $previewOutput );
@@ -3273,10 +3267,10 @@ HTML
                        $pageViewLang = $this->mTitle->getPageViewLanguage();
                        $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
                                'class' => 'mw-content-' . $pageViewLang->getDir() ];
-                       $out->addHTML( Html::rawElement( 'div', $attribs ) );
+                       $wgOut->addHTML( Html::rawElement( 'div', $attribs ) );
                }
 
-               $out->addHTML( '</div>' );
+               $wgOut->addHTML( '</div>' );
 
                if ( $this->formtype == 'diff' ) {
                        try {
@@ -3288,7 +3282,7 @@ HTML
                                        $this->contentFormat,
                                        $ex->getMessage()
                                );
-                               $out->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
+                               $wgOut->addWikiText( '<div class="error">' . $msg->text() . '</div>' );
                        }
                }
        }
@@ -3300,14 +3294,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', [ &$out, &$text ] );
-               $out->addHTML( $text );
+               Hooks::run( 'OutputPageBeforeHTML', [ &$wgOut, &$text ] );
+               $wgOut->addHTML( $text );
                if ( $this->mTitle->getNamespace() == NS_CATEGORY ) {
                        $this->mArticle->closeShowCategory();
                }
@@ -3321,7 +3315,7 @@ HTML
         * save and then make a comparison.
         */
        function showDiff() {
-               global $wgContLang;
+               global $wgUser, $wgContLang, $wgOut;
 
                $oldtitlemsg = 'currentrev';
                # if message does not exist, show diff against the preloaded default
@@ -3352,9 +3346,8 @@ HTML
                        ContentHandler::runLegacyHooks( 'EditPageGetDiffText', [ $this, &$newContent ] );
                        Hooks::run( 'EditPageGetDiffContent', [ $this, &$newContent ] );
 
-                       $user = $this->context->getUser();
-                       $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
-                       $newContent = $newContent->preSaveTransform( $this->mTitle, $user, $popts );
+                       $popts = ParserOptions::newFromUserAndLang( $wgUser, $wgContLang );
+                       $newContent = $newContent->preSaveTransform( $this->mTitle, $wgUser, $popts );
                }
 
                if ( ( $oldContent && !$oldContent->isEmpty() ) || ( $newContent && !$newContent->isEmpty() ) ) {
@@ -3378,7 +3371,7 @@ HTML
                        $difftext = '';
                }
 
-               $this->context->getOutput()->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
+               $wgOut->addHTML( '<div id="wikiDiff">' . $difftext . '</div>' );
        }
 
        /**
@@ -3387,7 +3380,8 @@ HTML
        protected function showHeaderCopyrightWarning() {
                $msg = 'editpage-head-copy-warn';
                if ( !wfMessage( $msg )->isDisabled() ) {
-                       $this->context->getOutput()->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
+                       global $wgOut;
+                       $wgOut->wrapWikiMsg( "<div class='editpage-head-copywarn'>\n$1\n</div>",
                                'editpage-head-copy-warn' );
                }
        }
@@ -3404,15 +3398,16 @@ HTML
                $msg = 'editpage-tos-summary';
                Hooks::run( 'EditPageTosSummary', [ $this->mTitle, &$msg ] );
                if ( !wfMessage( $msg )->isDisabled() ) {
-                       $out = $this->context->getOutput();
-                       $out->addHTML( '<div class="mw-tos-summary">' );
-                       $out->addWikiMsg( $msg );
-                       $out->addHTML( '</div>' );
+                       global $wgOut;
+                       $wgOut->addHTML( '<div class="mw-tos-summary">' );
+                       $wgOut->addWikiMsg( $msg );
+                       $wgOut->addHTML( '</div>' );
                }
        }
 
        protected function showEditTools() {
-               $this->context->getOutput()->addHTML( '<div class="mw-editTools">' .
+               global $wgOut;
+               $wgOut->addHTML( '<div class="mw-editTools">' .
                        wfMessage( 'edittools' )->inContentLanguage()->parse() .
                        '</div>' );
        }
@@ -3472,24 +3467,24 @@ HTML
        }
 
        protected function showStandardInputs( &$tabindex = 2 ) {
-               $out = $this->context->getOutput();
-               $out->addHTML( "<div class='editOptions'>\n" );
+               global $wgOut;
+               $wgOut->addHTML( "<div class='editOptions'>\n" );
 
                if ( $this->section != 'new' ) {
                        $this->showSummaryInput( false, $this->summary );
-                       $out->addHTML( $this->getSummaryPreview( false, $this->summary ) );
+                       $wgOut->addHTML( $this->getSummaryPreview( false, $this->summary ) );
                }
 
                $checkboxes = $this->getCheckboxes( $tabindex,
                        [ 'minor' => $this->minoredit, 'watch' => $this->watchthis ] );
-               $out->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
+               $wgOut->addHTML( "<div class='editCheckboxes'>" . implode( $checkboxes, "\n" ) . "</div>\n" );
 
                // Show copyright warning.
-               $out->addWikiText( $this->getCopywarn() );
-               $out->addHTML( $this->editFormTextAfterWarn );
+               $wgOut->addWikiText( $this->getCopywarn() );
+               $wgOut->addHTML( $this->editFormTextAfterWarn );
 
-               $out->addHTML( "<div class='editButtons'>\n" );
-               $out->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
+               $wgOut->addHTML( "<div class='editButtons'>\n" );
+               $wgOut->addHTML( implode( $this->getEditButtons( $tabindex ), "\n" ) . "\n" );
 
                $cancel = $this->getCancelLink();
                if ( $cancel !== '' ) {
@@ -3509,13 +3504,13 @@ HTML
                        wfMessage( 'word-separator' )->escaped() .
                        wfMessage( 'newwindow' )->parse();
 
-               $out->addHTML( "        <span class='cancelLink'>{$cancel}</span>\n" );
-               $out->addHTML( "        <span class='editHelp'>{$edithelp}</span>\n" );
-               $out->addHTML( "</div><!-- editButtons -->\n" );
+               $wgOut->addHTML( "      <span class='cancelLink'>{$cancel}</span>\n" );
+               $wgOut->addHTML( "      <span class='editHelp'>{$edithelp}</span>\n" );
+               $wgOut->addHTML( "</div><!-- editButtons -->\n" );
 
-               Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $out, &$tabindex ] );
+               Hooks::run( 'EditPage::showStandardInputs:options', [ $this, $wgOut, &$tabindex ] );
 
-               $out->addHTML( "</div><!-- editOptions -->\n" );
+               $wgOut->addHTML( "</div><!-- editOptions -->\n" );
        }
 
        /**
@@ -3523,9 +3518,10 @@ HTML
         * If you want to use another entry point to this function, be careful.
         */
        protected function showConflict() {
-               $out = $this->context->getOutput();
-               if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$out ] ) ) {
-                       $stats = $this->context->getStats();
+               global $wgOut;
+
+               if ( Hooks::run( 'EditPageBeforeConflictDiff', [ &$this, &$wgOut ] ) ) {
+                       $stats = $wgOut->getContext()->getStats();
                        $stats->increment( 'edit.failures.conflict' );
                        // Only include 'standard' namespaces to avoid creating unknown numbers of statsd metrics
                        if (
@@ -3535,7 +3531,7 @@ HTML
                                $stats->increment( 'edit.failures.conflict.byNamespaceId.' . $this->mTitle->getNamespace() );
                        }
 
-                       $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
+                       $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
 
                        $content1 = $this->toEditContent( $this->textbox1 );
                        $content2 = $this->toEditContent( $this->textbox2 );
@@ -3548,7 +3544,7 @@ HTML
                                wfMessage( 'storedversion' )->text()
                        );
 
-                       $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
+                       $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
                        $this->showTextbox2();
                }
        }
@@ -3661,10 +3657,10 @@ HTML
         * @return string
         */
        function getPreviewText() {
-               global $wgRawHtml, $wgAllowUserCss, $wgAllowUserJs;
+               global $wgOut, $wgRawHtml, $wgLang;
+               global $wgAllowUserCss, $wgAllowUserJs;
 
-               $stats = $this->context->getStats();
-               $out = $this->context->getOutput();
+               $stats = $wgOut->getContext()->getStats();
 
                if ( $wgRawHtml && !$this->mTokenOk ) {
                        // Could be an offsite preview attempt. This is very unsafe if
@@ -3674,7 +3670,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 = $out->parse( "<div class='previewnote'>" .
+                               $parsedNote = $wgOut->parse( "<div class='previewnote'>" .
                                        wfMessage( 'session_fail_preview_html' )->text() . "</div>", true, /* interface */true );
                        }
                        $stats->increment( 'edit.failures.session_loss' );
@@ -3696,7 +3692,7 @@ HTML
 
                        # provide a anchor link to the editform
                        $continueEditing = '<span class="mw-continue-editing">' .
-                               '[[#' . self::EDITFORM_ID . '|' . $this->context->getLanguage()->getArrow() . ' ' .
+                               '[[#' . self::EDITFORM_ID . '|' . $wgLang->getArrow() . ' ' .
                                wfMessage( 'continue-editing' )->text() . ']]</span>';
                        if ( $this->mTriedSave && !$this->mTokenOk ) {
                                if ( $this->mTokenOkExceptSuffix ) {
@@ -3762,7 +3758,7 @@ HTML
                        $parserOutput = $parserResult['parserOutput'];
                        $previewHTML = $parserResult['html'];
                        $this->mParserOutput = $parserOutput;
-                       $out->addParserOutputMetadata( $parserOutput );
+                       $wgOut->addParserOutputMetadata( $parserOutput );
 
                        if ( count( $parserOutput->getWarnings() ) ) {
                                $note .= "\n\n" . implode( "\n\n", $parserOutput->getWarnings() );
@@ -3788,7 +3784,7 @@ HTML
 
                $previewhead = "<div class='previewnote'>\n" .
                        '<h2 id="mw-previewheader">' . wfMessage( 'preview' )->escaped() . "</h2>" .
-                       $out->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
+                       $wgOut->parse( $note, true, /* interface */true ) . $conflict . "</div>\n";
 
                $pageViewLang = $this->mTitle->getPageViewLanguage();
                $attribs = [ 'lang' => $pageViewLang->getHtmlCode(), 'dir' => $pageViewLang->getDir(),
@@ -3803,7 +3799,7 @@ HTML
         * @return ParserOptions
         */
        protected function getPreviewParserOptions() {
-               $parserOptions = $this->page->makeParserOptions( $this->context );
+               $parserOptions = $this->page->makeParserOptions( $this->mArticle->getContext() );
                $parserOptions->setIsPreview( true );
                $parserOptions->setIsSectionPreview( !is_null( $this->section ) && $this->section !== '' );
                $parserOptions->enableLimitReport();
@@ -3820,11 +3816,11 @@ HTML
         *   - html: The HTML to be displayed
         */
        protected function doPreviewParse( Content $content ) {
-               $user = $this->context->getUser();
+               global $wgUser;
                $parserOptions = $this->getPreviewParserOptions();
-               $pstContent = $content->preSaveTransform( $this->mTitle, $user, $parserOptions );
+               $pstContent = $content->preSaveTransform( $this->mTitle, $wgUser, $parserOptions );
                $scopedCallback = $parserOptions->setupFakeRevision(
-                       $this->mTitle, $pstContent, $user );
+                       $this->mTitle, $pstContent, $wgUser );
                $parserOutput = $pstContent->getParserOutput( $this->mTitle, null, $parserOptions );
                ScopedCallback::consume( $scopedCallback );
                $parserOutput->setEditSectionTokens( false ); // no section edit links
@@ -4000,16 +3996,15 @@ HTML
         * @return array
         */
        public function getCheckboxes( &$tabindex, $checked ) {
-               global $wgUseMediaWikiUIEverywhere;
+               global $wgUser, $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 ( $user->isAllowed( 'minoredit' ) ) {
+                       if ( $wgUser->isAllowed( 'minoredit' ) ) {
                                $attribs = [
                                        'tabindex' => ++$tabindex,
                                        'accesskey' => wfMessage( 'accesskey-minoredit' )->text(),
@@ -4033,7 +4028,7 @@ HTML
 
                $watchLabel = wfMessage( 'watchthis' )->parse();
                $checkboxes['watch'] = '';
-               if ( $user->isLoggedIn() ) {
+               if ( $wgUser->isLoggedIn() ) {
                        $attribs = [
                                'tabindex' => ++$tabindex,
                                'accesskey' => wfMessage( 'accesskey-watch' )->text(),
@@ -4111,14 +4106,15 @@ HTML
         * they have attempted to edit a nonexistent section.
         */
        function noSuchSectionPage() {
-               $out = $this->context->getOutput();
-               $out->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
+               global $wgOut;
+
+               $wgOut->prepareErrorPage( wfMessage( 'nosuchsectiontitle' ) );
 
                $res = wfMessage( 'nosuchsectiontext', $this->section )->parseAsBlock();
                Hooks::run( 'EditPageNoSuchSection', [ &$this, &$res ] );
-               $out->addHTML( $res );
+               $wgOut->addHTML( $res );
 
-               $out->returnToMain( false, $this->mTitle );
+               $wgOut->returnToMain( false, $this->mTitle );
        }
 
        /**
@@ -4127,28 +4123,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 = $this->context->getLanguage()->listToText( $match );
+                       $match = $wgLang->listToText( $match );
                }
-               $out = $this->context->getOutput();
-               $out->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
+               $wgOut->prepareErrorPage( wfMessage( 'spamprotectiontitle' ) );
 
-               $out->addHTML( '<div id="spamprotected">' );
-               $out->addWikiMsg( 'spamprotectiontext' );
+               $wgOut->addHTML( '<div id="spamprotected">' );
+               $wgOut->addWikiMsg( 'spamprotectiontext' );
                if ( $match ) {
-                       $out->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
+                       $wgOut->addWikiMsg( 'spamprotectionmatch', wfEscapeWikiText( $match ) );
                }
-               $out->addHTML( '</div>' );
+               $wgOut->addHTML( '</div>' );
 
-               $out->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
+               $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourdiff" );
                $this->showDiff();
 
-               $out->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
+               $wgOut->wrapWikiMsg( '<h2>$1</h2>', "yourtext" );
                $this->showTextbox2();
 
-               $out->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
+               $wgOut->addReturnTo( $this->getContextTitle(), [ 'action' => 'edit' ] );
        }
 
        /**
@@ -4158,9 +4154,9 @@ HTML
         * @return bool
         */
        private function checkUnicodeCompliantBrowser() {
-               global $wgBrowserBlackList;
+               global $wgBrowserBlackList, $wgRequest;
 
-               $currentbrowser = $this->context->getRequest()->getHeader( 'User-Agent' );
+               $currentbrowser = $wgRequest->getHeader( 'User-Agent' );
                if ( $currentbrowser === false ) {
                        // No User-Agent header sent? Trust it by default...
                        return true;
index 77ac76a..cded064 100644 (file)
@@ -561,13 +561,14 @@ class MediaWiki {
                        // Abort if any transaction was too big
                        [ 'maxWriteDuration' => $config->get( 'MaxUserDBWriteDuration' ) ]
                );
-               // Record ChronologyProtector positions
-               $factory->shutdown();
-               wfDebug( __METHOD__ . ': all transactions committed' );
 
                DeferredUpdates::doUpdates( 'enqueue', DeferredUpdates::PRESEND );
                wfDebug( __METHOD__ . ': pre-send deferred updates completed' );
 
+               // Record ChronologyProtector positions
+               $factory->shutdown();
+               wfDebug( __METHOD__ . ': all transactions committed' );
+
                // Set a cookie to tell all CDN edge nodes to "stick" the user to the DC that handles this
                // POST request (e.g. the "master" data center). Also have the user briefly bypass CDN so
                // ChronologyProtector works for cacheable URLs.
index 49891df..037e96f 100644 (file)
@@ -198,7 +198,14 @@ class MediaWikiServices extends ServiceContainer {
         */
        private function salvage( self $other ) {
                foreach ( $this->getServiceNames() as $name ) {
-                       $oldService = $other->peekService( $name );
+                       // The service could be new in the new instance and not registered in the
+                       // other instance (e.g. an extension that was loaded after the instantiation of
+                       // the other instance. Skip this service in this case. See T143974
+                       try {
+                               $oldService = $other->peekService( $name );
+                       } catch ( NoSuchServiceException $e ) {
+                               continue;
+                       }
 
                        if ( $oldService instanceof SalvageableService ) {
                                /** @var SalvageableService $newService */
index b5c57ee..5492737 100644 (file)
@@ -394,6 +394,32 @@ class WebRequest {
                }
        }
 
+       /**
+        * Fetch a scalar from the input without normalization, or return $default
+        * if it's not set.
+        *
+        * Unlike self::getVal(), this does not perform any normalization on the
+        * input value.
+        *
+        * @since 1.28
+        * @param string $name
+        * @param string|null $default Optional default
+        * @return string
+        */
+       public function getRawVal( $name, $default = null ) {
+               $name = strtr( $name, '.', '_' ); // See comment in self::getGPCVal()
+               if ( isset( $this->data[$name] ) && !is_array( $this->data[$name] ) ) {
+                       $val = $this->data[$name];
+               } else {
+                       $val = $default;
+               }
+               if ( is_null( $val ) ) {
+                       return $val;
+               } else {
+                       return (string)$val;
+               }
+       }
+
        /**
         * Fetch a scalar from the input or return $default if it's not set.
         * Returns a string. Arrays are discarded. Useful for
@@ -683,6 +709,9 @@ class WebRequest {
 
        /**
         * Return the session for this request
+        *
+        * This might unpersist an existing session if it was invalid.
+        *
         * @since 1.27
         * @note For performance, keep the session locally if you will be making
         *  much use of it instead of calling this method repeatedly.
index 66c1b53..55d2430 100644 (file)
@@ -1000,6 +1000,31 @@ abstract class ApiBase extends ContextSource {
                                        $type = $this->getModuleManager()->getNames( $paramName );
                                }
                        }
+
+                       $request = $this->getMain()->getRequest();
+                       $rawValue = $request->getRawVal( $encParamName );
+                       if ( $rawValue === null ) {
+                               $rawValue = $default;
+                       }
+
+                       // Preserve U+001F for self::parseMultiValue(), or error out if that won't be called
+                       if ( isset( $value ) && substr( $rawValue, 0, 1 ) === "\x1f" ) {
+                               if ( $multi ) {
+                                       // This loses the potential $wgContLang->checkTitleEncoding() transformation
+                                       // done by WebRequest for $_GET. Let's call that a feature.
+                                       $value = join( "\x1f", $request->normalizeUnicode( explode( "\x1f", $rawValue ) ) );
+                               } else {
+                                       $this->dieUsage(
+                                               "U+001F multi-value separation may only be used for multi-valued parameters.",
+                                               'badvalue_notmultivalue'
+                                       );
+                               }
+                       }
+
+                       // Check for NFC normalization, and warn
+                       if ( $rawValue !== $value ) {
+                               $this->handleParamNormalization( $paramName, $value, $rawValue );
+                       }
                }
 
                if ( isset( $value ) && ( $multi || is_array( $type ) ) ) {
@@ -1145,6 +1170,40 @@ abstract class ApiBase extends ContextSource {
                return $value;
        }
 
+       /**
+        * Handle when a parameter was Unicode-normalized
+        * @since 1.28
+        * @param string $paramName Unprefixed parameter name
+        * @param string $value Input that will be used.
+        * @param string $rawValue Input before normalization.
+        */
+       protected function handleParamNormalization( $paramName, $value, $rawValue ) {
+               $encParamName = $this->encodeParamName( $paramName );
+               $this->setWarning(
+                       "The value passed for '$encParamName' contains invalid or non-normalized data. "
+                       . 'Textual data should be valid, NFC-normalized Unicode without '
+                       . 'C0 control characters other than HT (\\t), LF (\\n), and CR (\\r).'
+               );
+       }
+
+       /**
+        * Split a multi-valued parameter string, like explode()
+        * @since 1.28
+        * @param string $value
+        * @param int $limit
+        * @return string[]
+        */
+       protected function explodeMultiValue( $value, $limit ) {
+               if ( substr( $value, 0, 1 ) === "\x1f" ) {
+                       $sep = "\x1f";
+                       $value = substr( $value, 1 );
+               } else {
+                       $sep = '|';
+               }
+
+               return explode( $sep, $value, $limit );
+       }
+
        /**
         * Return an array of values that were given in a 'a|b|c' notation,
         * after it optionally validates them against the list allowed values.
@@ -1159,13 +1218,13 @@ abstract class ApiBase extends ContextSource {
         * @return string|string[] (allowMultiple ? an_array_of_values : a_single_value)
         */
        protected function parseMultiValue( $valueName, $value, $allowMultiple, $allowedValues ) {
-               if ( trim( $value ) === '' && $allowMultiple ) {
+               if ( ( trim( $value ) === '' || trim( $value ) === "\x1f" ) && $allowMultiple ) {
                        return [];
                }
 
                // This is a bit awkward, but we want to avoid calling canApiHighLimits()
                // because it unstubs $wgUser
-               $valuesList = explode( '|', $value, self::LIMIT_SML2 + 1 );
+               $valuesList = $this->explodeMultiValue( $value, self::LIMIT_SML2 + 1 );
                $sizeLimit = count( $valuesList ) > self::LIMIT_SML1 && $this->mMainModule->canApiHighLimits()
                        ? self::LIMIT_SML2
                        : self::LIMIT_SML1;
index 90438d4..ed229cb 100644 (file)
@@ -495,10 +495,14 @@ class ApiPageSet extends ApiBase {
         * @since 1.21
         */
        public function getNormalizedTitlesAsResult( $result = null ) {
+               global $wgContLang;
+
                $values = [];
                foreach ( $this->getNormalizedTitles() as $rawTitleStr => $titleStr ) {
+                       $encode = ( $wgContLang->normalize( $rawTitleStr ) !== $rawTitleStr );
                        $values[] = [
-                               'from' => $rawTitleStr,
+                               'fromencoded' => $encode,
+                               'from' => $encode ? rawurlencode( $rawTitleStr ) : $rawTitleStr,
                                'to' => $titleStr
                        ];
                }
@@ -1403,6 +1407,23 @@ class ApiPageSet extends ApiBase {
                return $result;
        }
 
+       protected function handleParamNormalization( $paramName, $value, $rawValue ) {
+               parent::handleParamNormalization( $paramName, $value, $rawValue );
+
+               if ( $paramName === 'titles' ) {
+                       // For the 'titles' parameter, we want to split it like ApiBase would
+                       // and add any changed titles to $this->mNormalizedTitles
+                       $value = $this->explodeMultiValue( $value, self::LIMIT_SML2 + 1 );
+                       $l = count( $value );
+                       $rawValue = $this->explodeMultiValue( $rawValue, $l );
+                       for ( $i = 0; $i < $l; $i++ ) {
+                               if ( $value[$i] !== $rawValue[$i] ) {
+                                       $this->mNormalizedTitles[$rawValue[$i]] = $value[$i];
+                               }
+                       }
+               }
+       }
+
        private static $generators = null;
 
        /**
index e293fed..a3ff076 100644 (file)
        "api-help-param-type-boolean": "Typ: boolisch ([[Special:ApiHelp/main#main/datatypes|Einzelheiten]])",
        "api-help-param-type-timestamp": "Typ: {{PLURAL:$1|1=Zeitstempel|2=Liste von Zeitstempeln}} ([[Special:ApiHelp/main#main/datatypes|erlaubte Formate]])",
        "api-help-param-type-user": "Typ: {{PLURAL:$1|1=Benutzername|2=Liste von Benutzernamen}}",
-       "api-help-param-list": "{{PLURAL:$1|1=Einer der folgenden Werte|2=Werte (mit <kbd>{{!}}</kbd> trennen)}}: $2",
+       "api-help-param-list": "{{PLURAL:$1|1=Einer der folgenden Werte|2=Werte (mit <kbd>{{!}}</kbd> trennen oder [[Special:ApiHelp/main#main/datatypes|Alternative]])}}: $2",
        "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Muss leer sein|Kann leer sein oder $2}}",
        "api-help-param-limit": "Nicht mehr als $1 erlaubt.",
        "api-help-param-limit2": "Nicht mehr als $1 ($2 für Bots) erlaubt.",
        "api-help-param-integer-max": "{{PLURAL:$1|1=Der Wert darf|2=Die Werte dürfen}} nicht größer sein als $3.",
        "api-help-param-integer-minmax": "{{PLURAL:$1|1=Der Wert muss|2=Die Werte müssen}} zwischen $2 und $3 sein.",
        "api-help-param-upload": "Muss als Dateiupload mithilfe eines multipart/form-data-Formular bereitgestellt werden.",
-       "api-help-param-multi-separate": "Werte mit <kbd>|</kbd> trennen.",
+       "api-help-param-multi-separate": "Werte mit <kbd>|</kbd> trennen oder [[Special:ApiHelp/main#main/datatypes|Alternative]].",
        "api-help-param-multi-max": "Maximale Anzahl der Werte ist {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} für Bots).",
        "api-help-param-default": "Standard: $1",
        "api-help-param-default-empty": "Standard: <span class=\"apihelp-empty\">(leer)</span>",
index 7892efa..0b86f10 100644 (file)
@@ -6,6 +6,7 @@
                        "Kumkumuk"
                ]
        },
+       "apihelp-main-param-action": "Performansa kamci aksiyon",
        "apihelp-block-description": "Enê karberi bloqe ke",
        "apihelp-block-param-reason": "Sebeba Bloqey",
        "apihelp-block-param-nocreate": "Hesab viraştişi bloqe ke.",
index 1815836..a68a87f 100644 (file)
        "api-help-param-deprecated": "Deprecated.",
        "api-help-param-required": "This parameter is required.",
        "api-help-datatypes-header": "Data types",
-       "api-help-datatypes": "Some parameter types in API requests need further explanation:\n;boolean\n:Boolean parameters work like HTML checkboxes: if the parameter is specified, regardless of value, it is considered true. For a false value, omit the parameter entirely.\n;timestamp\n:Timestamps may be specified in several formats. ISO 8601 date and time is recommended. All times are in UTC, any included timezone is ignored.\n:* ISO 8601 date and time, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (punctuation and <kbd>Z</kbd> are optional)\n:* ISO 8601 date and time with (ignored) fractional seconds, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (dashes, colons, and <kbd>Z</kbd> are optional)\n:* MediaWiki format, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Generic numeric format, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (optional timezone of <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, or <kbd>-<var>##</var></kbd> is ignored)\n:* EXIF format, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*RFC 2822 format (timezone may be omitted), <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 850 format (timezone may be omitted), <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* C ctime format, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* Seconds since 1970-01-01T00:00:00Z as a 1 to 13 digit integer (excluding <kbd>0</kbd>)\n:* The string <kbd>now</kbd>",
+       "api-help-datatypes": "Input to MediaWiki should be NFC-normalized UTF-8. MediaWiki may attempt to convert other input, but this may cause some operations (such as [[Special:ApiHelp/edit|edits]] with MD5 checks) to fail.\n\nSome parameter types in API requests need further explanation:\n;boolean\n:Boolean parameters work like HTML checkboxes: if the parameter is specified, regardless of value, it is considered true. For a false value, omit the parameter entirely.\n;timestamp\n:Timestamps may be specified in several formats. ISO 8601 date and time is recommended. All times are in UTC, any included timezone is ignored.\n:* ISO 8601 date and time, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (punctuation and <kbd>Z</kbd> are optional)\n:* ISO 8601 date and time with (ignored) fractional seconds, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (dashes, colons, and <kbd>Z</kbd> are optional)\n:* MediaWiki format, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Generic numeric format, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (optional timezone of <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, or <kbd>-<var>##</var></kbd> is ignored)\n:* EXIF format, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*RFC 2822 format (timezone may be omitted), <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 850 format (timezone may be omitted), <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* C ctime format, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* Seconds since 1970-01-01T00:00:00Z as a 1 to 13 digit integer (excluding <kbd>0</kbd>)\n:* The string <kbd>now</kbd>\n;alternative multiple-value separator\n:Parameters that take multiple values are normally submitted with the values separated using the pipe character, e.g. <kbd>param=value1|value2</kbd> or <kbd>param=value1%7Cvalue2</kbd>. If a value must contain the pipe character, use U+001F (Unit Separator) as the separator ''and'' prefix the value with U+001F, e.g. <kbd>param=%1Fvalue1%1Fvalue2</kbd>.",
        "api-help-param-type-limit": "Type: integer or <kbd>max</kbd>",
        "api-help-param-type-integer": "Type: {{PLURAL:$1|1=integer|2=list of integers}}",
        "api-help-param-type-boolean": "Type: boolean ([[Special:ApiHelp/main#main/datatypes|details]])",
        "api-help-param-type-password": "",
        "api-help-param-type-timestamp": "Type: {{PLURAL:$1|1=timestamp|2=list of timestamps}} ([[Special:ApiHelp/main#main/datatypes|allowed formats]])",
        "api-help-param-type-user": "Type: {{PLURAL:$1|1=user name|2=list of user names}}",
-       "api-help-param-list": "{{PLURAL:$1|1=One of the following values|2=Values (separate with <kbd>{{!}}</kbd>)}}: $2",
+       "api-help-param-list": "{{PLURAL:$1|1=One of the following values|2=Values (separate with <kbd>{{!}}</kbd> or [[Special:ApiHelp/main#main/datatypes|alternative]])}}: $2",
        "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Must be empty|Can be empty, or $2}}",
        "api-help-param-limit": "No more than $1 allowed.",
        "api-help-param-limit2": "No more than $1 ($2 for bots) allowed.",
        "api-help-param-integer-max": "The {{PLURAL:$1|1=value|2=values}} must be no greater than $3.",
        "api-help-param-integer-minmax": "The {{PLURAL:$1|1=value|2=values}} must be between $2 and $3.",
        "api-help-param-upload": "Must be posted as a file upload using multipart/form-data.",
-       "api-help-param-multi-separate": "Separate values with <kbd>|</kbd>.",
+       "api-help-param-multi-separate": "Separate values with <kbd>|</kbd> or [[Special:ApiHelp/main#main/datatypes|alternative]].",
        "api-help-param-multi-max": "Maximum number of values is {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} for bots).",
        "api-help-param-default": "Default: $1",
        "api-help-param-default-empty": "Default: <span class=\"apihelp-empty\">(empty)</span>",
index b752751..4180fae 100644 (file)
        "api-help-param-deprecated": "Obsoleto.",
        "api-help-param-required": "Este parámetro é obrigatorio.",
        "api-help-datatypes-header": "Tipos de datos",
-       "api-help-datatypes": "Algúns tipos de parámetros nas solicitudes de API necesitan máis explicación:\n;boolean\n:Os parámetros booleanos traballan como caixas de verificación HTML: se o parámetro se especifica, independentemente do seu valor, considérase verdadeiro. Para un valor falso, omíta o parámetro completo.\n;timestamp\n:Os selos de tempo poden especificarse en varios formatos. Recoméndase o ISO 8601 coa data e a hora. Todas as horas están en UTC, a inclusión da zona horaria é ignorada.\n:* ISO 8601 con data e hora, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (signos de puntuación e <kbd>Z</kbd> son opcionais)\n:* ISO 8601 data e hora (omítense) fraccións de segundo, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (guións, dous puntos e, <kbd>Z</kbd> son opcionais)\n:* Formato MediaWiki, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Formato numérico xenérico, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (opcional na zona horaria <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, o <kbd>-<var>##</var></kbd> omítese)\n:* Formato EXIF, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*Formato RFC 2822 (a zona horaria pódese omitir), <kbd><var>Mon</var>, <var>15</var> <var>Xan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Formato RFC 850 (a zona horaria pódese omitir), <kbd><var>luns</var>, <var>15</var>-<var>xaneiro</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Formato C ctime, <kbd><var>luns</var> <var>xaneiro</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>de 2001</var></kbd>\n:* Segundos desde 1970-01-01T00:00:00Z como de 1 a 13, díxitos enteiros (excluíndo o <kbd>0</kbd>)\n:* O texto <kbd>now</kbd> (agora)",
+       "api-help-datatypes": "A entrada a MediaWiki debe ser normalizada NFC UTF-8. MediaWiki puede intentar converter outras entradas, pero isto pode provocar que algunhas operacións (como as [[Special:ApiHelp/edit|edición]] con comprobación MD5) fallen.\n\nAlgúns tipos de parámetros nas solicitudes de API necesitan máis explicación:\n;boolean\n:Os parámetros booleanos traballan como caixas de verificación HTML: se o parámetro se especifica, independentemente do seu valor, considérase verdadeiro. Para un valor falso, omíta o parámetro completo.\n;timestamp\n:Os selos de tempo poden especificarse en varios formatos. Recoméndase o ISO 8601 coa data e a hora. Todas as horas están en UTC, a inclusión da zona horaria é ignorada.\n:* ISO 8601 con data e hora, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (signos de puntuación e <kbd>Z</kbd> son opcionais)\n:* ISO 8601 data e hora (omítense) fraccións de segundo, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (guións, dous puntos e, <kbd>Z</kbd> son opcionais)\n:* Formato MediaWiki, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Formato numérico xenérico, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (opcional na zona horaria <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, o <kbd>-<var>##</var></kbd> omítese)\n:* Formato EXIF, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*Formato RFC 2822 (a zona horaria pódese omitir), <kbd><var>Mon</var>, <var>15</var> <var>Xan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Formato RFC 850 (a zona horaria pódese omitir), <kbd><var>luns</var>, <var>15</var>-<var>xaneiro</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Formato C ctime, <kbd><var>luns</var> <var>xaneiro</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>de 2001</var></kbd>\n:* Segundos desde 1970-01-01T00:00:00Z como de 1 a 13, díxitos enteiros (excluíndo o <kbd>0</kbd>)\n:* O texto <kbd>now</kbd> (agora)",
        "api-help-param-type-limit": "Tipo: enteiro ou <kbd>max</kbd>",
        "api-help-param-type-integer": "Tipo: {{PLURAL:$1|1=enteiro|2=lista de enteiros}}",
        "api-help-param-type-boolean": "Tipo: booleano ([[Special:ApiHelp/main#main/datatypes|detalles]])",
        "api-help-param-type-timestamp": "Tipo: {{PLURAL:$1|1=selo de tempo|2=lista de selos de tempo}} ([[Special:ApiHelp/main#main/datatypes|formatos permitidos]])",
        "api-help-param-type-user": "Tipo: {{PLURAL:$1|1=nome de usuario|2=lista de nomes de usuarios}}",
-       "api-help-param-list": "{{PLURAL:$1|1=Un valor dos seguintes valores|2=Valores (separados con <kbd>{{!}}</kbd>)}}: $2",
+       "api-help-param-list": "{{PLURAL:$1|1=Un valor dos seguintes valores|2=Valores (separados con <kbd>{{!}}</kbd> ou [[Special:ApiHelp/main#main/datatypes|outros]])}}: $2",
        "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Debe ser baleiro|Pode ser baleiro, ou $2}}",
        "api-help-param-limit": "Non se permiten máis de $1.",
        "api-help-param-limit2": "Non se permiten máis de $1 ($2 para bots).",
        "api-help-param-integer-max": "{{PLURAL:$1|1=O valor debe ser menor |2=Os valores deben ser menores}} que $3.",
        "api-help-param-integer-minmax": "{{PLURAL:$1|1=O valor debe estar entre $2 e $3 |2=Os valores deben estar entre $2 e $3}}.",
        "api-help-param-upload": "Debe ser enviado como un ficheiro importado usando multipart/form-data.",
-       "api-help-param-multi-separate": "Separe os valores con <kbd>|</kbd>.",
+       "api-help-param-multi-separate": "Separe os valores con <kbd>|</kbd> ou [[Special:ApiHelp/main#main/datatypes|outros]].",
        "api-help-param-multi-max": "O número máximo de valores é {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} para os bots).",
        "api-help-param-default": "Por defecto: $1",
        "api-help-param-default-empty": "Por defecto: <span class=\"apihelp-empty\">(baleiro)</span>",
index a28cbda..670a8a5 100644 (file)
        "api-help-param-deprecated": "מיושן.",
        "api-help-param-required": "פרמטר זה נדרש.",
        "api-help-datatypes-header": "סוגי נתונים",
-       "api-help-datatypes": "×\97×\9cק ×\9eס×\95×\92×\99 ×\94פר×\9e×\98ר×\99×\9d ×\91×\91קש×\95ת API ×\93×\95רש×\99×\9d ×\94ס×\91ר × ×\95סף:\n;×\91×\95×\9c×\99×\90× ×\99 (boolean)\n:פר×\9e×\98ר×\99×\9d ×\91×\95×\9c×\99×\90× ×\99×\99×\9d ×¢×\95×\91×\93×\99×\9d ×\9b×\9e×\95 ×ª×\99×\91×\95ת ×¡×\99×\9e×\95×\9f ×©×\9c HTML: ×\90×\9d ×\94פר×\9e×\98ר ×¦×\95×\99×\9f, ×\91×\9c×\99 ×§×©×¨ ×\9cער×\9a ×©×\9c×\95, ×\94×\95×\90 ×\90×\9eת (true). ×\91ש×\91×\99×\9c ×¢×¨×\9a ×©×§×¨ (false), ×\99ש ×\9c×\94ש×\9e×\99×\98 ×\90ת ×\94פר×\9e×\98ר ×\9c×\92×\9eר×\99.\n;×\97×\95ת×\9dÖ¾×\96×\9e×\9f (timestamp)\n:×\90פשר ×\9c×\9bת×\95×\91 ×\97×\95ת×\9e×\99Ö¾×\96×\9e×\9f ×\91×\9eספר ×ª×¡×\93×\99ר×\99×\9d. ×ª×\90ר×\99×\9a ×\95שע×\94 ×\9cפ×\99 ISO 8601 ×\94×\95×\90 ×\94×\93×\91ר ×\94×\9e×\95×\9e×\9cת. ×\9b×\9c ×\94×\96×\9e× ×\99×\9d ×\9eצ×\95×\99× ×\99×\9d ×\91Ö¾ UTC, ×\9c×\90 ×ª×\94×\99×\94 ×\94שפע×\94 ×\9cש×\95×\9d ×\90×\96×\95ר ×\96×\9e×\9f ×©×\99צ×\95×\99×\9f.\n:* ×ª×\90ר×\99×\9a ×\95שע×\94 ×\9cפ×\99 ISO 8601â\80\8f, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (×\9c×\90 ×\97×\95×\91×\94 ×\9c×\9bת×\95×\91 ×¤×\99ס×\95ק ×\95Ö¾<kbd>Z</kbd>)\n:* ×ª×\90ר×\99×\9a ×\95שע×\94 ×\9cפ×\99 ISO 8601 ×¢×\9d ×\97×\9cק×\99 ×©× ×\99×\99×\94 (ש×\9c×\90 ×ª×\94×\99×\94 ×\9c×\94×\9d ×©×\95×\9d ×\94שפע×\94), <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (×\9c×\90 ×\97×\95×\91×\94 ×\9c×\9bת×\95×\91 ×§×\95×\95×\99×\9d ×\9eפר×\99×\93×\99×\9d, × ×§×\95×\93ת×\99×\99×\9d ×\95Ö¾<kbd>Z</kbd>)\n:* ×ª×¡×\93×\99ר MediaWikiâ\80\8f, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* ×ª×¡×\93×\99ר ×\9eספר×\99 ×\9b×\9c×\9c×\99, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (×\9c×\90×\96×\95ר ×\96×\9e×\9f ×\90×\95פצ×\99×\95× ×\9c×\99 ×©×\9c <kbd>GMT</kbd>â\80\8f, <kbd dir=\"ltr\">+<var>##</var></kbd>, ×\90×\95 <kbd dir=\"ltr\">-<var>##</var></kbd> ×\90×\99×\9f ×\94שפע×\94)\n:* ×ª×¡×\93×\99ר EXIFâ\80\8f, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* ×ª×¡×\93×\99ר RFC 2822 (×\90פשר ×\9c×\94ש×\9e×\99×\98 ×\90ת ×\90×\96×\95ר ×\94×\96×\9e×\9f), <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* ×ª×¡×\93×\99ר RFC 850 (×\90פשר ×\9c×\94ש×\9e×\99×\98 ×\90ת ×\90×\96×\95ר ×\94×\96×\9e×\9f), <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* ×ª×¡×\93×\99ר C ctimeâ\80\8f, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* ×©× ×\99×\95ת ×\9e×\90×\96 1970-01-01T00:00:00Z ×\91ת×\95ר ×\9eספר ×©×\9c×\9a ×\91×\99×\9f 1 ×\9cÖ¾13 (×\9c×\90 ×\9b×\95×\9c×\9c <kbd>0</kbd>)\n:* ×\94×\9e×\97ר×\95×\96ת <kbd>now</kbd>",
+       "api-help-datatypes": "ק×\9c×\98 ×\9c×\9e×\93×\99×\94Ö¾×\95×\99ק×\99 ×¦×¨×\99×\9a ×\9c×\94×\99×\95ת ×\91ק×\99×\93×\95×\93 UTF-8 ×\9e× ×\95ר×\9e×\9c ×\91Ö¾NFC. ×\9e×\93×\99×\94Ö¾×\95×\99ק×\99 ×\99×\9b×\95×\9c×\94 ×\9cנס×\95ת ×\9c×\94×\9e×\99ר ×§×\9c×\98 ×\90×\97ר, ×\90×\91×\9c ×\96×\94 ×¢×\9c×\95×\9c ×\9c×\92ר×\95×\9d ×\9cפע×\95×\9c×\95ת ×\9eס×\95×\99×\9e×\95ת (×\9b×\92×\95×\9f [[Special:ApiHelp/edit|ער×\99×\9b×\95ת]] ×¢×\9d ×\91×\93×\99ק×\95ת MD5) ×\9c×\94×\99×\9bש×\9c.\n\n×\97×\9cק ×\9eס×\95×\92×\99 ×\94פר×\9e×\98ר×\99×\9d ×\91×\91קש×\95ת API ×\93×\95רש×\99×\9d ×\94ס×\91ר × ×\95סף:\n;×\91×\95×\9c×\99×\90× ×\99 (boolean)\n:פר×\9e×\98ר×\99×\9d ×\91×\95×\9c×\99×\90× ×\99×\99×\9d ×¢×\95×\91×\93×\99×\9d ×\9b×\9e×\95 ×ª×\99×\91×\95ת ×¡×\99×\9e×\95×\9f ×©×\9c HTML: ×\90×\9d ×\94פר×\9e×\98ר ×¦×\95×\99×\9f, ×\91×\9c×\99 ×§×©×¨ ×\9cער×\9a ×©×\9c×\95, ×\94×\95×\90 ×\90×\9eת (true). ×\91ש×\91×\99×\9c ×¢×¨×\9a ×©×§×¨ (false), ×\99ש ×\9c×\94ש×\9e×\99×\98 ×\90ת ×\94פר×\9e×\98ר ×\9c×\92×\9eר×\99.\n;×\97×\95ת×\9dÖ¾×\96×\9e×\9f (timestamp)\n:×\90פשר ×\9c×\9bת×\95×\91 ×\97×\95ת×\9e×\99Ö¾×\96×\9e×\9f ×\91×\9eספר ×ª×¡×\93×\99ר×\99×\9d. ×ª×\90ר×\99×\9a ×\95שע×\94 ×\9cפ×\99 ISO 8601 ×\94×\95×\90 ×\94×\93×\91ר ×\94×\9e×\95×\9e×\9cת. ×\9b×\9c ×\94×\96×\9e× ×\99×\9d ×\9eצ×\95×\99× ×\99×\9d ×\91Ö¾ UTC, ×\9c×\90 ×ª×\94×\99×\94 ×\94שפע×\94 ×\9cש×\95×\9d ×\90×\96×\95ר ×\96×\9e×\9f ×©×\99צ×\95×\99×\9f.\n:* ×ª×\90ר×\99×\9a ×\95שע×\94 ×\9cפ×\99 ISO 8601â\80\8f, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (×\9c×\90 ×\97×\95×\91×\94 ×\9c×\9bת×\95×\91 ×¤×\99ס×\95ק ×\95Ö¾<kbd>Z</kbd>)\n:* ×ª×\90ר×\99×\9a ×\95שע×\94 ×\9cפ×\99 ISO 8601 ×¢×\9d ×\97×\9cק×\99 ×©× ×\99×\99×\94 (ש×\9c×\90 ×ª×\94×\99×\94 ×\9c×\94×\9d ×©×\95×\9d ×\94שפע×\94), <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd> (×\9c×\90 ×\97×\95×\91×\94 ×\9c×\9bת×\95×\91 ×§×\95×\95×\99×\9d ×\9eפר×\99×\93×\99×\9d, × ×§×\95×\93ת×\99×\99×\9d ×\95Ö¾<kbd>Z</kbd>)\n:* ×ª×¡×\93×\99ר MediaWikiâ\80\8f, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* ×ª×¡×\93×\99ר ×\9eספר×\99 ×\9b×\9c×\9c×\99, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (×\9c×\90×\96×\95ר ×\96×\9e×\9f ×\90×\95פצ×\99×\95× ×\9c×\99 ×©×\9c <kbd>GMT</kbd>â\80\8f, <kbd dir=\"ltr\">+<var>##</var></kbd>, ×\90×\95 <kbd dir=\"ltr\">-<var>##</var></kbd> ×\90×\99×\9f ×\94שפע×\94)\n:* ×ª×¡×\93×\99ר EXIFâ\80\8f, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* ×ª×¡×\93×\99ר RFC 2822 (×\90פשר ×\9c×\94ש×\9e×\99×\98 ×\90ת ×\90×\96×\95ר ×\94×\96×\9e×\9f), <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* ×ª×¡×\93×\99ר RFC 850 (×\90פשר ×\9c×\94ש×\9e×\99×\98 ×\90ת ×\90×\96×\95ר ×\94×\96×\9e×\9f), <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* ×ª×¡×\93×\99ר C ctimeâ\80\8f, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* ×©× ×\99×\95ת ×\9e×\90×\96 1970-01-01T00:00:00Z ×\91ת×\95ר ×\9eספר ×©×\9c×\9a ×\91×\99×\9f 1 ×\9cÖ¾13 (×\9c×\90 ×\9b×\95×\9c×\9c <kbd>0</kbd>)\n:* ×\94×\9e×\97ר×\95×\96ת <kbd>now</kbd>\n;×\9eפר×\99×\93 ×¢×¨×\9b×\99×\9d ×\9eר×\95×\91×\99×\9d ×\97×\9c×\95פ×\99\n:פר×\9e×\98ר×\99×\9d ×©×\9c×\95ק×\97×\99×\9d ×¢×¨×\9b×\99×\9d ×\9eר×\95×\91×\99×\9d ×\91×\93ר×\9aÖ¾×\9b×\9c×\9c × ×©×\9c×\97×\99×\9d ×¢×\9d ×\94ער×\9b×\99×\9d ×\9e×\95פר×\93×\99×\9d ×\91×\90×\9eצע×\95ת ×ª×\95 ×\9eק×\9c, ×\9c×\9eש×\9c <kbd>param=value1|value2</kbd> ×\90×\95 <kbd>param=value1%7Cvalue2</kbd>. ×\90×\9d ×\94ער×\9a ×¦×¨×\99×\9a ×\9c×\94×\9b×\99×\9c ×\90ת ×ª×\95 ×\94×\9eק×\9c, ×\99ש ×\9c×\94שת×\9eש ×\91Ö¾U+001F (×\9eפר×\99×\93 ×\99×\97×\99×\93×\95ת) ×\91ת×\95ר ×\94×\9eפר×\99×\93 ''×\95×\92×\9d'' ×\9c×\94×\95ס×\99×£ ×\9cת×\97×\99×\9cת ×\94ער×\9a U+001F, ×\9c×\9eש×\9c <kbd>param=%1Fvalue1%1Fvalue2</kbd>.",
        "api-help-param-type-limit": "סוג: מספר שלם או <kbd>max</kbd>",
        "api-help-param-type-integer": "סוג: {{PLURAL:$1|1=מספר שלם|2=רשימת מספרים שלמים}}",
        "api-help-param-type-boolean": "סוג: בוליאני ([[Special:ApiHelp/main#main/datatypes|פרטים]])",
        "api-help-param-type-timestamp": "סוג: {{PLURAL:$1|חותם־זמן|רשימת חותמי־זמן}} ([[Special:ApiHelp/main#main/datatypes|תסדירים מורשים]])",
        "api-help-param-type-user": "סוג: {{PLURAL:$1|1=שם משתמש|2=רשימת שמות משתמשים}}",
-       "api-help-param-list": "{{PLURAL:$1|1=אחד מהערכים הבאים|2=ערכים (מופרדים באמצעות \"<kbd>{{!}}</kbd>\")}}: $2",
+       "api-help-param-list": "{{PLURAL:$1|1=אחד מהערכים הבאים|2=ערכים (מופרדים באמצעות \"<kbd>{{!}}</kbd>\" או or [[Special:ApiHelp/main#main/datatypes|תו חלופי]])}}: $2",
        "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=חייב להיות ריק|יכול להיות ריק או $2}}",
        "api-help-param-limit": "מספר הפרמטרים לא יכול להיות גדול מ־$1.",
        "api-help-param-limit2": "המספר המרבי המותר הוא $1 (עבור בוטים – $2).",
        "api-help-param-integer-max": "ה{{PLURAL:$1|1=ערך לא יכול להיות גדול|2=ערכים לא יכולים להיות גדולים}} מ־$3.",
        "api-help-param-integer-minmax": "ה{{PLURAL:$1|1=ערך חייב|2=ערכים חייבים}} להיות בין $2 ל־$3.",
        "api-help-param-upload": "חייב להישלח (posted) בתור העלאת קובץ באמצעות multipart/form-data.",
-       "api-help-param-multi-separate": "הפרדה בין ערכים נעשית באמצעות <kbd>|</kbd>",
+       "api-help-param-multi-separate": "הפרדה בין ערכים נעשית באמצעות <kbd>|</kbd> או [[Special:ApiHelp/main#main/datatypes|תו חלופי]].",
        "api-help-param-multi-max": "מספר הערכים המרבי הוא {{PLURAL:$1|$1}} (עבור בוטים – {{PLURAL:$2|$2}}).",
        "api-help-param-default": "ברירת מחדל: $1",
        "api-help-param-default-empty": "ברירת מחדל: <span class=\"apihelp-empty\">(ריק)</span>",
index e02abe0..d053593 100644 (file)
        "apihelp-linkaccount-example-link": "Avvia il processo di collegamento ad un'utenza da <kbd>Example</kbd>.",
        "apihelp-login-description": "Accedi e ottieni i cookie di autenticazione.\n\nQuesta azione deve essere usata esclusivamente in combinazione con [[Special:BotPasswords]]; utilizzarla per l'accesso all'account principale è deprecato e può fallire senza preavviso. Per accedere in modo sicuro all'utenza principale, usa <kbd>[[Special:ApiHelp/clientlogin|action=clientlogin]]</kbd>.",
        "apihelp-login-description-nobotpasswords": "Accedi e ottieni i cookies di autenticazione.\n\nQuesta azione è deprecata e può fallire senza preavviso. Per accedere in modo sicuro, usa [[Special:ApiHelp/clientlogin|action=clientlogin]].",
-       "apihelp-login-description-nonauthmanager": "Accedi e ottieni i cookie di autenticazione.\n\nIn caso di accesso riuscito, i cookies necessari saranno inclusi nella intestazioni di risposta HTTP. In caso di accesso fallito, ulteriori tentativi potrebbero essere limitati, in modo da contenere gli attacchi automatizzati per indovinare le password.",
        "apihelp-login-param-name": "Nome utente.",
        "apihelp-login-param-password": "Password.",
        "apihelp-login-param-domain": "Dominio (opzionale).",
        "api-help-param-type-boolean": "Tipo: booleano ([[Special:ApiHelp/main#main/datatypes|dettagli]])",
        "api-help-param-type-timestamp": "Tipo: {{PLURAL:$1|1=timestamp|2=elenco di timestamp}} ([[Special:ApiHelp/main#main/datatypes|formati consentiti]])",
        "api-help-param-type-user": "Tipo: {{PLURAL:$1|1=nome utente|2=elenco di nomi utente}}",
-       "api-help-param-list": "{{PLURAL:$1|1=Uno dei seguenti valori|2=Valori (separati da <kbd>{{!}}</kbd>)}}: $2",
+       "api-help-param-list": "{{PLURAL:$1|1=Uno dei seguenti valori|2=Valori (separati da <kbd>{{!}}</kbd> o [[Special:ApiHelp/main#main/datatypes|alternativa]])}}: $2",
        "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Deve essere vuoto|Può essere vuoto, o $2}}",
        "api-help-param-limit": "Non più di $1 consentito.",
        "api-help-param-limit2": "Non più di $1 ($2 per bot) consentito.",
        "api-help-param-integer-min": "{{PLURAL:$1|1=Il valore non deve essere inferiore|2=I valori non devono essere inferiori}} a $2.",
        "api-help-param-integer-max": "{{PLURAL:$1|1=Il valore non deve essere superiore|2=I valori non devono essere superiori}} a $3.",
        "api-help-param-integer-minmax": "{{PLURAL:$1|1=Il valore deve essere compreso|2=I valori devono essere compresi}} tra $2 e $3.",
-       "api-help-param-multi-separate": "Separa i valori con <kbd>|</kbd>.",
+       "api-help-param-multi-separate": "Separa i valori con <kbd>|</kbd> o [[Special:ApiHelp/main#main/datatypes|alternativa]].",
        "api-help-param-multi-max": "Il numero massimo di valori è {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} per i bot).",
        "api-help-param-default": "Predefinito: $1",
        "api-help-param-default-empty": "Predefinito: <span class=\"apihelp-empty\">(vuoto)</span>",
index af13948..6d2dbf0 100644 (file)
@@ -43,6 +43,7 @@
        "apihelp-checktoken-example-simple": "<kbd>csrf</kbd> トークンの妥当性を調べる。",
        "apihelp-clearhasmsg-description": "現在の利用者の <code>hasmsg</code> フラグを消去します。",
        "apihelp-clearhasmsg-example-1": "現在の利用者の <code>hasmsg</code> フラグを消去する。",
+       "apihelp-clientlogin-example-login": "利用者 <kbd>Example</kbd> としてのログイン処理をパスワード <kbd>ExamplePassword</kbd> で開始する",
        "apihelp-compare-description": "2つの版間の差分を取得します。\n\n\"from\" と \"to\" の両方の版番号、ページ名、もしくはページIDを渡す必要があります。",
        "apihelp-compare-param-fromtitle": "比較する1つ目のページ名。",
        "apihelp-compare-param-fromid": "比較する1つ目のページID。",
        "apihelp-query+backlinks-param-pageid": "検索するページID。<var>$1title</var>とは同時に使用できません。",
        "apihelp-query+backlinks-param-namespace": "列挙する名前空間。",
        "apihelp-query+backlinks-param-dir": "一覧表示する方向。",
+       "apihelp-query+backlinks-param-limit": "返すページの総数。<var>$1redirect</var> を有効化した場合は、各レベルに対し個別にlimitが適用されます (つまり、最大で 2 * <var>$1limit</var> 件の結果が返されます)。",
        "apihelp-query+backlinks-example-simple": "<kbd>Main page</kbd> へのリンクを表示する。",
        "apihelp-query+backlinks-example-generator": "<kbd>Main page</kbd> にリンクしているページの情報を取得する。",
        "apihelp-query+blocks-description": "ブロックされた利用者とIPアドレスを一覧表示します。",
index e285130..dabc560 100644 (file)
@@ -89,6 +89,7 @@
        "api-help-license-unknown": "Licéncia : <span class=\"apihelp-unknown\">desconeguda</span>",
        "api-help-parameters": "{{PLURAL:$1|Paramètre|Paramètres}} :",
        "api-help-param-deprecated": "Obsolèt.",
+       "api-help-param-required": "Aqueste paramètre es obligatòri.",
        "api-help-datatypes-header": "Tipe de donadas",
        "api-help-param-default": "Per defaut : $1",
        "api-credits-header": "Mercejaments"
index 860b7a7..adda3c4 100644 (file)
        "api-help-param-deprecated": "已弃用。",
        "api-help-param-required": "这个参数是必须的。",
        "api-help-datatypes-header": "数据类型",
-       "api-help-datatypes": "一些在API请求中的参数类型需要更进一步解释:\n;boolean\n:布尔参数就像HTML复选框一样工作:如果指定参数,无论何值都被认为是真。如果要假值,则可完全忽略参数。\n;timestamp\n:时间戳可被指定为很多格式。推荐使用ISO 8601日期和时间标准。所有时间为UTC时间,包含的任何时区会被忽略。\n:* ISO 8601日期和时间,<kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd>(标点和<kbd>Z</kbd>是可选项)\n:* 带小数秒(会被忽略)的ISO 8601日期和时间,<kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd>(破折号、括号和<kbd>Z</kbd>是可选的)\n:* MediaWiki格式,<kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* 一般数字格式,<kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>(<kbd>GMT</kbd>、<kbd>+<var>##</var></kbd>或<kbd>-<var>##</var></kbd>的可选时区会被忽略)\n:* EXIF格式,<kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 2822格式(时区可能会被省略),<kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 850格式(时区可能会被省略),<kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* C ctime格式,<kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* 秒数是从1970-01-01T00:00:00Z开始,作为1到13位数的整数(除了<kbd>0</kbd>)\n:* 字符串<kbd>now</kbd>",
+       "api-help-datatypes": "至MediaWiki的输入应为NFC标准化的UTF-8。MediaWiki可以尝试转换其他输入,但这可能导致一些操作失败(例如[[Special:ApiHelp/edit|edits]]与MD5校验)。\n\n一些在API请求中的参数类型需要更进一步解释:\n;boolean\n:布尔参数就像HTML复选框一样工作:如果指定参数,无论何值都被认为是真。如果要假值,则可完全忽略参数。\n;timestamp\n:时间戳可被指定为很多格式。推荐使用ISO 8601日期和时间标准。所有时间为UTC时间,包含的任何时区会被忽略。\n:* ISO 8601日期和时间,<kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd>(标点和<kbd>Z</kbd>是可选项)\n:* 带小数秒(会被忽略)的ISO 8601日期和时间,<kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>.<var>00001</var>Z</kbd>(破折号、括号和<kbd>Z</kbd>是可选的)\n:* MediaWiki格式,<kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* 一般数字格式,<kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>(<kbd>GMT</kbd>、<kbd>+<var>##</var></kbd>或<kbd>-<var>##</var></kbd>的可选时区会被忽略)\n:* EXIF格式,<kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 2822格式(时区可能会被省略),<kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* RFC 850格式(时区可能会被省略),<kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* C ctime格式,<kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* 秒数是从1970-01-01T00:00:00Z开始,作为1到13位数的整数(除了<kbd>0</kbd>)\n:* 字符串<kbd>now</kbd>\n;替代多值分隔符\n:使用多个值的参数通常会与管道符号分隔的值一起提交,例如<kbd>param=value1|value2</kbd>或<kbd>param=value1%7Cvalue2</kbd>。如果值必须包含管道符号,使用U+001F(单位分隔符)作为分隔符,''并''在值前加前缀U+001F,例如<kbd>param=%1Fvalue1%1Fvalue2</kbd>。",
        "api-help-param-type-limit": "类型:整数或<kbd>max</kbd>",
        "api-help-param-type-integer": "类型:{{PLURAL:$1|1=整数|2=整数列表}}",
        "api-help-param-type-boolean": "类型:布尔值([[Special:ApiHelp/main#main/datatypes|详细信息]])",
        "api-help-param-type-timestamp": "类型:{{PLURAL:$1|1=时间戳|2=时间戳列表}}([[Special:ApiHelp/main#main/datatypes|允许格式]])",
        "api-help-param-type-user": "类型:{{PLURAL:$1|1=用户名|2=用户名列表}}",
-       "api-help-param-list": "{{PLURAL:$1|1=以下值中的一个|2=值(以<kbd>{{!}}</kbd>分隔)}}:$2",
+       "api-help-param-list": "{{PLURAL:$1|1=以下值中的一个|2=值(以<kbd>{{!}}</kbd>或[[Special:ApiHelp/main#main/datatypes|替代物]]分隔)}}:$2",
        "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=必须为空|可以为空,或$2}}",
        "api-help-param-limit": "不允许超过$1。",
        "api-help-param-limit2": "不允许超过$1个(对于机器人则是$2个)。",
        "api-help-param-integer-max": "{{PLURAL:$1|值}}必须不大于$3。",
        "api-help-param-integer-minmax": "{{PLURAL:$1|值}}必须介于$2和$3之间。",
        "api-help-param-upload": "必须被公布为使用multipart/form-data的一次文件上传。",
-       "api-help-param-multi-separate": "通过“<kbd>|</kbd>”隔开各值。",
+       "api-help-param-multi-separate": "通过<kbd>|</kbd>或[[Special:ApiHelp/main#main/datatypes|替代物]]隔开各值。",
        "api-help-param-multi-max": "值的最高数字是{{PLURAL:$1|$1}}(对于机器人则是{{PLURAL:$2|$2}})。",
        "api-help-param-default": "默认:$1",
        "api-help-param-default-empty": "默认:<span class=\"apihelp-empty\">(空)</span>",
index acdc01b..a5c86be 100644 (file)
@@ -37,8 +37,46 @@ use WebRequest;
  * In the future, it may also serve as the entry point to the authorization
  * system.
  *
+ * If you are looking at this because you are working on an extension that creates its own
+ * login or signup page, then 1) you really shouldn't do that, 2) if you feel you absolutely
+ * have to, subclass AuthManagerSpecialPage or build it on the client side using the clientlogin
+ * or the createaccount API. Trying to call this class directly will very likely end up in
+ * security vulnerabilities or broken UX in edge cases.
+ *
+ * If you are working on an extension that needs to integrate with the authentication system
+ * (e.g. by providing a new login method, or doing extra permission checks), you'll probably
+ * need to write an AuthenticationProvider.
+ *
+ * If you want to create a "reserved" user programmatically, User::newSystemUser() might be what
+ * you are looking for. If you want to change user data, use User::changeAuthenticationData().
+ * Code that is related to some SessionProvider or PrimaryAuthenticationProvider can
+ * create a (non-reserved) user by calling AuthManager::autoCreateUser(); it is then the provider's
+ * responsibility to ensure that the user can authenticate somehow (see especially
+ * PrimaryAuthenticationProvider::autoCreatedAccount()).
+ * If you are writing code that is not associated with such a provider and needs to create accounts
+ * programmatically for real users, you should rethink your architecture. There is no good way to
+ * do that as such code has no knowledge of what authentication methods are enabled on the wiki and
+ * cannot provide any means for users to access the accounts it would create.
+ *
+ * The two main control flows when using this class are as follows:
+ * * Login, user creation or account linking code will call getAuthenticationRequests(), populate
+ *   the requests with data (by using them to build a HTMLForm and have the user fill it, or by
+ *   exposing a form specification via the API, so that the client can build it), and pass them to
+ *   the appropriate begin* method. That will return either a success/failure response, or more
+ *   requests to fill (either by building a form or by redirecting the user to some external
+ *   provider which will send the data back), in which case they need to be submitted to the
+ *   appropriate continue* method and that step has to be repeated until the response is a success
+ *   or failure response. AuthManager will use the session to maintain internal state during the
+ *   process.
+ * * Code doing an authentication data change will call getAuthenticationRequests(), select
+ *   a single request, populate it, and pass it to allowsAuthenticationDataChange() and then
+ *   changeAuthenticationData(). If the data change is user-initiated, the whole process needs
+ *   to be preceded by a call to securitySensitiveOperationStatus() and aborted if that returns
+ *   a non-OK status.
+ *
  * @ingroup Auth
  * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
  */
 class AuthManager implements LoggerAwareInterface {
        /** Log in with an existing (not necessarily local) user */
@@ -737,7 +775,10 @@ class AuthManager implements LoggerAwareInterface {
        /**
         * Determine whether a username can authenticate
         *
-        * @param string $username
+        * This is mainly for internal purposes and only takes authentication data into account,
+        * not things like blocks that can change without the authentication system being aware.
+        *
+        * @param string $username MediaWiki username
         * @return bool
         */
        public function userCanAuthenticate( $username ) {
@@ -832,6 +873,9 @@ class AuthManager implements LoggerAwareInterface {
         * If $req was returned for AuthManager::ACTION_REMOVE, using $req should
         * no longer result in a successful login.
         *
+        * This method should only be called if allowsAuthenticationDataChange( $req, true )
+        * returned success.
+        *
         * @param AuthenticationRequest $req
         */
        public function changeAuthenticationData( AuthenticationRequest $req ) {
@@ -871,7 +915,7 @@ class AuthManager implements LoggerAwareInterface {
 
        /**
         * Determine whether a particular account can be created
-        * @param string $username
+        * @param string $username MediaWiki username
         * @param array $options
         *  - flags: (int) Bitfield of User:READ_* constants, default User::READ_NORMAL
         *  - creating: (bool) For internal use only. Never specify this.
@@ -1474,6 +1518,13 @@ class AuthManager implements LoggerAwareInterface {
 
        /**
         * Auto-create an account, and log into that account
+        *
+        * PrimaryAuthenticationProviders can invoke this method by returning a PASS from
+        * beginPrimaryAuthentication/continuePrimaryAuthentication with the username of a
+        * non-existing user. SessionProviders can invoke it by returning a SessionInfo with
+        * the username of a non-existing user from provideSessionInfo(). Calling this method
+        * explicitly (e.g. from a maintenance script) is also fine.
+        *
         * @param User $user User to auto-create
         * @param string $source What caused the auto-creation? This must be the ID
         *  of a PrimaryAuthenticationProvider or the constant self::AUTOCREATE_SOURCE_SESSION.
@@ -2310,6 +2361,7 @@ class AuthManager implements LoggerAwareInterface {
        }
 
        /**
+        * Log the user in
         * @param User $user
         * @param bool|null $remember
         */
@@ -2374,6 +2426,7 @@ class AuthManager implements LoggerAwareInterface {
 
        /**
         * Reset the internal caching for unit testing
+        * @protected Unit tests only
         */
        public static function resetCache() {
                if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
index 4db0a84..11f3e22 100644 (file)
@@ -28,6 +28,11 @@ use Psr\Log\LoggerAwareInterface;
 
 /**
  * An AuthenticationProvider is used by AuthManager when authenticating users.
+ *
+ * This interface should not be implemented directly; use one of its children.
+ *
+ * Authentication providers can be registered via $wgAuthManagerAutoConfig.
+ *
  * @ingroup Auth
  * @since 1.27
  */
@@ -83,9 +88,9 @@ interface AuthenticationProvider extends LoggerAwareInterface {
         *    - ACTION_LINK: The local user being linked to.
         *    - ACTION_CHANGE: The user having data changed.
         *    - ACTION_REMOVE: The user having data removed.
-        *    This does not need to be copied into the returned requests, you only
-        *    need to pay attention to it if the set of requests differs based on
-        *    the user.
+        *    If you leave the username property of the returned requests empty, this
+        *    will automatically be copied there (except for ACTION_CREATE where it
+        *    wouldn't really make sense).
         * @return AuthenticationRequest[]
         */
        public function getAuthenticationRequests( $action, array $options );
index 2474b8b..ff4569b 100644 (file)
@@ -29,7 +29,7 @@ use Message;
  * This is a value object for authentication requests.
  *
  * An AuthenticationRequest represents a set of form fields that are needed on
- * and provided from the login, account creation, or password change forms.
+ * and provided from a login, account creation, password change or similar form.
  *
  * @ingroup Auth
  * @since 1.27
@@ -39,11 +39,14 @@ abstract class AuthenticationRequest {
        /** Indicates that the request is not required for authentication to proceed. */
        const OPTIONAL = 0;
 
-       /** Indicates that the request is required for authentication to proceed. */
+       /** Indicates that the request is required for authentication to proceed.
+        * This will only be used for UI purposes; it is the authentication providers'
+        * responsibility to verify that all required requests are present.
+        */
        const REQUIRED = 1;
 
        /** Indicates that the request is required by a primary authentication
-        * provdier. Since the user can choose which primary to authenticate with,
+        * provider. Since the user can choose which primary to authenticate with,
         * the request might or might not end up being actually required. */
        const PRIMARY_REQUIRED = 2;
 
@@ -60,7 +63,8 @@ abstract class AuthenticationRequest {
        /** @var string|null Return-to URL, in case of redirect */
        public $returnToUrl = null;
 
-       /** @var string|null Username. May not be used by all subclasses. */
+       /** @var string|null Username. See AuthenticationProvider::getAuthenticationRequests()
+        * for details of what this means and how it behaves. */
        public $username = null;
 
        /**
@@ -105,6 +109,11 @@ abstract class AuthenticationRequest {
         *  - sensitive: (bool) If set and truthy, the field is considered sensitive. Code using the
         *      request should avoid exposing the value of the field.
         *
+        * All AuthenticationRequests are populated from the same data, so most of the time you'll
+        * want to prefix fields names with something unique to the extension/provider (although
+        * in some cases sharing the field with other requests is the right thing to do, e.g. for
+        * a 'password' field).
+        *
         * @return array As above
         */
        abstract public function getFieldInfo();
@@ -126,10 +135,13 @@ abstract class AuthenticationRequest {
        /**
         * Initialize form submitted form data.
         *
-        * Should always return false if self::getFieldInfo() returns an empty
-        * array.
+        * The default behavior is to to check for each key of self::getFieldInfo()
+        * in the submitted data, and copy the value - after type-appropriate transformations -
+        * to $this->$key. Most subclasses won't need to override this; if you do override it,
+        * make sure to always return false if self::getFieldInfo() returns an empty array.
         *
-        * @param array $data Submitted data as an associative array
+        * @param array $data Submitted data as an associative array (keys will correspond
+        *   to getFieldInfo())
         * @return bool Whether the request data was successfully loaded
         */
        public function loadFromSubmission( array $data ) {
@@ -250,7 +262,7 @@ abstract class AuthenticationRequest {
         *
         * Only considers requests that have a "username" field.
         *
-        * @param AuthenticationRequest[] $requests
+        * @param AuthenticationRequest[] $reqs
         * @return string|null
         * @throws \UnexpectedValueException If multiple different usernames are present.
         */
index 5048cf8..0339e45 100644 (file)
@@ -27,6 +27,10 @@ use Message;
 
 /**
  * This is a value object to hold authentication response data
+ *
+ * An AuthenticationResponse represents both the status of the authentication
+ * (success, failure, in progress) and it its state (what data is needed to continue).
+ *
  * @ingroup Auth
  * @since 1.27
  */
@@ -39,7 +43,8 @@ class AuthenticationResponse {
 
        /** Indicates that third-party authentication succeeded but no user exists.
         * Either treat this like a UI response or pass $this->createRequest to
-        * AuthManager::beginCreateAccount().
+        * AuthManager::beginCreateAccount(). For use by AuthManager only (providers
+        * should just return a PASS with no username).
         */
        const RESTART = 'RESTART';
 
@@ -67,7 +72,9 @@ class AuthenticationResponse {
 
        /**
         * @var AuthenticationRequest[] Needed AuthenticationRequests to continue
-        * after a UI or REDIRECT response
+        * after a UI or REDIRECT response. This plays the same role when continuing
+        * authentication as AuthManager::getAuthenticationRequests() does when
+        * beginning it.
         */
        public $neededRequests = [];
 
@@ -84,35 +91,42 @@ class AuthenticationResponse {
         * @var AuthenticationRequest|null
         *
         * Returned with a PrimaryAuthenticationProvider login FAIL or a PASS with
-        * no username, this holds a request that should result in a PASS when
+        * no username, this can be set to a request that should result in a PASS when
         * passed to that provider's PrimaryAuthenticationProvider::beginPrimaryAccountCreation().
+        * The client will be able to send that back for expedited account creation where only
+        * the username needs to be filled.
         *
         * Returned with an AuthManager login FAIL or RESTART, this holds a
         * CreateFromLoginAuthenticationRequest that may be passed to
         * AuthManager::beginCreateAccount(), possibly in place of any
         * "primary-required" requests. It may also be passed to
-        * AuthManager::beginAuthentication() to preserve state.
+        * AuthManager::beginAuthentication() to preserve the list of
+        * accounts which can be linked after success (see $linkRequest).
         */
        public $createRequest = null;
 
        /**
-        * @var AuthenticationRequest|null Returned with a PrimaryAuthenticationProvider
-        *  login PASS with no username, this holds a request to pass to
-        *  AuthManager::changeAuthenticationData() to link the account once the
-        *  local user has been determined.
+        * @var AuthenticationRequest|null When returned with a PrimaryAuthenticationProvider
+        *  login PASS with no username, the request this holds will be passed to
+        *  AuthManager::changeAuthenticationData() once the local user has been determined and the
+        *  user has confirmed the account ownership (by reviewing the information given by
+        *  $linkRequest->describeCredentials()). The provider should handle that
+        *  changeAuthenticationData() call by doing the actual linking.
         */
        public $linkRequest = null;
 
        /**
         * @var AuthenticationRequest|null Returned with an AuthManager account
         *  creation PASS, this holds a request to pass to AuthManager::beginAuthentication()
-        *  to immediately log into the created account.
+        *  to immediately log into the created account. All provider methods except
+        *   postAuthentication will be skipped.
         */
        public $loginRequest = null;
 
        /**
         * @param string|null $username Local username
         * @return AuthenticationResponse
+        * @see AuthenticationResponse::PASS
         */
        public static function newPass( $username = null ) {
                $ret = new AuthenticationResponse;
@@ -124,6 +138,7 @@ class AuthenticationResponse {
        /**
         * @param Message $msg
         * @return AuthenticationResponse
+        * @see AuthenticationResponse::FAIL
         */
        public static function newFail( Message $msg ) {
                $ret = new AuthenticationResponse;
@@ -135,6 +150,7 @@ class AuthenticationResponse {
        /**
         * @param Message $msg
         * @return AuthenticationResponse
+        * @see AuthenticationResponse::RESTART
         */
        public static function newRestart( Message $msg ) {
                $ret = new AuthenticationResponse;
@@ -145,6 +161,7 @@ class AuthenticationResponse {
 
        /**
         * @return AuthenticationResponse
+        * @see AuthenticationResponse::ABSTAIN
         */
        public static function newAbstain() {
                $ret = new AuthenticationResponse;
@@ -156,6 +173,7 @@ class AuthenticationResponse {
         * @param AuthenticationRequest[] $reqs AuthenticationRequests needed to continue
         * @param Message $msg
         * @return AuthenticationResponse
+        * @see AuthenticationResponse::UI
         */
        public static function newUI( array $reqs, Message $msg ) {
                if ( !$reqs ) {
@@ -174,6 +192,7 @@ class AuthenticationResponse {
         * @param string $redirectTarget URL
         * @param mixed $redirectApiData Data suitable for adding to an ApiResult
         * @return AuthenticationResponse
+        * @see AuthenticationResponse::REDIRECT
         */
        public static function newRedirect( array $reqs, $redirectTarget, $redirectApiData = null ) {
                if ( !$reqs ) {
index 13fae6e..8590cbd 100644 (file)
@@ -27,16 +27,19 @@ use StatusValue;
 use User;
 
 /**
- * A pre-authentication provider is a check that must pass for authentication
- * to proceed.
+ * A pre-authentication provider can prevent authentication early on.
  *
  * A PreAuthenticationProvider is used to supply arbitrary checks to be
  * performed before the PrimaryAuthenticationProviders are consulted during the
- * login process. Possible uses include checking that a per-IP throttle has not
- * been reached or that a captcha has been solved.
+ * login / account creation / account linking process. Possible uses include
+ * checking that a per-IP throttle has not been reached or that a captcha has been solved.
+ *
+ * This interface also provides callbacks that are invoked after login / account creation
+ * / account linking succeeded or failed.
  *
  * @ingroup Auth
  * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
  */
 interface PreAuthenticationProvider extends AuthenticationProvider {
 
@@ -52,10 +55,17 @@ interface PreAuthenticationProvider extends AuthenticationProvider {
 
        /**
         * Post-login callback
+        *
+        * This will be called at the end of a login attempt. It will not be called for unfinished
+        * login attempts that fail by the session timing out.
+        *
+        * @note Under certain circumstances, this can be called even when testForAuthentication
+        *   was not; see AuthenticationRequest::$loginRequest.
         * @param User|null $user User that was attempted to be logged in, if known.
         *   This may become a "UserValue" in the future, or User may be refactored
         *   into such.
         * @param AuthenticationResponse $response Authentication response that will be returned
+        *   (PASS or FAIL)
         */
        public function postAuthentication( $user, AuthenticationResponse $response );
 
@@ -97,12 +107,18 @@ interface PreAuthenticationProvider extends AuthenticationProvider {
 
        /**
         * Post-creation callback
+        *
+        * This will be called at the end of an account creation attempt. It will not be called if
+        * the account creation process results in a session timeout (possibly after a successful
+        * user creation, while a secondary provider is waiting for a response).
+        *
         * @param User $user User that was attempted to be created.
         *   This may become a "UserValue" in the future, or User may be refactored
         *   into such.
         * @param User $creator User doing the creation. This may become a
         *   "UserValue" in the future, or User may be refactored into such.
         * @param AuthenticationResponse $response Authentication response that will be returned
+        *   (PASS or FAIL)
         */
        public function postAccountCreation( $user, $creator, AuthenticationResponse $response );
 
@@ -118,10 +134,14 @@ interface PreAuthenticationProvider extends AuthenticationProvider {
 
        /**
         * Post-link callback
+        *
+        * This will be called at the end of an account linking attempt.
+        *
         * @param User $user User that was attempted to be linked.
         *   This may become a "UserValue" in the future, or User may be refactored
         *   into such.
         * @param AuthenticationResponse $response Authentication response that will be returned
+        *   (PASS or FAIL)
         */
        public function postAccountLink( $user, AuthenticationResponse $response );
 
index 35f3287..4033613 100644 (file)
@@ -27,27 +27,50 @@ use StatusValue;
 use User;
 
 /**
- * A primary authentication provider determines which user is trying to log in.
+ * A primary authentication provider is responsible for associating the submitted
+ * authentication data with a MediaWiki account.
  *
- * A PrimaryAuthenticationProvider is used as part of presenting a login form
- * to authenticate a user. In particular, the PrimaryAuthenticationProvider
- * takes form data and determines the authenticated user (if any) corresponds
- * to that form data. It might do this on the basis of a username and password
- * in that data, or by interacting with an external authentication service
- * (e.g. using OpenID), or by some other mechanism.
+ * When multiple primary authentication providers are configured for a site, they
+ * act as alternatives; the first one that recognizes the data will handle it,
+ * and further primary providers are not called (although they all get a chance
+ * to prevent actions).
  *
- * A PrimaryAuthenticationProvider would not be appropriate for something like
+ * For login, the PrimaryAuthenticationProvider takes form data and determines
+ * which authenticated user (if any) corresponds to that form data. It might
+ * do this on the basis of a username and password in that data, or by
+ * interacting with an external authentication service (e.g. using OpenID),
+ * or by some other mechanism.
+ *
+ * (A PrimaryAuthenticationProvider would not be appropriate for something like
  * HTTP authentication, OAuth, or SSL client certificates where each HTTP
  * request contains all the information needed to identify the user. In that
- * case you'll want to be looking at a \\MediaWiki\\Session\\SessionProvider
- * instead.
+ * case you'll want to be looking at a \MediaWiki\Session\SessionProvider
+ * instead.)
+ *
+ * For account creation, the PrimaryAuthenticationProvider takes form data and
+ * stores some authentication details which will allow it to verify a login by
+ * that user in the future. This might for example involve saving it in the
+ * database in a table that can be joined to the user table, or sending it to
+ * some external service for account creation, or authenticating the user with
+ * some remote service and then recording that the remote identity is linked to
+ * the local account.
+ * The creation of the local user (i.e. calling User::addToDatabase()) is handled
+ * by AuthManager once the primary authentication provider returns a PASS
+ * from begin/continueAccountCreation; do not try to do it yourself.
+ *
+ * For account linking, the PrimaryAuthenticationProvider verifies the user's
+ * identity at some external service (typically by redirecting the user and
+ * asking the external service to verify) and then records which local account
+ * is linked to which remote accounts. It should keep track of this and be able
+ * to enumerate linked accounts via getAuthenticationRequests(ACTION_REMOVE).
  *
  * This interface also provides methods for changing authentication data such
- * as passwords and for creating new users who can later be authenticated with
- * this provider.
+ * as passwords, and callbacks that are invoked after login / account creation
+ * / account linking succeeded or failed.
  *
  * @ingroup Auth
  * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
  */
 interface PrimaryAuthenticationProvider extends AuthenticationProvider {
        /** Provider can create accounts */
@@ -93,16 +116,25 @@ interface PrimaryAuthenticationProvider extends AuthenticationProvider {
 
        /**
         * Post-login callback
+        *
+        * This will be called at the end of any login attempt, regardless of whether this provider was
+        * the one that handled it. It will not be called for unfinished login attempts that fail by
+        * the session timing out.
+        *
         * @param User|null $user User that was attempted to be logged in, if known.
         *   This may become a "UserValue" in the future, or User may be refactored
         *   into such.
         * @param AuthenticationResponse $response Authentication response that will be returned
+        *   (PASS or FAIL)
         */
        public function postAuthentication( $user, AuthenticationResponse $response );
 
        /**
         * Test whether the named user exists
-        * @param string $username
+        *
+        * Single-sign-on providers can use this to reserve a username for autocreation.
+        *
+        * @param string $username MediaWiki username
         * @param int $flags Bitfield of User:READ_* constants
         * @return bool
         */
@@ -110,7 +142,11 @@ interface PrimaryAuthenticationProvider extends AuthenticationProvider {
 
        /**
         * Test whether the named user can authenticate with this provider
-        * @param string $username
+        *
+        * Should return true if the provider has any data for this user which can be used to
+        * authenticate it, even if the user is temporarily prevented from authentication somehow.
+        *
+        * @param string $username MediaWiki username
         * @return bool
         */
        public function testUserCanAuthenticate( $username );
@@ -184,6 +220,10 @@ interface PrimaryAuthenticationProvider extends AuthenticationProvider {
         * If $req was returned for AuthManager::ACTION_REMOVE, the corresponding
         * credentials should no longer result in a successful login.
         *
+        * It can be assumed that providerAllowsAuthenticationDataChange with $checkData === true
+        * was called before this, and passed. This method should never fail (other than throwing an
+        * exception).
+        *
         * @param AuthenticationRequest $req
         */
        public function providerChangeAuthenticationData( AuthenticationRequest $req );
@@ -249,7 +289,8 @@ interface PrimaryAuthenticationProvider extends AuthenticationProvider {
         * Post-creation callback
         *
         * Called after the user is added to the database, before secondary
-        * authentication providers are run.
+        * authentication providers are run. Only called if this provider was the one that issued
+        * a PASS.
         *
         * @param User $user User being created (has been added to the database now).
         *   This may become a "UserValue" in the future, or User may be refactored
@@ -266,7 +307,10 @@ interface PrimaryAuthenticationProvider extends AuthenticationProvider {
        /**
         * Post-creation callback
         *
-        * Called when the account creation process ends.
+        * This will be called at the end of any account creation attempt, regardless of whether this
+        * provider was the one that handled it. It will not be called if the account creation process
+        * results in a session timeout (possibly after a successful user creation, while a secondary
+        * provider is waiting for a response).
         *
         * @param User $user User that was attempted to be created.
         *   This may become a "UserValue" in the future, or User may be refactored
@@ -274,6 +318,7 @@ interface PrimaryAuthenticationProvider extends AuthenticationProvider {
         * @param User $creator User doing the creation. This may become a
         *   "UserValue" in the future, or User may be refactored into such.
         * @param AuthenticationResponse $response Authentication response that will be returned
+        *   (PASS or FAIL)
         */
        public function postAccountCreation( $user, $creator, AuthenticationResponse $response );
 
@@ -340,10 +385,15 @@ interface PrimaryAuthenticationProvider extends AuthenticationProvider {
 
        /**
         * Post-link callback
+        *
+        * This will be called at the end of any account linking attempt, regardless of whether this
+        * provider was the one that handled it.
+        *
         * @param User $user User that was attempted to be linked.
         *   This may become a "UserValue" in the future, or User may be refactored
         *   into such.
         * @param AuthenticationResponse $response Authentication response that will be returned
+        *   (PASS or FAIL)
         */
        public function postAccountLink( $user, AuthenticationResponse $response );
 
index 1ccc9c6..c55e65d 100644 (file)
@@ -27,16 +27,27 @@ use StatusValue;
 use User;
 
 /**
- * A secondary authentication provider performs additional authentication steps
- * after a PrimaryAuthenticationProvider has done its thing.
+ * A secondary provider mostly acts when the submitted authentication data has
+ * already been associated to a MediaWiki user account.
  *
- * A SecondaryAuthenticationProvider is used to perform arbitrary checks on an
- * authentication request after the user itself has been authenticated. For
- * example, it might implement a password reset, request the second factor for
- * two-factor auth, or prevent the login if the account is blocked.
+ * For login, a secondary provider performs additional authentication steps
+ * after a PrimaryAuthenticationProvider has identified which MediaWiki user is
+ * trying to log in. For example, it might implement a password reset, request
+ * the second factor for two-factor auth, or prevent the login if the account is blocked.
+ *
+ * For account creation, a secondary provider performs optional extra steps after
+ * a PrimaryAuthenticationProvider has created the user; for example, it can collect
+ * further user information such as a biography.
+ *
+ * (For account linking, secondary providers are not involved.)
+ *
+ * This interface also provides methods for changing authentication data such
+ * as a second-factor token, and callbacks that are invoked after login / account creation
+ * succeeded or failed.
  *
  * @ingroup Auth
  * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
  */
 interface SecondaryAuthenticationProvider extends AuthenticationProvider {
 
@@ -75,10 +86,15 @@ interface SecondaryAuthenticationProvider extends AuthenticationProvider {
 
        /**
         * Post-login callback
+        *
+        * This will be called at the end of a login attempt. It will not be called for unfinished
+        * login attempts that fail by the session timing out.
+        *
         * @param User|null $user User that was attempted to be logged in, if known.
         *   This may become a "UserValue" in the future, or User may be refactored
         *   into such.
         * @param AuthenticationResponse $response Authentication response that will be returned
+        *   (PASS or FAIL)
         */
        public function postAuthentication( $user, AuthenticationResponse $response );
 
@@ -129,6 +145,10 @@ interface SecondaryAuthenticationProvider extends AuthenticationProvider {
         * If $req was returned for AuthManager::ACTION_REMOVE, the corresponding
         * credentials should no longer result in a successful login.
         *
+        * It can be assumed that providerAllowsAuthenticationDataChange with $checkData === true
+        * was called before this, and passed. This method should never fail (other than throwing an
+        * exception).
+        *
         * @param AuthenticationRequest $req
         */
        public function providerChangeAuthenticationData( AuthenticationRequest $req );
@@ -151,6 +171,12 @@ interface SecondaryAuthenticationProvider extends AuthenticationProvider {
 
        /**
         * Start an account creation flow
+        *
+        * @note There is no guarantee this will be called in a successful account
+        *   creation process as the user can just abandon the process at any time
+        *   after the primary provider has issued a PASS and still have a valid
+        *   account. Be prepared to handle any database inconsistencies that result
+        *   from this or continueSecondaryAccountCreation() not being called.
         * @param User $user User being created (has been added to the database).
         *   This may become a "UserValue" in the future, or User may be refactored
         *   into such.
@@ -167,6 +193,7 @@ interface SecondaryAuthenticationProvider extends AuthenticationProvider {
 
        /**
         * Continue an authentication flow
+        *
         * @param User $user User being created (has been added to the database).
         *   This may become a "UserValue" in the future, or User may be refactored
         *   into such.
@@ -183,12 +210,18 @@ interface SecondaryAuthenticationProvider extends AuthenticationProvider {
 
        /**
         * Post-creation callback
+        *
+        * This will be called at the end of an account creation attempt. It will not be called if
+        * the account creation process results in a session timeout (possibly after a successful
+        * user creation, while a secondary provider is waiting for a response).
+        *
         * @param User $user User that was attempted to be created.
         *   This may become a "UserValue" in the future, or User may be refactored
         *   into such.
         * @param User $creator User doing the creation. This may become a
         *   "UserValue" in the future, or User may be refactored into such.
         * @param AuthenticationResponse $response Authentication response that will be returned
+        *   (PASS or FAIL)
         */
        public function postAccountCreation( $user, $creator, AuthenticationResponse $response );
 
index 790a073..86d40f4 100644 (file)
@@ -103,7 +103,7 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function pendingWriteQueryDuration() {
+       public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
@@ -477,8 +477,10 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function ping() {
-               return $this->__call( __FUNCTION__, func_get_args() );
+       public function ping( &$rtt = null ) {
+               return func_num_args()
+                       ? $this->__call( __FUNCTION__, [ &$rtt ] )
+                       : $this->__call( __FUNCTION__, [] ); // method cares about null vs missing
        }
 
        public function getLag() {
index 874e9c4..1adf73d 100644 (file)
@@ -39,6 +39,11 @@ abstract class DatabaseBase implements IDatabase {
 
        /** How long before it is worth doing a dummy query to test the connection */
        const PING_TTL = 1.0;
+       const PING_QUERY = 'SELECT 1 AS ping';
+
+       const TINY_WRITE_SEC = .010;
+       const SLOW_WRITE_SEC = .500;
+       const SMALL_WRITE_ROWS = 100;
 
        /** @var string SQL query */
        protected $mLastQuery = '';
@@ -102,7 +107,6 @@ abstract class DatabaseBase implements IDatabase {
         * @var int
         */
        protected $mTrxLevel = 0;
-
        /**
         * Either a short hexidecimal string if a transaction is active or ""
         *
@@ -110,7 +114,6 @@ abstract class DatabaseBase implements IDatabase {
         * @see DatabaseBase::mTrxLevel
         */
        protected $mTrxShortId = '';
-
        /**
         * The UNIX time that the transaction started. Callers can assume that if
         * snapshot isolation is used, then the data is *at least* up to date to that
@@ -120,10 +123,8 @@ abstract class DatabaseBase implements IDatabase {
         * @see DatabaseBase::mTrxLevel
         */
        private $mTrxTimestamp = null;
-
        /** @var float Lag estimate at the time of BEGIN */
        private $mTrxSlaveLag = null;
-
        /**
         * Remembers the function name given for starting the most recent transaction via begin().
         * Used to provide additional context for error reporting.
@@ -132,7 +133,6 @@ abstract class DatabaseBase implements IDatabase {
         * @see DatabaseBase::mTrxLevel
         */
        private $mTrxFname = null;
-
        /**
         * Record if possible write queries were done in the last transaction started
         *
@@ -140,7 +140,6 @@ abstract class DatabaseBase implements IDatabase {
         * @see DatabaseBase::mTrxLevel
         */
        private $mTrxDoneWrites = false;
-
        /**
         * Record if the current transaction was started implicitly due to DBO_TRX being set.
         *
@@ -148,34 +147,44 @@ abstract class DatabaseBase implements IDatabase {
         * @see DatabaseBase::mTrxLevel
         */
        private $mTrxAutomatic = false;
-
        /**
         * Array of levels of atomicity within transactions
         *
         * @var array
         */
        private $mTrxAtomicLevels = [];
-
        /**
         * Record if the current transaction was started implicitly by DatabaseBase::startAtomic
         *
         * @var bool
         */
        private $mTrxAutomaticAtomic = false;
-
        /**
         * Track the write query callers of the current transaction
         *
         * @var string[]
         */
        private $mTrxWriteCallers = [];
-
        /**
-        * Track the seconds spent in write queries for the current transaction
-        *
-        * @var float
+        * @var float Seconds spent in write queries for the current transaction
         */
        private $mTrxWriteDuration = 0.0;
+       /**
+        * @var integer Number of write queries for the current transaction
+        */
+       private $mTrxWriteQueryCount = 0;
+       /**
+        * @var float Like mTrxWriteQueryCount but excludes lock-bound, easy to replicate, queries
+        */
+       private $mTrxWriteAdjDuration = 0.0;
+       /**
+        * @var integer Number of write queries counted in mTrxWriteAdjDuration
+        */
+       private $mTrxWriteAdjQueryCount = 0;
+       /**
+        * @var float RTT time estimate
+        */
+       private $mRTTEstimate = 0.0;
 
        /** @var array Map of (name => 1) for locks obtained via lock() */
        private $mNamedLocksHeld = [];
@@ -419,8 +428,26 @@ abstract class DatabaseBase implements IDatabase {
                );
        }
 
-       public function pendingWriteQueryDuration() {
-               return $this->mTrxLevel ? $this->mTrxWriteDuration : false;
+       public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL ) {
+               if ( !$this->mTrxLevel ) {
+                       return false;
+               } elseif ( !$this->mTrxDoneWrites ) {
+                       return 0.0;
+               }
+
+               switch ( $type ) {
+                       case self::ESTIMATE_DB_APPLY:
+                               $this->ping( $rtt );
+                               $rttAdjTotal = $this->mTrxWriteAdjQueryCount * $rtt;
+                               $applyTime = max( $this->mTrxWriteAdjDuration - $rttAdjTotal, 0 );
+                               // For omitted queries, make them count as something at least
+                               $omitted = $this->mTrxWriteQueryCount - $this->mTrxWriteAdjQueryCount;
+                               $applyTime += self::TINY_WRITE_SEC * $omitted;
+
+                               return $applyTime;
+                       default: // everything
+                               return $this->mTrxWriteDuration;
+               }
        }
 
        public function pendingWriteCallers() {
@@ -808,7 +835,16 @@ abstract class DatabaseBase implements IDatabase {
         * @return bool
         */
        protected function isWriteQuery( $sql ) {
-               return !preg_match( '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
+               return !preg_match(
+                       '/^(?:SELECT|BEGIN|ROLLBACK|COMMIT|SET|SHOW|EXPLAIN|\(SELECT)\b/i', $sql );
+       }
+
+       /**
+        * @param $sql
+        * @return string|null
+        */
+       protected function getQueryVerb( $sql ) {
+               return preg_match( '/^\s*([a-z]+)/i', $sql, $m ) ? strtoupper( $m[1] ) : null;
        }
 
        /**
@@ -821,8 +857,8 @@ abstract class DatabaseBase implements IDatabase {
         * @return bool
         */
        protected function isTransactableQuery( $sql ) {
-               $verb = substr( $sql, 0, strcspn( $sql, " \t\r\n" ) );
-               return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ] );
+               $verb = $this->getQueryVerb( $sql );
+               return !in_array( $verb, [ 'BEGIN', 'COMMIT', 'ROLLBACK', 'SHOW', 'SET' ], true );
        }
 
        public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
@@ -945,18 +981,22 @@ abstract class DatabaseBase implements IDatabase {
                $this->profiler->profileIn( $queryProf );
                $ret = $this->doQuery( $commentedSql );
                $this->profiler->profileOut( $queryProf );
-               $queryRuntime = microtime( true ) - $startTime;
+               $queryRuntime = max( microtime( true ) - $startTime, 0.0 );
 
                unset( $queryProfSection ); // profile out (if set)
 
                if ( $ret !== false ) {
                        $this->lastPing = $startTime;
                        if ( $isWrite && $this->mTrxLevel ) {
-                               $this->mTrxWriteDuration += $queryRuntime;
+                               $this->updateTrxWriteQueryTime( $sql, $queryRuntime );
                                $this->mTrxWriteCallers[] = $fname;
                        }
                }
 
+               if ( $sql === self::PING_QUERY ) {
+                       $this->mRTTEstimate = $queryRuntime;
+               }
+
                $this->getTransactionProfiler()->recordQueryCompletion(
                        $queryProf, $startTime, $isWrite, $this->affectedRows()
                );
@@ -965,6 +1005,37 @@ abstract class DatabaseBase implements IDatabase {
                return $ret;
        }
 
+       /**
+        * Update the estimated run-time of a query, not counting large row lock times
+        *
+        * LoadBalancer can be set to rollback transactions that will create huge replication
+        * lag. It bases this estimate off of pendingWriteQueryDuration(). Certain simple
+        * queries, like inserting a row can take a long time due to row locking. This method
+        * uses some simple heuristics to discount those cases.
+        *
+        * @param string $sql
+        * @param float $runtime Total runtime, including RTT
+        */
+       private function updateTrxWriteQueryTime( $sql, $runtime ) {
+               $indicativeOfSlaveRuntime = true;
+               if ( $runtime > self::SLOW_WRITE_SEC ) {
+                       $verb = $this->getQueryVerb( $sql );
+                       // insert(), upsert(), replace() are fast unless bulky in size or blocked on locks
+                       if ( $verb === 'INSERT' ) {
+                               $indicativeOfSlaveRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS;
+                       } elseif ( $verb === 'REPLACE' ) {
+                               $indicativeOfSlaveRuntime = $this->affectedRows() > self::SMALL_WRITE_ROWS / 2;
+                       }
+               }
+
+               $this->mTrxWriteDuration += $runtime;
+               $this->mTrxWriteQueryCount += 1;
+               if ( $indicativeOfSlaveRuntime ) {
+                       $this->mTrxWriteAdjDuration += $runtime;
+                       $this->mTrxWriteAdjQueryCount += 1;
+               }
+       }
+
        private function canRecoverFromDisconnect( $sql, $priorWritesPending ) {
                # Transaction dropped; this can mean lost writes, or REPEATABLE-READ snapshots.
                # Dropped connections also mean that named locks are automatically released.
@@ -2739,6 +2810,9 @@ abstract class DatabaseBase implements IDatabase {
                $this->mTrxAtomicLevels = [];
                $this->mTrxShortId = wfRandomString( 12 );
                $this->mTrxWriteDuration = 0.0;
+               $this->mTrxWriteQueryCount = 0;
+               $this->mTrxWriteAdjDuration = 0.0;
+               $this->mTrxWriteAdjQueryCount = 0;
                $this->mTrxWriteCallers = [];
                // First SELECT after BEGIN will establish the snapshot in REPEATABLE-READ.
                // Get an estimate of the slave lag before then, treating estimate staleness
@@ -2967,17 +3041,24 @@ abstract class DatabaseBase implements IDatabase {
                }
        }
 
-       public function ping() {
+       public function ping( &$rtt = null ) {
+               // Avoid hitting the server if it was hit recently
                if ( $this->isOpen() && ( microtime( true ) - $this->lastPing ) < self::PING_TTL ) {
-                       return true;
+                       if ( !func_num_args() || $this->mRTTEstimate > 0 ) {
+                               $rtt = $this->mRTTEstimate;
+                               return true; // don't care about $rtt
+                       }
                }
 
-               $ignoreErrors = true;
-               $this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
                // This will reconnect if possible or return false if not
-               $ok = (bool)$this->query( "SELECT 1 AS ping", __METHOD__, $ignoreErrors );
+               $this->clearFlag( DBO_TRX, self::REMEMBER_PRIOR );
+               $ok = ( $this->query( self::PING_QUERY, __METHOD__, true ) !== false );
                $this->restoreFlags( self::RESTORE_PRIOR );
 
+               if ( $ok ) {
+                       $rtt = $this->mRTTEstimate;
+               }
+
                return $ok;
        }
 
index b689e82..5c632ca 100644 (file)
@@ -59,6 +59,11 @@ interface IDatabase {
        /** @var string Restore to the initial flag state */
        const RESTORE_INITIAL = 'initial';
 
+       /** @var string Estimate total time (RTT, scanning, waiting on locks, applying) */
+       const ESTIMATE_TOTAL = 'total';
+       /** @var string Estimate time to apply (scanning, applying) */
+       const ESTIMATE_DB_APPLY = 'apply';
+
        /**
         * A string describing the current software version, and possibly
         * other details in a user-friendly way. Will be listed on Special:Version, etc.
@@ -210,10 +215,11 @@ interface IDatabase {
         *
         * High times could be due to scanning, updates, locking, and such
         *
+        * @param string $type IDatabase::ESTIMATE_* constant [default: ESTIMATE_ALL]
         * @return float|bool Returns false if not transaction is active
         * @since 1.26
         */
-       public function pendingWriteQueryDuration();
+       public function pendingWriteQueryDuration( $type = self::ESTIMATE_TOTAL );
 
        /**
         * Get the list of method names that did write queries for this transaction
@@ -1485,9 +1491,10 @@ interface IDatabase {
        /**
         * Ping the server and try to reconnect if it there is no connection
         *
+        * @param float|null &$rtt Value to store the estimated RTT [optional]
         * @return bool Success or failure
         */
-       public function ping();
+       public function ping( &$rtt = null );
 
        /**
         * Get slave lag. Currently supported only by MySQL.
index 65cd3b3..32729dd 100644 (file)
@@ -1101,7 +1101,7 @@ class LoadBalancer {
                        }
                        // Assert that the time to replicate the transaction will be sane.
                        // If this fails, then all DB transactions will be rollback back together.
-                       $time = $conn->pendingWriteQueryDuration();
+                       $time = $conn->pendingWriteQueryDuration( $conn::ESTIMATE_DB_APPLY );
                        if ( $limit > 0 && $time > $limit ) {
                                throw new DBTransactionError(
                                        $conn,
index c9ebe32..6f418d3 100644 (file)
@@ -22,7 +22,7 @@
        "config-page-dbsettings": "Sazê Database",
        "config-page-name": "Name",
        "config-page-options": "Weçinegi",
-       "config-page-install": "Barine",
+       "config-page-install": "Bıselagne",
        "config-page-complete": "Temamyayo",
        "config-page-restart": "Barkerdışi fına ser kı",
        "config-page-readme": "Mı bıwane",
index 5f48dca..f25c1ba 100644 (file)
@@ -503,16 +503,21 @@ class JobRunner implements LoggerAwareInterface {
                if ( $wgJobSerialCommitThreshold !== false && $lb->getServerCount() > 1 ) {
                        // Generally, there is one master connection to the local DB
                        $dbwSerial = $lb->getAnyOpenConnection( $lb->getWriterIndex() );
+                       // We need natively blocking fast locks
+                       if ( $dbwSerial && $dbwSerial->namedLocksEnqueue() ) {
+                               $time = $dbwSerial->pendingWriteQueryDuration( $dbwSerial::ESTIMATE_DB_APPLY );
+                               if ( $time < $wgJobSerialCommitThreshold ) {
+                                       $dbwSerial = false;
+                               }
+                       } else {
+                               $dbwSerial = false;
+                       }
                } else {
+                       // There are no slaves or writes are all to foreign DB (we don't handle that)
                        $dbwSerial = false;
                }
 
-               if ( !$dbwSerial
-                       || !$dbwSerial->namedLocksEnqueue()
-                       || $dbwSerial->pendingWriteQueryDuration() < $wgJobSerialCommitThreshold
-               ) {
-                       // Writes are all to foreign DBs, named locks don't form queues,
-                       // or $wgJobSerialCommitThreshold is not reached; commit changes now
+               if ( !$dbwSerial ) {
                        wfGetLBFactory()->commitMasterChanges( __METHOD__ );
                        return;
                }
index 7a89991..73f8280 100644 (file)
@@ -801,6 +801,10 @@ class SqlBagOStuff extends BagOStuff {
                if ( $this->usesMainDB() ) {
                        $lb = $this->getSeparateMainLB()
                                ?: MediaWikiServices::getInstance()->getDBLoadBalancer();
+                       // Return if there are no slaves
+                       if ( $lb->getServerCount() <= 1 ) {
+                               return true;
+                       }
                        // Main LB is used; wait for any slaves to catch up
                        try {
                                $pos = $lb->getMasterPos();
index 8fa212e..31761c3 100644 (file)
@@ -129,6 +129,11 @@ final class Session implements \Countable, \Iterator, \ArrayAccess {
 
        /**
         * Make this session not be persisted across requests
+        *
+        * This will remove persistence information (e.g. delete cookies)
+        * from the associated WebRequest(s), and delete session data in the
+        * backend. The session data will still be available via get() until
+        * the end of the request.
         */
        public function unpersist() {
                $this->backend->unpersist();
@@ -603,6 +608,9 @@ final class Session implements \Countable, \Iterator, \ArrayAccess {
 
        /**
         * Save the session
+        *
+        * This will update the backend data and might re-persist the session
+        * if needed.
         */
        public function save() {
                $this->backend->save();
index 0439b36..263cb11 100644 (file)
@@ -599,7 +599,8 @@ final class SessionBackend {
        }
 
        /**
-        * Save and persist session data, unless delayed
+        * Save the session, unless delayed
+        * @see SessionBackend::save()
         */
        private function autosave() {
                if ( $this->delaySave <= 0 ) {
@@ -608,7 +609,12 @@ final class SessionBackend {
        }
 
        /**
-        * Save and persist session data
+        * Save the session
+        *
+        * Update both the backend data and the associated WebRequest(s) to
+        * reflect the state of the the SessionBackend. This might include
+        * persisting or unpersisting the session.
+        *
         * @param bool $closing Whether the session is being closed
         */
        public function save( $closing = false ) {
index c235861..287da9d 100644 (file)
@@ -73,7 +73,8 @@ class SessionInfo {
         *    Defaults to true.
         *  - forceHTTPS: (bool) Whether to force HTTPS for this session
         *  - metadata: (array) Provider metadata, to be returned by
-        *    Session::getProviderMetadata().
+        *    Session::getProviderMetadata(). See SessionProvider::mergeMetadata()
+        *    and SessionProvider::refreshSessionInfo().
         *  - idIsSafe: (bool) Set true if the 'id' did not come from the user.
         *    Generally you'll use this from SessionProvider::newEmptySession(),
         *    and not from any other method.
@@ -200,7 +201,8 @@ class SessionInfo {
         * The normal behavior is to discard the SessionInfo if validation against
         * the data stored in the session store fails. If this returns true,
         * SessionManager will instead delete the session store data so this
-        * SessionInfo may still be used.
+        * SessionInfo may still be used. This is important for providers which use
+        * deterministic IDs and so cannot just generate a random new one.
         *
         * @return bool
         */
index 8ccb6d1..87fdcd3 100644 (file)
@@ -35,8 +35,15 @@ use WebRequest;
 /**
  * This serves as the entry point to the MediaWiki session handling system.
  *
+ * Most methods here are for internal use by session handling code. Other callers
+ * should only use getGlobalSession and the methods of SessionManagerInterface;
+ * the rest of the functionality is exposed via MediaWiki\Session\Session methods.
+ *
+ * To provide custom session handling, implement a MediaWiki\Session\SessionProvider.
+ *
  * @ingroup Session
  * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
  */
 final class SessionManager implements SessionManagerInterface {
        /** @var SessionManager|null */
@@ -819,9 +826,9 @@ final class SessionManager implements SessionManagerInterface {
        }
 
        /**
-        * Create a session corresponding to the passed SessionInfo
+        * Create a Session corresponding to the passed SessionInfo
         * @private For use by a SessionProvider that needs to specially create its
-        *  own session.
+        *  own Session. Most session providers won't need this.
         * @param SessionInfo $info
         * @param WebRequest $request
         * @return Session
@@ -941,6 +948,7 @@ final class SessionManager implements SessionManagerInterface {
 
        /**
         * Reset the internal caching for unit testing
+        * @protected Unit tests only
         */
        public static function resetCache() {
                if ( !defined( 'MW_PHPUNIT_TEST' ) ) {
index d4e52c7..3ab0f43 100644 (file)
@@ -36,7 +36,8 @@ use WebRequest;
  */
 interface SessionManagerInterface extends LoggerAwareInterface {
        /**
-        * Fetch the session for a request
+        * Fetch the session for a request (or a new empty session if none is
+        * attached to it)
         *
         * @note You probably want to use $request->getSession() instead. It's more
         *  efficient and doesn't break FauxRequests or sessions that were changed
@@ -52,6 +53,7 @@ interface SessionManagerInterface extends LoggerAwareInterface {
 
        /**
         * Fetch a session by ID
+        *
         * @param string $id
         * @param bool $create If no session exists for $id, try to create a new one.
         *  May still return null if a session for $id exists but cannot be loaded.
@@ -62,7 +64,7 @@ interface SessionManagerInterface extends LoggerAwareInterface {
        public function getSessionById( $id, $create = false, WebRequest $request = null );
 
        /**
-        * Fetch a new, empty session
+        * Create a new, empty session
         *
         * The first provider configured that is able to provide an empty session
         * will be used.
index 4d57ad9..61c7500 100644 (file)
@@ -66,13 +66,14 @@ use WebRequest;
  *    would make sense.
  *
  * Note that many methods that are technically "cannot persist ID" could be
- * turned into "can persist ID but not changing User" using a session cookie,
+ * turned into "can persist ID but not change User" using a session cookie,
  * as implemented by ImmutableSessionProviderWithCookie. If doing so, different
  * session cookie names should be used for different providers to avoid
  * collisions.
  *
  * @ingroup Session
  * @since 1.27
+ * @see https://www.mediawiki.org/wiki/Manual:SessionManager_and_AuthManager
  */
 abstract class SessionProvider implements SessionProviderInterface, LoggerAwareInterface {
 
@@ -180,14 +181,23 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI
        /**
         * Merge saved session provider metadata
         *
+        * This method will be used to compare the metadata returned by
+        * provideSessionInfo() with the saved metadata (which has been returned by
+        * provideSessionInfo() the last time the session was saved), and merge the two
+        * into the new saved metadata, or abort if the current request is not a valid
+        * continuation of the session.
+        *
         * The default implementation checks that anything in both arrays is
         * identical, then returns $providedMetadata.
         *
         * @protected For use by \MediaWiki\Session\SessionManager only
         * @param array $savedMetadata Saved provider metadata
-        * @param array $providedMetadata Provided provider metadata
+        * @param array $providedMetadata Provided provider metadata (from the SessionInfo)
         * @return array Resulting metadata
-        * @throws MetadataMergeException If the metadata cannot be merged
+        * @throws MetadataMergeException If the metadata cannot be merged.
+        *  Such exceptions will be handled by SessionManager and are a safe way of rejecting
+        *  a suspicious or incompatible session. The provider is expected to write an
+        *  appropriate message to its logger.
         */
        public function mergeMetadata( array $savedMetadata, array $providedMetadata ) {
                foreach ( $providedMetadata as $k => $v ) {
@@ -211,7 +221,7 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI
         * expected to write an appropriate message to its logger.
         *
         * @protected For use by \MediaWiki\Session\SessionManager only
-        * @param SessionInfo $info
+        * @param SessionInfo $info Any changes by mergeMetadata() will already be reflected here.
         * @param WebRequest $request
         * @param array|null &$metadata Provider metadata, may be altered.
         * @return bool Return false to reject the SessionInfo after all.
@@ -420,6 +430,11 @@ abstract class SessionProvider implements SessionProviderInterface, LoggerAwareI
 
        /**
         * Fetch the rights allowed the user when the specified session is active.
+        *
+        * This is mainly meant for allowing the user to restrict access to the account
+        * by certain methods; you probably want to use this with MWGrants. The returned
+        * rights will be intersected with the user's actual rights.
+        *
         * @param SessionBackend $backend
         * @return null|string[] Allowed user rights, or null to allow all.
         */
index 5322a04..61b6a8c 100644 (file)
@@ -188,6 +188,9 @@ class SpecialPageLanguage extends FormSpecialPage {
                $logid = $entry->insert();
                $entry->publish( $logid );
 
+               // Force re-render so that language-based content (parser functions etc.) gets updated
+               $title->invalidateCache();
+
                return true;
        }
 
index 0646f21..58a91bf 100644 (file)
        "view-pool-error": "Wālā, þā þegntōlas nū oferlīce wyrcaþ.\nTō mænige brūcendas gesēcaþ tō sēonne þisne tramet.\nWē biddaþ þæt þū abīde scortne tīman ǣr þū gesēce to sēonne þisne tramet eft.\n\n$1",
        "pool-queuefull": "Pundfaldes forepenn is full",
        "pool-errorunknown": "Uncūþ wōh",
-       "pool-servererror": "Seo pundaldgetalere þēgnung nis gearo",
+       "pool-servererror": "Seo pundfaldgetalere þēgnung nis gearo",
        "aboutsite": "Gecȳþness ymbe {{GRAMMAR:wrēgendlīc|{{SITENAME}}}}",
        "aboutpage": "Project:Gefrǣge",
        "copyright": "Man mæg innunge under $1 findan, būton þǣr hit is elles amearcod.",
index 1d40a9e..c1c9660 100644 (file)
        "booksources-text": "توجد أدناه قائمة بوصلات لمواقع أخرى تبيع الكتب الجديدة والمستعملة، أيضا يمكنك أن تحصل على معلومات إضافية عن الكتب التي تبحث عنها من هناك:",
        "booksources-invalid-isbn": "رقم ISBN المعطى لا يبدو صحيحا؛ تحقق من أخطاء النسخ من المصدر الأصلي.",
        "specialloguserlabel": "المؤدي:",
-       "speciallogtitlelabel": "اÙ\84Ù\87دÙ\81 (عÙ\86Ù\88اÙ\86 Ø§Ù\88 {{ns:user}}:username للمستخدم):",
+       "speciallogtitlelabel": "اÙ\84Ù\87دÙ\81 (عÙ\86Ù\88اÙ\86 Ø£Ù\88 {{ns:user}}:اسÙ\85 Ø§Ù\84Ù\85ستخدÙ\85 للمستخدم):",
        "log": "سجلات",
        "logeventslist-submit": "أظهر",
        "all-logs-page": "كل السجلات العامة",
index 19e709f..219df61 100644 (file)
        "log-action-filter-rights-autopromote": "স্বয়ংক্রিয় পরিবর্তন",
        "log-action-filter-upload-upload": "নতুন আপলোড",
        "log-action-filter-upload-overwrite": "পুনঃআপলোড",
+       "authmanager-authn-no-primary": "সরবরাহকৃত পরিচয়পত্রের অনুমোদন যাচাই করা যায়নি।",
        "authmanager-create-from-login": "আপনার একাউন্ট তৈরি করতে, নীচের ক্ষেত্রগুলি পূরণ করুন।",
        "authmanager-authplugin-setpass-failed-title": "পাসওয়ার্ড পরিবর্তন ব্যর্থ হয়েছে",
        "authmanager-authplugin-setpass-bad-domain": "অবৈধ ডোমেইন।",
index 388d6c2..9026636 100644 (file)
        "sort-descending": "Rêzkerdışo kêmbiyaye",
        "sort-ascending": "Rêzkerdışo zêdiyaye",
        "nstab-main": "Pele",
-       "nstab-user": "Pela karberi",
+       "nstab-user": "Pera karberi",
        "nstab-media": "Pela medya",
        "nstab-special": "Pera spesiyal",
        "nstab-project": "Pela proceyi",
        "parser-unstrip-recursion-limit": "Sinorê limit dê qayış dê ($1) ravêrya",
        "converter-manual-rule-error": "Rehberê zıwan açarnayışi dı xırabin tesbit biya",
        "undo-success": "No vurnayiş tepeye geryeno. pêverronayişêyê cêrıni kontrol bıkeri.",
-       "undo-failure": "Sebayê pêverameyişê vurnayişan karo tepêya gırewtış nêbı.",
+       "undo-failure": "Poxta pëverameyişa vurnayişan ra  peyd grotışë kari në bı",
        "undo-norev": "Vurnayiş tepêya nêgeryeno çunke ya vere cû hewna biyo ya zi ca ra çino.",
        "undo-summary": "Vırnayışê $1'i [[Special:Contributions/$2|$2i]] ([[User talk:$2|Werênayış]]) peyser gırot",
        "undo-summary-username-hidden": "Rewizyona veri $1'i hewada",
        "historysize": "({{PLURAL:$1|1 bayt|$1 bayti}})",
        "historyempty": "(veng)",
        "history-feed-title": "Tarixê çımraviyarnayışi",
-       "history-feed-description": "Wiki de tarixê çımraviyarnayışê na pele",
+       "history-feed-description": "Wiki de tarixê çım ra viyarnayışë na perer",
        "history-feed-item-nocomment": "$1 miyanê $2i de",
        "history-feed-empty": "Pela cıgeyrayiye çıniya.\nBeno ke ena esteriya, ya zi namê cı vuriyo.\nSeba pelanê muhimanê newan [[Special:Search|cıgeyrayışê wiki de]] bıcerebne.",
        "history-edit-tags": "Etiketa weçinaye rewizyoni timar ke",
        "rev-suppressed-unhide-diff": "Nê Timarkerdışi ra yewi '''çap biyo'''.\n[{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} rocaneyê vındertışi] de teferru'ati esti.\nEke şıma serkari u devam bıkeri [$1 no vurnayiş şıma eşkeni bıvini].",
        "rev-deleted-diff-view": "Jew timarkerdışê ena versiyon '''wedariyayo''.\nÎdarekarî şenê ena versiyon bivîne; belki tiya de [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} wedarnayişî] de teferruat esto.",
        "rev-suppressed-diff-view": "Jew timarkerdışê ena versiyon '''Ploxneyış'' biyo.\nÎdarekarî eşkeno ena dif bivîne; belki tiya de [{{fullurl:{{#Special:Log}}/suppress|page={{FULLPAGENAMEE}}}} ploxnayış] de teferruat esto.",
-       "rev-delundel": "bıasne/bınımne",
+       "rev-delundel": "bımocne/bınımne",
        "rev-showdeleted": "bıasene",
        "revisiondelete": "Çımraviyarnayışan bestere/peyser biya",
        "revdelete-nooldid-title": "Çımraviyarnayışo waşte nêvêreno",
        "mergelog": "Qeydé zew kerdışi",
        "revertmerge": "Abırnê",
        "mergelogpagetext": "Cêr de yew liste esta ke mocnena ra, raya tewr peyêne kamci pela tarixi be a bine ra şanawa pê.",
-       "history-title": "Tarixê çımraviyarnayışê \"$1\"",
+       "history-title": "Tarixê çım ra viyarnayışë \"$1\"",
        "difference-title": "Pela \"$1\" ferqê çım ra viyarnayışan",
        "difference-title-multipage": "Ferkê pelan dê \"$1\" u \"$2\"",
        "difference-multipage": "(Ferqê pelan)",
        "skin-preview": "Verqayt",
        "datedefault": "Tercih çıniyo",
        "prefs-labs": "Xacetê labs",
-       "prefs-user-pages": "Pela Karberi",
-       "prefs-personal": "Pela karberi",
+       "prefs-user-pages": "Pera Karberi",
+       "prefs-personal": "Pera  karberi",
        "prefs-rc": "Vurriyayışê peyêni",
        "prefs-watchlist": "Lista seyrkerdışi",
        "prefs-editwatchlist": "Lista seyrkerdışi bıvurne",
        "recentchanges-legend-heading": "<strong>Kıtabekê Vurriyayışê peyêni:</strong>",
        "recentchanges-legend-newpage": "{{int:recentchanges-label-newpage}} Şıma şenê ([[Special:NewPages|Listey peranê  newan]] zi bıvinê)",
        "recentchanges-legend-plusminus": "''(±123)''",
-       "recentchanges-submit": "Bıasne",
+       "recentchanges-submit": "Bımocne",
        "rcnotefrom": "Cêr de <strong>$2</strong> ra nata {{PLURAL:$5|vurnayışiyê}} asenê (tewr vêşi <strong>$1</strong> asenê) <strong>$3, $4</strong>",
        "rclistfrom": "$3 $2 ra tepiya vurnayışanê neweyan bımocne",
        "rcshowhideminor": "vurriyayışê werdi $1",
        "rcshowhidebots-show": "Bıasne",
        "rcshowhidebots-hide": "Bınımne",
        "rcshowhideliu": "karberê qeydbiyayeyi $1",
-       "rcshowhideliu-show": "Bıasne",
+       "rcshowhideliu-show": "Bımocne",
        "rcshowhideliu-hide": "Bınımne",
        "rcshowhideanons": "karberê bênameyi $1",
        "rcshowhideanons-show": "Bıasne",
        "rcshowhideanons-hide": "Bınımne",
        "rcshowhidepatr": "$1 vurnayışê ke dewriya geyrayê",
-       "rcshowhidepatr-show": "Bıasne",
+       "rcshowhidepatr-show": "Bımocne",
        "rcshowhidepatr-hide": "Bınımne",
        "rcshowhidemine": "vurnayışanê mı $1",
-       "rcshowhidemine-show": "Bıasne",
+       "rcshowhidemine-show": "Bımocne",
        "rcshowhidemine-hide": "Bınımne",
        "rcshowhidecategorization": "kategorizasyonê pele $1",
-       "rcshowhidecategorization-show": "Bıasne",
+       "rcshowhidecategorization-show": "Bımocne",
        "rcshowhidecategorization-hide": "Bınımne",
        "rclinks": "Peyniya $2 rocan de $1 vurriyayışan ra <br />$3 asenê",
        "diff": "ferq",
        "hist": "verên",
        "hide": "Bınımne",
-       "show": "Bıasne",
+       "show": "Bımocne",
        "minoreditletter": "q",
        "newpageletter": "N",
        "boteditletter": "b",
        "recentchanges-page-added-to-category": "[[:$1]] kerd kategoriye miyan",
        "recentchanges-page-removed-from-category": "[[:$1]] kategoriye ra vet",
        "autochange-username": "MediaWiki vurnayışo otomatik",
-       "upload": "Dosya bar ke",
-       "uploadbtn": "Dosya bar ke",
+       "upload": "Dosya bıselagne",
+       "uploadbtn": "Dosya bıselagne",
        "reuploaddesc": "Barkerdışi iptal ke u peyser şo formê barkerdışi",
        "upload-tryagain": "Deskripyonê dosyayî ke vurîya ey qeyd bike",
        "uploadnologin": "Şıma cıkewtış nêvıraşto",
        "upload-too-many-redirects": "Eno URL de zaf redireksiyonî esto.",
        "upload-http-error": "Yew ğeletê HTTPî biyo: $1",
        "upload-copy-upload-invalid-domain": "Na domain ra kopyayê barkerdışanê nêbenê.",
-       "upload-dialog-title": "Dosya bar ke",
+       "upload-dialog-title": "Dosya bıselagne",
        "upload-dialog-button-cancel": "Bıterkın",
        "upload-dialog-button-done": "Temam",
        "upload-dialog-button-save": "Bışevekne",
-       "upload-dialog-button-upload": "Bar ke",
+       "upload-dialog-button-upload": "Bıselagne",
        "upload-form-label-infoform-title": "Teferuati",
        "upload-form-label-infoform-name": "Name",
        "upload-form-label-infoform-description": "Şınasnayış",
        "mimesearch": "MIME bigêre",
        "mimesearch-summary": "Na pele, dosyayanê MIME goreyê tewran ra parzûn kena. Cıkewtış: tewrê zerreki/tewro bınên ya zi tewrê zerreki/*, nımune: <code>image/jpeg</code>.",
        "mimetype": "Babetê NIME",
-       "download": "bar ke",
+       "download": "Bıselagne",
        "unwatchedpages": "Pelanê seyrnibiyeyî",
        "listredirects": "Listeya Hetenayışan",
        "listduplicatedfiles": "Lista dosyeyanê ke kopyaya cı vêniyena",
        "restriction-edit": "Bıvurne",
        "restriction-move": "Bıkırış",
        "restriction-create": "Bıvıraz",
-       "restriction-upload": "Bar ke",
+       "restriction-upload": "Bıselagne",
        "restriction-level-sysop": "tam pawiyayo",
        "restriction-level-autoconfirmed": "nêm pawiyayo",
        "restriction-level-all": "kamci be sewiya",
        "sp-contributions-newbies-title": "Îştîrakê karberî ser hesabê neweyî",
        "sp-contributions-blocklog": "qeydê kılitbiyayeyi",
        "sp-contributions-deleted": "iştırakê karberi esterdi",
-       "sp-contributions-uploads": "barkerdey",
+       "sp-contributions-uploads": "Selagneyey",
        "sp-contributions-logs": "qeydi",
        "sp-contributions-talk": "werênayış",
        "sp-contributions-userrights": "idareyê heqanê karberan",
        "redirect-value": "Erc:",
        "redirect-user": "Kamiya Karberi:",
        "redirect-page": "Kamiya pele",
-       "redirect-revision": "Çımraviyarnayışê pele",
+       "redirect-revision": "Çım ra viyarnayışê perer",
        "redirect-file": "Namey dosya",
        "redirect-logid": "Qeydé  ID",
        "redirect-not-exists": "Erc nêvineyê",
index 67bedaa..379da19 100644 (file)
@@ -13,7 +13,7 @@
        "tog-hideminor": "अहिलका मामूली सम्पादनलाई लुकाउन्या",
        "tog-hidepatrolled": "गस्ती(patrolled)सम्पादनलाई लुकाउन्या",
        "tog-newpageshidepatrolled": "गस्ती गरिया पानानलाई नयाँ पाना  सूचीबठेई लुकाउन्या",
-       "tog-hidecategorization": "पà¥\83षà¥\8dठहरà¥\81को श्रेणीकरण हटाया",
+       "tog-hidecategorization": "पà¥\83षà¥\8dठहरà¥\82को श्रेणीकरण हटाया",
        "tog-extendwatchlist": "निगरानी सूचीलाई सबै परिवर्तन धेकुन्या गरी बढुन्या , ऐईलका बाहेक",
        "tog-usenewrc": "पानाका अहिलका  परिवर्तन र अवलोकन सूचीका आधारमी सामूहिक परिवर्तनहरू",
        "tog-numberheadings": "शीर्षकहरूलाई स्वत:अङ्कित गर",
@@ -44,7 +44,7 @@
        "tog-watchlisthideliu": "प्रवेश गरेका प्रयोगकर्ताहरूको सम्पादन ध्यान सूचीबठेई लुकाउन्या",
        "tog-watchlisthideanons": "अज्ञात प्रयोगकर्ताहरूबाट गरिएको सम्पादन ध्यान सूचीबठेई लुकाउन्या",
        "tog-watchlisthidepatrolled": "बोट सम्पादनहरू ध्यान सूचीबठेई लुकाउन्या",
-       "tog-watchlisthidecategorization": "पà¥\83षà¥\8dठहरà¥\81को श्रेणीकरण लुकौन्या",
+       "tog-watchlisthidecategorization": "पà¥\83षà¥\8dठहरà¥\82को श्रेणीकरण लुकौन्या",
        "tog-ccmeonemails": "मुईले अन्य प्रयोगकर्ताहरूलाई पठाउन्या इ-मेलको प्रतिलिपि मुईलाई पठाउन्या",
        "tog-diffonly": "तलका पानाहरुको भिन्नहरू सामग्री नदेखाउन्या",
        "tog-showhiddencats": "लुकाइएका श्रेणीहरू धेखाउन्या",
index 6e92c24..a620724 100644 (file)
        "rcshowhidemine": "$1 miajn redaktojn",
        "rcshowhidemine-show": "Montri",
        "rcshowhidemine-hide": "Kaŝi",
-       "rcshowhidecategorization": "$1 paĝa enkategoriigo",
+       "rcshowhidecategorization": "$1 kategoriigon de paĝoj",
        "rcshowhidecategorization-show": "Montri",
        "rcshowhidecategorization-hide": "Kaŝi",
        "rclinks": "Montri $1 lastajn ŝanĝojn dum la $2 lastaj tagoj.<br />$3",
index 3aabbe6..5c4bd0b 100644 (file)
        "import-error-invalid": "Kaca \"$1\" ora diimpor amarga jenengé ora sah.",
        "import-error-unserialize": "Revisi $2 saka kaca \"$1\" ora bisa diurutaké. Revisi iku dilapuraké murih nganggo gagrag isi $3 sing diurutaké minangka $4.",
        "import-options-wrong": "{{PLURAL:$2|Opsi|Opsi}} salah: <nowiki>$1</nowiki>",
-       "import-rootpage-invalid": "Halaman turunan yang diberikan adalah judul yang salah.",
+       "import-rootpage-invalid": "Kaca wod iki sesirahé ora sah.",
        "import-rootpage-nosubpage": "Ruang nama \"$1\" di halaman turunan tidak mengizinkan subhalaman.",
        "importlogpage": "Log impor",
        "importlogpagetext": "Impor administratif kaca-kaca mawa sajarah panyuntingan saka wiki liya.",
        "tooltip-diff": "Tuduhaké owah-owahan endi sing sampéyan gawé tumrap tulisan iki",
        "tooltip-compareselectedversions": "Delengen prabédan antara rong vèrsi kaca iki sing dipilih.",
        "tooltip-watch": "Wuwuh kaca iki nyang pawawanganing sampéyan",
-       "tooltip-watchlistedit-normal-submit": "Singkiraké judhul",
+       "tooltip-watchlistedit-normal-submit": "Busak sesirah",
        "tooltip-watchlistedit-raw-submit": "Anyari daptar pangawasan",
        "tooltip-recreate": "Gawéa kaca iki manèh senadyan tau dibusak",
        "tooltip-upload": "Miwiti pangunggahan",
        "pageinfo-header-edits": "Sujarah besutan",
        "pageinfo-header-restrictions": "Perlindungan halaman",
        "pageinfo-header-properties": "Properti kaca",
-       "pageinfo-display-title": "Judul tampilan",
+       "pageinfo-display-title": "Sesirah pajangan",
        "pageinfo-default-sort": "Kunci urut baku",
        "pageinfo-length": "Panjang halaman (dalam bita)",
        "pageinfo-article-id": "ID kaca",
        "exif-ycbcrcoefficients": "Koèfisièn matriks transformasi papan werna",
        "exif-referenceblackwhite": "Wiji réferènsi pasangan ireng putih",
        "exif-datetime": "Tanggal lan tabuh owahing barkas",
-       "exif-imagedescription": "Judhul gambar",
+       "exif-imagedescription": "Sesirah gambar",
        "exif-make": "Produsèn kamera",
        "exif-model": "Modhèl kaméra",
        "exif-software": "Piranti alus sing dianggo",
        "exif-provinceorstatedest": "Propinsi utawa nagara bagéyan katampilaké",
        "exif-citydest": "Kutha katampilaké",
        "exif-sublocationdest": "Dhaèrahé kutha katampilaké",
-       "exif-objectname": "Judhul cendhèk",
+       "exif-objectname": "Sesirah cekak",
        "exif-specialinstructions": "Prèntah kusus",
        "exif-headline": "Tajuk",
        "exif-credit": "Krédit/Panyadhiya",
        "compare-rev1": "Révisi 1",
        "compare-rev2": "Révisi 2",
        "compare-submit": "Bandingaké",
-       "compare-invalid-title": "Judhul sing Sampéyan awèhaké ora sah.",
-       "compare-title-not-exists": "Judhul sing Sampéyan jaluk ora ana.",
+       "compare-invalid-title": "Sesirah sing kokawèhaké ora sah.",
+       "compare-title-not-exists": "Sesirah sing kokawèhaké ora ana.",
        "compare-revision-not-exists": "Benahan sing Sampéyan jaluk ora ana.",
        "dberr-problems": "Nyuwun ngapura! Situs iki ngalami masalah tèknis.",
        "dberr-again": "Coba nunggu sawetara menit lan unggahna manèh.",
index f6df3c3..8e077ea 100644 (file)
        "mw-widgets-dateinput-placeholder-day": "ЖЖЖЖ-АА-КК",
        "mw-widgets-dateinput-placeholder-month": "ЖЖЖЖ-АА",
        "mw-widgets-titleinput-description-new-page": "бет жоқ екен",
-       "mw-widgets-titleinput-description-redirect": "$1 дегенге бағыттату"
+       "mw-widgets-titleinput-description-redirect": "$1 дегенге бағыттату",
+       "log-action-filter-protect": "Қорғау түрі"
 }
index c23dca5..0513571 100644 (file)
        "tagline": "Från {{SITENAME}}",
        "help": "Hjälp",
        "search": "Sök",
-       "search-ignored-headings": "#<!-- lämna denna rad precis som den är --> <pre>\n# Rubriker som kommer att ignoreras av sökningen.\n# Ändringar till detta kommer att gälla så fort sidan med rubriken är indexerad.\n# Du kan tvinga sidan att indexeras om genom att göra en null-redigering.\n# Syntaxen är som följer:\n#  * Allt från ett \"#\" tecken till slutet av raden är en kommentar.\n#  * Varje icke-tom rad är den exakta titeln som ska ignoreras, shiftläge och allt.\nReferenser\nExterna länkar\nSe även\n #</pre> <!-- lämna denna rad precis som den är -->",
+       "search-ignored-headings": "#<!-- lämna denna rad precis som den är --> <pre>\n# Rubriker som kommer att ignoreras av sökningen.\n# Förändringar av detta kommer att gälla så fort sidan med rubriken är indexerad.\n# Du kan tvinga sidan att indexeras om genom att göra en null-redigering.\n# Syntaxen är som följer:\n#  * Allt från ett \"#\" tecken till slutet av raden är en kommentar.\n#  * Varje icke-tom rad är den exakta titeln som ska ignoreras, shiftläge och allt.\nReferenser\nExterna länkar\nSe även\n #</pre> <!-- lämna denna rad precis som den är -->",
        "searchbutton": "Sök",
        "go": "Gå till",
        "searcharticle": "Gå till",
        "actionthrottledtext": "Som skydd mot missbruk finns det en begränsning av hur många gånger du kan utföra den här åtgärden under en viss tid. Du har överskridit den gränsen.\nFörsök igen om några minuter.",
        "protectedpagetext": "Den här sidan har skrivskyddats för att förhindra redigering eller andra åtgärder.",
        "viewsourcetext": "Du kan se och kopiera denna sidas källtext.",
-       "viewyourtext": "Du kan se och kopiera källan för <strong>dina redigeringar</strong> av denna sida.",
+       "viewyourtext": "Du kan se och kopiera källtexten för <strong>dina redigeringar</strong> av denna sida.",
        "protectedinterface": "Denna sida innehåller text för mjukvarans gränssnitt på denna wiki, och är skrivskyddad för att förebygga missbruk.\nFör att lägga till eller ändra översättningar för alla wikis, var god använd [https://translatewiki.net/ translatewiki.net], lokaliseringsprojektet för MediaWiki.",
        "editinginterface": "<strong>Varning:</strong> Du redigerar en sida som används för texten i gränssnittet.\nÄndringar på denna sida kommer att påverka användargränssnittets utseende för andra användare på denna wiki.",
        "translateinterface": "För att lägga till eller ändra översättningar för alla wikis, använd [https://translatewiki.net/ translatewiki.net], lokaliseringsprojektet för MediaWiki.",
        "right-createpage": "Skapa sidor (som inte är diskussionssidor)",
        "right-createtalk": "Skapa diskussionssidor",
        "right-createaccount": "Skapa nya användarkonton",
-       "right-autocreateaccount": "Logga in automatiskt med en extern användarkonto",
+       "right-autocreateaccount": "Logga in automatiskt med ett externt användarkonto",
        "right-minoredit": "Markera redigeringar som mindre",
        "right-move": "Flytta sidor",
        "right-move-subpages": "Flytta sidor med deras undersidor",
        "filename-tooshort": "Filnamnet är för kort.",
        "filetype-banned": "Denna typ av fil är förbjuden.",
        "verification-error": "Denna fil klarade inte verifieringen.",
-       "hookaborted": "Ändringen du försökte göra avbröts av en extension hook.",
+       "hookaborted": "Ändringen du försökte göra avbröts av ett tillägg.",
        "illegal-filename": "Filnamnet är inte tillåtet.",
        "overwrite": "Det är inte tillåtet att skriva över en befintlig fil.",
        "unknown-error": "Ett okänt fel uppstod.",
        "fileexists-extension": "En fil med ett liknande namn finns redan: [[$2|thumb]]\n* Namn på den fil du försöker ladda upp: <strong>[[:$1]]</strong>\n* Namn på filen som redan finns: <strong>[[:$2]]</strong>\nVill du möjligen välja ett mer distinkt namn?",
        "fileexists-thumbnail-yes": "Filen verkar vara en bild med förminskad storlek ''(miniatyrbild)''. [[$1|thumb]]\nVar vänlig kontrollera filen <strong>[[:$1]]</strong>.\nOm det är samma fil i originalstorlek så är det inte nödvändigt att ladda upp en extra miniatyrbild.",
        "file-thumbnail-no": "Filnamnet börjar med <strong>$1</strong>.\nDet verkar vara en bild med förminskad storlek ''(miniatyrbild)''.\nOm du har denna bild i full storlek, ladda då hellre upp den, annars var vänlig och ändra filens namn.",
-       "fileexists-forbidden": "En fil med detta namn existerar redan, och kan inte överskrivas.\nOm du fortfarande vill ladda upp din fil, var god gå tillbaka och välj ett nytt namn. [[File:$1|thumb|center|$1]]",
+       "fileexists-forbidden": "En fil med detta namn existerar redan, och kan inte skrivas över.\nOm du ändå vill ladda upp din fil, gå då tillbaka och använd ett annat namn. [[File:$1|thumb|center|$1]]",
        "fileexists-shared-forbidden": "En fil med detta namn finns redan bland de delade filerna.\nOm du ändå vill ladda upp din fil, gå då tillbaka och använd ett annat namn. [[File:$1|thumb|center|$1]]",
        "file-exists-duplicate": "Denna fil är en dubblett av följande {{PLURAL:$1|fil|filer}}:",
        "file-deleted-duplicate": "En identisk fil till den här filen ([[:$1]]) har tidigare raderats. Du bör kontrollera den filens raderingshistorik innan du fortsätter att återuppladda den.",
        "filejournal-fail-dbquery": "Kunde inte uppdatera journaldatabasen för lagringssystemet \"$1\".",
        "lockmanager-notlocked": "Kunde inte låsa upp \"$1\"; den är inte låst.",
        "lockmanager-fail-closelock": "Kunde inte att stänga låsfilen för \"$1\".",
-       "lockmanager-fail-deletelock": "Kunde inte att radera låsfilen för \"$1\".",
-       "lockmanager-fail-acquirelock": "Kunde inte skaffa låset för \"$1\".",
-       "lockmanager-fail-openlock": "Kunde inte att öppna låsfilen för \"$1\".",
-       "lockmanager-fail-releaselock": "Kunde inte att frigöra låset för \"$1\".",
+       "lockmanager-fail-deletelock": "Kunde inte radera låsfilen för \"$1\".",
+       "lockmanager-fail-acquirelock": "Kunde inte skaffa lås för \"$1\".",
+       "lockmanager-fail-openlock": "Kunde inte öppna låsfilen för \"$1\".",
+       "lockmanager-fail-releaselock": "Kunde inte att frigöra lås för \"$1\".",
        "lockmanager-fail-db-bucket": "Kunde inte kontakta tillräckligt många låsdatabaser i hinken $1.",
        "lockmanager-fail-db-release": "Kunde inte frigöra låsen på databasen $1 .",
        "lockmanager-fail-svr-acquire": "Kunde inte erhålla lås på servern $1 .",
-       "lockmanager-fail-svr-release": "Kunde inte frigöra låsen på servern $1.",
+       "lockmanager-fail-svr-release": "Kunde inte frigöra lås på servern $1.",
        "zip-file-open-error": "Ett fel inträffade när filen öppnades för en ZIP-kontroll.",
        "zip-wrong-format": "Den angivna filen var inte en ZIP-fil.",
        "zip-bad": "Filen är en skadad eller annars oläsbar ZIP-fil.\nDen kan inte säkerhetskontrolleras ordentligt.",
        "filerevert-legend": "Återställ fil",
        "filerevert-intro": "Du återställer '''[[Media:$1|$1]]''' till [$4 versionen från $2 kl. $3].",
        "filerevert-comment": "Anledning:",
-       "filerevert-defaultcomment": "Återställer till versionen från $1 kl. $2 ($3)",
+       "filerevert-defaultcomment": "Återställd till versionen från $1, kl. $2 ($3)",
        "filerevert-submit": "Återställ",
        "filerevert-success": "'''[[Media:$1|$1]]''' har återställts till [$4 versionen från $2 kl. $3].",
        "filerevert-badversion": "Det finns ingen tidigare version av filen från den angivna tidpunkten.",
        "listgrouprights-namespaceprotection-header": "Namnrymdsbegränsningar",
        "listgrouprights-namespaceprotection-namespace": "Namnrymd",
        "listgrouprights-namespaceprotection-restrictedto": "Rättighet(er) som låter användare redigera",
-       "listgrants": "Beviljanden",
+       "listgrants": "Behörigheter",
        "listgrants-summary": "Följande är en lista över behörigheter med deras associerade tillgång till användarrättigheter. Användare kan tillåta applikationer att använda deras konto, men med begränsad åtkomst baserat på de behörigheter användaren gav applikationen. En applikation som agerar på uppdrag av en användare kan i praktiken inte använda rättigheter som den användaren saknar.\nDet kan finnas [[{{MediaWiki:Listgrouprights-helppage}}|ytterligare information]] om individuella rättigheter.",
        "listgrants-grant": "Behörighet",
        "listgrants-rights": "Rättigheter",
        "watchlist-hide": "Dölj",
        "watchlist-submit": "Visa",
        "wlshowtime": "Tidsperiod att visa:",
-       "wlshowhideminor": "mindre redigering",
+       "wlshowhideminor": "mindre redigeringar",
        "wlshowhidebots": "robotar",
        "wlshowhideliu": "registrerade användare",
        "wlshowhideanons": "anonyma användare",
        "exbeforeblank": "innehåll före tömning var: \"$1\"",
        "delete-confirm": "Radera \"$1\"",
        "delete-legend": "Radera",
-       "historywarning": "<strong>Varning:</strong> Sidan du håller på att radera har en historik med ungefär $1 {{PLURAL:$1|version|versioner}}:",
+       "historywarning": "<strong>Varning:</strong> Sidan du håller på att radera har en historik med $1 {{PLURAL:$1|version|versioner}}:",
        "historyaction-submit": "Visa",
        "confirmdeletetext": "Du håller på att ta bort en sida med hela dess historik.\nBekräfta att du förstår vad du håller på med och vilka konsekvenser detta leder till, och att du följer [[{{MediaWiki:Policy-url}}|riktlinjerna]].",
        "actioncomplete": "Genomfört",
        "sp-contributions-username": "IP-adress eller användarnamn:",
        "sp-contributions-toponly": "Visa endast aktuella sidversioner",
        "sp-contributions-newonly": "Visa endast redigeringar där sidor skapas",
-       "sp-contributions-hideminor": "Dölj mindre ändringar",
+       "sp-contributions-hideminor": "Dölj mindre redigeringar",
        "sp-contributions-submit": "Sök",
        "whatlinkshere": "Vad som länkar hit",
        "whatlinkshere-title": "Sidor som länkar till \"$1\"",
        "moveuserpage-warning": "'''Varning:''' Du håller på att flytta en användarsida. Observera att endast sidan kommer att flyttas och att användaren ''inte'' kommer att byta namn.",
        "movecategorypage-warning": "<strong>Varning:</strong> Du är på väg att flytta en kategorisida. Observera att endast sidan kommer att flyttas och eventuella sidor i den gamla kategorin kommer <em>inte</em> att kategoriseras om till den nya kategorin.",
        "movenologintext": "För att flytta en sida måste du vara registrerad användare och [[Special:UserLogin|inloggad]].",
-       "movenotallowed": "Du har inte behörighet att flytta sidor på den här wikin.",
+       "movenotallowed": "Du har inte behörighet att flytta sidor.",
        "movenotallowedfile": "Du har inte tillåtelse att flytta filer.",
        "cant-move-user-page": "Du har inte behörighet att flytta användarsidor (bortsett från undersidor).",
        "cant-move-to-user-page": "Du har inte behörighet att flytta en sida till en användarsida (förutom till en användarundersida).",
        "cant-move-category-page": "Du har inte behörighet att flytta kategorisidor.",
-       "cant-move-to-category-page": "Du har inte behörighet att en sida till en kategorisida.",
+       "cant-move-to-category-page": "Du har inte behörighet att flytta en sida till en kategorisida.",
        "newtitle": "Ny titel:",
        "move-watch": "Bevaka denna sida",
        "movepagebtn": "Flytta sidan",
        "export-addcat": "Lägg till",
        "export-addnstext": "Lägg till sidor från namnrymd:",
        "export-addns": "Lägg till",
-       "export-download": "Ladda ner som fil",
+       "export-download": "Spara som fil",
        "export-templates": "Inkludera mallar",
        "export-pagelinks": "Inkludera länkade sidor till ett djup på:",
        "export-manual": "Lägg till sidor manuellt:",
        "thumbnail_image-type": "Bildtypen stöds inte",
        "thumbnail_gd-library": "Inkomplett GD library konfigurering: saknar funktionen $1",
        "thumbnail_image-missing": "Fil verkar saknas: $1",
-       "thumbnail_image-failure-limit": "Det har nyligen förekommit alltför många misslyckade ($1 eller fler) försök skapa den här miniatyrbilden. Försök igen senare.",
+       "thumbnail_image-failure-limit": "Det har nyligen förekommit alltför många misslyckade försök ($1 eller fler) att skapa den här miniatyrbilden. Försök igen senare.",
        "import": "Importera sidor",
        "importinterwiki": "Importera från en annan wiki",
        "import-interwiki-text": "Välj en wiki och sidtitel att importera.\nVersionshistorikens datum och redigerare kommer att bevaras.\nAll importering från andra wikis listas i [[Special:Log/import|importloggen]].",
        "import-mapping-subpage": "Importera som undersidor till följande sida:",
        "import-upload-filename": "Filnamn:",
        "import-comment": "Kommentar:",
-       "importtext": "Var god exportera filen från ursprungs-wikin med hjälp av [[Special:Export|exporteringsverktyget]].\nSpara den på din dator och ladda upp den här.",
+       "importtext": "Var god exportera filen frånkällwikin med hjälp av [[Special:Export|exporteringsverktyget]].\nSpara den på din dator och ladda upp den här.",
        "importstart": "Importerar sidor....",
        "import-revision-count": "$1 {{PLURAL:$1|version|versioner}}",
        "importnopages": "Det finns inga sidor att importera.",
        "importuploaderrorpartial": "Uppladdningen av importfilen misslyckades. Bara en del av filen laddades upp.",
        "importuploaderrortemp": "Uppladdningen av importfilen misslyckades. En temporär katalog saknas.",
        "import-parse-failure": "Tolkningsfel vid XML-import",
-       "import-noarticle": "Inga sidor att importera!",
+       "import-noarticle": "Ingen sida att importera!",
        "import-nonewrevisions": "Inga sidversioner importerades (alla var antingen redan där eller hoppades över p.g.a. fel).",
        "xml-error-string": "$1 på rad $2, kolumn $3 (byte $4): $5",
        "import-upload": "Ladda upp XML-data",
        "javascripttest-pagetext-unknownaction": "Okänd handling \"$1\".",
        "javascripttest-qunit-intro": "Se [$1 testningsdokumentationen] på mediawiki.org.",
        "tooltip-pt-userpage": "{{GENDER:|Din användarsida}}",
-       "tooltip-pt-anonuserpage": "Användarsida för ip-numret du redigerar från",
+       "tooltip-pt-anonuserpage": "Användarsida för IP-numret du redigerar från",
        "tooltip-pt-mytalk": "{{GENDER:|Din}} diskussionssida",
-       "tooltip-pt-anontalk": "Diskussion om redigeringar från det här ip-numret",
+       "tooltip-pt-anontalk": "Diskussion om redigeringar från det här IP-numret",
        "tooltip-pt-preferences": "{{GENDER:|Dina}} inställningar",
        "tooltip-pt-watchlist": "Listan över sidor du bevakar för ändringar",
        "tooltip-pt-mycontris": "Lista över {{GENDER:|dina}} bidrag",
        "pageinfo-few-watchers": "Färre än $1 {{PLURAL:$1|bevakare}}",
        "pageinfo-few-visiting-watchers": "Det kan finnas någon bevakande användare som granskar nyliga redigeringar",
        "pageinfo-redirects-name": "Antal omdirigeringar till denna sida",
-       "pageinfo-subpages-name": "Undersidor till denna sida",
+       "pageinfo-subpages-name": "Antal undersidor till denna sida",
        "pageinfo-subpages-value": "$1 ($2 {{PLURAL:$2|omdirigering|omdirigeringar}}; $3 {{PLURAL:$3|icke-omdirigering|icke-omdirigeringar}})",
        "pageinfo-firstuser": "Sidskapare",
        "pageinfo-firsttime": "Datum när sidan skapades",
        "markaspatrolledtext": "Märk den här sidan som patrullerad",
        "markaspatrolledtext-file": "Märk denna filversion som patrullerad",
        "markedaspatrolled": "Markerad som patrullerad",
-       "markedaspatrolledtext": "Den valda versionen av [[:$1]] har märkts som patrullerad.",
+       "markedaspatrolledtext": "Den valda versionen av [[:$1]] har markerats som patrullerad.",
        "rcpatroldisabled": "Patrullering av Senaste ändringar är avstängd.",
        "rcpatroldisabledtext": "Funktionen \"patrullering av Senaste ändringar\" är tillfälligt avstängd.",
        "markedaspatrollederror": "Kan inte markera som patrullerad",
index 5be33fa..6118bb0 100644 (file)
        "cantrollback": "تدوین ثانی کا اعادہ نہیں کیا جاسکتا؛ کیونکہ اس میں آخری بار حصہ لینے والا ہی اس صفحہ کا واحد کاتب ہے۔",
        "changecontentmodel-title-label": "صفحہ کا عنوان",
        "changecontentmodel-reason-label": "وجہ:",
+       "log-name-contentmodel": "نوشتہ تبدیلی نمونہ مواد",
+       "logentry-contentmodel-change": "$1 نے صفحہ $3 کے مواد کی ساخت کو \"$4\" سے \"$5\" میں {{GENDER:$2|تبدیل کیا}}",
        "protectlogpage": "نوشتۂ محفوظ شدگی",
        "protectedarticle": "\"[[$1]]\" کومحفوظ کردیا",
        "unprotectedarticle": "\"[[$1]]\" کوغیر محفوظ کیا",
index 2afd362..92c5a16 100644 (file)
        "rollbacklinkcount-morethan": "lùi tất cả hơn $1 sửa đổi",
        "rollbackfailed": "Lùi sửa đổi không thành công",
        "rollback-missingparam": "Yêu cầu thiếu những tham số bắt buộc.",
+       "rollback-missingrevision": "Không thể tải dữ liệu phiên bản.",
        "cantrollback": "Không lùi sửa đổi được;\nngười viết trang cuối cùng cũng là tác giả duy nhất của trang này.",
        "alreadyrolled": "Không thể lùi tất cả sửa đổi cuối của [[User:$2|$2]] ([[User talk:$2|thảo luận]]{{int:pipe-separator}}[[Special:Contributions/$2|{{int:contribslink}}]]) tại [[:$1]]; ai đó đã thực hiện sửa đổi hoặc thực hiện lùi tất cả rồi.\n\nSửa đổi cuối cùng tại trang do [[User:$3|$3]] ([[User talk:$3|thảo luận]]{{int:pipe-separator}}[[Special:Contributions/$3|{{int:contribslink}}]]) thực hiện.",
        "editcomment": "Tóm lược sửa đổi: <em>$1</em>.",
        "linkaccounts-submit": "Liên kết tài khoản",
        "unlinkaccounts": "Gỡ liên kết tài khoản",
        "unlinkaccounts-success": "Đã gỡ liên kết tài khoản.",
-       "authenticationdatachange-ignored": "Tác vụ thay đổi dữ liệu xác thực không được xử lý. Có lẽ nhà cung cấp chưa được cấu hình?"
+       "authenticationdatachange-ignored": "Tác vụ thay đổi dữ liệu xác thực không được xử lý. Có lẽ nhà cung cấp chưa được cấu hình?",
+       "userjsispublic": "Xin lưu ý: Các trang con JavaScript không nên chứa dữ liệu bí mật, vì những người dùng khác có thể xem các trang này.",
+       "usercssispublic": "Xin lưu ý: Các trang con CSS không nên chứa dữ liệu bí mật, vì những người dùng khác có thể xem các trang này."
 }
index 3d90307..5c3715d 100644 (file)
 
                capsuleWidget: {
                        getApiValue: function () {
-                               return this.getItemsData().join( '|' );
+                               var items = this.getItemsData();
+                               if ( items.join( '' ).indexOf( '|' ) === -1 ) {
+                                       return items.join( '|' );
+                               } else {
+                                       return '\x1f' + items.join( '\x1f' );
+                               }
                        },
                        setApiValue: function ( v ) {
-                               this.setItemsFromData( v === undefined || v === '' ? [] : String( v ).split( '|' ) );
+                               if ( v === undefined || v === '' || v === '\x1f' ) {
+                                       this.setItemsFromData( [] );
+                               } else {
+                                       v = String( v );
+                                       if ( v.indexOf( '\x1f' ) !== 0 ) {
+                                               this.setItemsFromData( v.split( '|' ) );
+                                       } else {
+                                               this.setItemsFromData( v.substr( 1 ).split( '\x1f' ) );
+                                       }
+                               }
                        },
                        apiCheckValid: function () {
                                var ok = this.getApiValue() !== undefined || suppressErrors;
index 946823d..4d86cfd 100644 (file)
                        prop: [ 'info' ],
                        titles: titles
                } ).done( function ( response ) {
+                       var
+                               normalized = {},
+                               pages = {};
+                       $.each( response.query.normalized || [], function ( index, data ) {
+                               normalized[ data.fromencoded ? decodeURIComponent( data.from ) : data.from ] = data.to;
+                       } );
                        $.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( '|' ) + '"' );
+                               pages[ page.title ] = !page.missing;
+                       } );
+                       $.each( titles, function ( index, title ) {
+                               var normalizedTitle = title;
+                               while ( normalized[ normalizedTitle ] ) {
+                                       normalizedTitle = normalized[ normalizedTitle ];
                                }
+                               cache.existenceCache[ title ] = pages[ normalizedTitle ];
                                queue[ title ].resolve( cache.existenceCache[ title ] );
                        } );
                } );
index a8ee4c7..b7579ff 100644 (file)
@@ -9,6 +9,9 @@
         *     `options` to mw.Api constructor.
         * @property {Object} defaultOptions.parameters Default query parameters for API requests.
         * @property {Object} defaultOptions.ajax Default options for jQuery#ajax.
+        * @property {boolean} defaultOptions.useUS Whether to use U+001F when joining multi-valued
+        *     parameters (since 1.28). Default is true if ajax.url is not set, false otherwise for
+        *     compatibility.
         * @private
         */
        var defaultOptions = {
@@ -95,6 +98,8 @@
                        options.ajax.url = String( options.ajax.url );
                }
 
+               options = $.extend( { useUS: !options.ajax || !options.ajax.url }, options );
+
                options.parameters = $.extend( {}, defaultOptions.parameters, options.parameters );
                options.ajax = $.extend( {}, defaultOptions.ajax, options.ajax );
 
                 *
                 * @private
                 * @param {Object} parameters (modified in-place)
+                * @param {boolean} useUS Whether to use U+001F when joining multi-valued parameters.
                 */
-               preprocessParameters: function ( parameters ) {
+               preprocessParameters: function ( parameters, useUS ) {
                        var key;
                        // Handle common MediaWiki API idioms for passing parameters
                        for ( key in parameters ) {
                                // Multiple values are pipe-separated
                                if ( $.isArray( parameters[ key ] ) ) {
-                                       parameters[ key ] = parameters[ key ].join( '|' );
+                                       if ( !useUS || parameters[ key ].join( '' ).indexOf( '|' ) === -1 ) {
+                                               parameters[ key ] = parameters[ key ].join( '|' );
+                                       } else {
+                                               parameters[ key ] = '\x1f' + parameters[ key ].join( '\x1f' );
+                                       }
                                }
                                // Boolean values are only false when not given at all
                                if ( parameters[ key ] === false || parameters[ key ] === undefined ) {
                                delete parameters.token;
                        }
 
-                       this.preprocessParameters( parameters );
+                       this.preprocessParameters( parameters, this.defaults.useUS );
 
                        // If multipart/form-data has been requested and emulation is possible, emulate it
                        if (
index 077e84d..a1a4999 100644 (file)
                 * Get a set of messages.
                 *
                 * @param {Array} messages Messages to retrieve
+                * @param {Object} [options] Additional parameters for the API call
                 * @return {jQuery.Promise}
                 */
-               getMessages: function ( messages ) {
-                       return this.get( {
+               getMessages: function ( messages, options ) {
+                       options = options || {};
+                       return this.get( $.extend( {
                                action: 'query',
                                meta: 'allmessages',
                                ammessages: messages,
                                amlang: mw.config.get( 'wgUserLanguage' ),
                                formatversion: 2
-                       } ).then( function ( data ) {
+                       }, options ) ).then( function ( data ) {
                                var result = {};
 
                                $.each( data.query.allmessages, function ( i, obj ) {
                 * Loads a set of messages and add them to mw.messages.
                 *
                 * @param {Array} messages Messages to retrieve
+                * @param {Object} [options] Additional parameters for the API call
                 * @return {jQuery.Promise}
                 */
-               loadMessages: function ( messages ) {
-                       return this.getMessages( messages ).then( $.proxy( mw.messages, 'set' ) );
+               loadMessages: function ( messages, options ) {
+                       return this.getMessages( messages, options ).then( $.proxy( mw.messages, 'set' ) );
                },
 
                /**
                 * are loaded. If all messages are known, the returned promise is resolved immediately.
                 *
                 * @param {Array} messages Messages to retrieve
+                * @param {Object} [options] Additional parameters for the API call
                 * @return {jQuery.Promise}
                 */
-               loadMessagesIfMissing: function ( messages ) {
+               loadMessagesIfMissing: function ( messages, options ) {
                        var missing = messages.filter( function ( msg ) {
                                return !mw.message( msg ).exists();
                        } );
@@ -62,7 +66,7 @@
                                return $.Deferred().resolve();
                        }
 
-                       return this.getMessages( missing ).then( $.proxy( mw.messages, 'set' ) );
+                       return this.getMessages( missing, options ).then( $.proxy( mw.messages, 'set' ) );
                }
        } );
 
index 0af2a75..069fbbf 100644 (file)
                                value = options[ name ] === null ? null : String( options[ name ] );
 
                                // Can we bundle this option, or does it need a separate request?
-                               bundleable =
-                                       ( value === null || value.indexOf( '|' ) === -1 ) &&
-                                       ( name.indexOf( '|' ) === -1 && name.indexOf( '=' ) === -1 );
+                               if ( this.defaults.useUS ) {
+                                       bundleable = name.indexOf( '=' ) === -1;
+                               } else {
+                                       bundleable =
+                                               ( value === null || value.indexOf( '|' ) === -1 ) &&
+                                               ( name.indexOf( '|' ) === -1 && name.indexOf( '=' ) === -1 );
+                               }
 
                                if ( bundleable ) {
                                        if ( value !== null ) {
index 4c57faa..e468768 100644 (file)
                '|&#x[0-9A-Fa-f]+;'
        ),
 
-       // From MediaWikiTitleCodec.php#L225 @26fcab1f18c568a41
-       // "Clean up whitespace" in function MediaWikiTitleCodec::splitTitleString()
-       rWhitespace = /[ _\u0009\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000\s]+/g,
+       // From MediaWikiTitleCodec::splitTitleString() in PHP
+       // Note that this is not equivalent to /\s/, e.g. underscore is included, tab is not included.
+       rWhitespace = /[ _\u00A0\u1680\u180E\u2000-\u200A\u2028\u2029\u202F\u205F\u3000]+/g,
+
+       // From MediaWikiTitleCodec::splitTitleString() in PHP
+       rUnicodeBidi = /[\u200E\u200F\u202A-\u202E]/g,
 
        /**
         * Slightly modified from Flinfo. Credit goes to Lupo and Flominator.
                        replace: '',
                        generalRule: true
                },
-               // Space, underscore, tab, NBSP and other unusual spaces
-               {
-                       pattern: rWhitespace,
-                       replace: ' ',
-                       generalRule: true
-               },
-               // unicode bidi override characters: Implicit, Embeds, Overrides
-               {
-                       pattern: /[\u200E\u200F\u202A-\u202E]/g,
-                       replace: '',
-                       generalRule: true
-               },
                // control characters
                {
                        pattern: /[\x00-\x1f\x7f]/g,
                namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace;
 
                title = title
+                       // Strip Unicode bidi override characters
+                       .replace( rUnicodeBidi, '' )
                        // Normalise whitespace to underscores and remove duplicates
-                       .replace( /[ _\s]+/g, '_' )
+                       .replace( rWhitespace, '_' )
                        // Trim underscores
                        .replace( rUnderscoreTrim, '' );
 
 
                namespace = defaultNamespace === undefined ? NS_MAIN : defaultNamespace;
 
-               // Normalise whitespace and remove duplicates
-               title = $.trim( title.replace( rWhitespace, ' ' ) );
+               // Normalise additional whitespace
+               title = $.trim( title.replace( /\s/g, ' ' ) );
 
                // Process initial colon
                if ( title !== '' && title[ 0 ] === ':' ) {
index 78627fc..7bf73b6 100644 (file)
@@ -10,7 +10,8 @@
                $tocList = $toc.find( 'ul' ).eq( 0 );
 
                // Hide/show the table of contents element
-               function toggleToc() {
+               function toggleToc( e ) {
+                       e.preventDefault();
                        if ( $tocList.is( ':hidden' ) ) {
                                $tocList.slideDown( 'fast' );
                                $tocToggleLink.text( mw.msg( 'hidetoc' ) );
                        hideToc = mw.cookie.get( 'hidetoc' ) === '1';
 
                        $tocToggleLink = $( '<a href="#" id="togglelink"></a>' )
-                               .text( hideToc ? mw.msg( 'showtoc' ) : mw.msg( 'hidetoc' ) )
-                               .click( function ( e ) {
-                                       e.preventDefault();
-                                       toggleToc();
-                               } );
+                               .text( mw.msg( hideToc ? 'showtoc' : 'hidetoc' ) )
+                               .click( toggleToc );
 
                        $tocTitle.append(
                                $tocToggleLink
index 7850f24..7925c6f 100644 (file)
@@ -90,6 +90,8 @@ class TitleTest extends MediaWikiTestCase {
                        [ 'A < B', 'title-invalid-characters' ],
                        [ 'A > B', 'title-invalid-characters' ],
                        [ 'A | B', 'title-invalid-characters' ],
+                       [ "A \t B", 'title-invalid-characters' ],
+                       [ "A \n B", 'title-invalid-characters' ],
                        // URL encoding
                        [ 'A%20B', 'title-invalid-characters' ],
                        [ 'A%23B', 'title-invalid-characters' ],
index 5d1ead0..8b75d56 100644 (file)
@@ -43,4 +43,87 @@ class ApiBaseTest extends ApiTestCase {
                );
        }
 
+       /**
+        * @dataProvider provideGetParameterFromSettings
+        * @param string|null $input
+        * @param array $paramSettings
+        * @param mixed $expected
+        * @param string[] $warnings
+        */
+       public function testGetParameterFromSettings( $input, $paramSettings, $expected, $warnings ) {
+               $mock = new MockApi();
+               $wrapper = TestingAccessWrapper::newFromObject( $mock );
+
+               $context = new DerivativeContext( $mock );
+               $context->setRequest( new FauxRequest( $input !== null ? [ 'foo' => $input ] : [] ) );
+               $wrapper->mMainModule = new ApiMain( $context );
+
+               if ( $expected instanceof UsageException ) {
+                       try {
+                               $wrapper->getParameterFromSettings( 'foo', $paramSettings, true );
+                       } catch ( UsageException $ex ) {
+                               $this->assertEquals( $expected, $ex );
+                       }
+               } else {
+                       $result = $wrapper->getParameterFromSettings( 'foo', $paramSettings, true );
+                       $this->assertSame( $expected, $result );
+                       $this->assertSame( $warnings, $mock->warnings );
+               }
+       }
+
+       public static function provideGetParameterFromSettings() {
+               $warnings = [
+                       'The value passed for \'foo\' contains invalid or non-normalized data. Textual data should ' .
+                       'be valid, NFC-normalized Unicode without C0 control characters other than ' .
+                       'HT (\\t), LF (\\n), and CR (\\r).'
+               ];
+
+               $c0 = '';
+               $enc = '';
+               for ( $i = 0; $i < 32; $i++ ) {
+                       $c0 .= chr( $i );
+                       $enc .= ( $i === 9 || $i === 10 || $i === 13 )
+                               ? chr( $i )
+                               : '�';
+               }
+
+               return [
+                       'Basic param' => [ 'bar', null, 'bar', [] ],
+                       'Basic param, C0 controls' => [ $c0, null, $enc, $warnings ],
+                       'String param' => [ 'bar', '', 'bar', [] ],
+                       'String param, defaulted' => [ null, '', '', [] ],
+                       'String param, empty' => [ '', 'default', '', [] ],
+                       'String param, required, empty' => [
+                               '',
+                               [ ApiBase::PARAM_DFLT => 'default', ApiBase::PARAM_REQUIRED => true ],
+                               new UsageException( 'The foo parameter must be set', 'nofoo' ),
+                               []
+                       ],
+                       'Multi-valued parameter' => [
+                               'a|b|c',
+                               [ ApiBase::PARAM_ISMULTI => true ],
+                               [ 'a', 'b', 'c' ],
+                               []
+                       ],
+                       'Multi-valued parameter, alternative separator' => [
+                               "\x1fa|b\x1fc|d",
+                               [ ApiBase::PARAM_ISMULTI => true ],
+                               [ 'a|b', 'c|d' ],
+                               []
+                       ],
+                       'Multi-valued parameter, other C0 controls' => [
+                               $c0,
+                               [ ApiBase::PARAM_ISMULTI => true ],
+                               [ $enc ],
+                               $warnings
+                       ],
+                       'Multi-valued parameter, other C0 controls (2)' => [
+                               "\x1f" . $c0,
+                               [ ApiBase::PARAM_ISMULTI => true ],
+                               [ substr( $enc, 0, -3 ), '' ],
+                               $warnings
+                       ],
+               ];
+       }
+
 }
index 367210a..ad1deee 100644 (file)
@@ -75,4 +75,25 @@ class ApiPageSetTest extends ApiTestCase {
 
                return [ $target, $pageSet ];
        }
+
+       public function testHandleNormalization() {
+               $context = new RequestContext();
+               $context->setRequest( new FauxRequest( [ 'titles' => "a|B|a\xcc\x8a" ] ) );
+               $main = new ApiMain( $context );
+               $pageSet = new ApiPageSet( $main );
+               $pageSet->execute();
+
+               $this->assertSame(
+                       [ 0 => [ 'A' => -1, 'B' => -2, 'Å' => -3 ] ],
+                       $pageSet->getAllTitlesByNamespace()
+               );
+               $this->assertSame(
+                       [
+                               [ 'fromencoded' => true, 'from' => 'a%CC%8A', 'to' => 'å' ],
+                               [ 'fromencoded' => false, 'from' => 'a', 'to' => 'A' ],
+                               [ 'fromencoded' => false, 'from' => 'å', 'to' => 'Å' ],
+                       ],
+                       $pageSet->getNormalizedTitlesAsResult()
+               );
+       }
 }
index 9a64d08..d7db538 100644 (file)
@@ -1,12 +1,18 @@
 <?php
 
 class MockApi extends ApiBase {
+       public $warnings = [];
+
        public function execute() {
        }
 
        public function __construct() {
        }
 
+       public function setWarning( $warning ) {
+               $this->warnings[] = $warning;
+       }
+
        public function getAllowedParams() {
                return [
                        'filename' => null,
index 504b16a..8cb2327 100644 (file)
@@ -43,6 +43,7 @@ class ApiQueryTest extends ApiTestCase {
 
                $this->assertEquals(
                        [
+                               'fromencoded' => false,
                                'from' => 'Project:articleA',
                                'to' => $to->getPrefixedText(),
                        ],
@@ -51,6 +52,7 @@ class ApiQueryTest extends ApiTestCase {
 
                $this->assertEquals(
                        [
+                               'fromencoded' => false,
                                'from' => 'article_B',
                                'to' => 'Article B'
                        ],
index a480671..0797f32 100644 (file)
                assert.deepEqual( stub.getCall( 0 ).args, [ { foo: 'bar' } ], '#saveOptions called correctly' );
        } );
 
-       QUnit.test( 'saveOptions', function ( assert ) {
+       QUnit.test( 'saveOptions without Unit Separator', function ( assert ) {
                QUnit.expect( 13 );
 
-               var api = new mw.Api();
+               var api = new mw.Api( { useUS: false } );
 
                // We need to respond to the request for token first, otherwise the other requests won't be sent
                // until after the server.respond call, which confuses sinon terribly. This sucks a lot.
                        }
                } );
        } );
+
+       QUnit.test( 'saveOptions with Unit Separator', function ( assert ) {
+               QUnit.expect( 14 );
+
+               var api = new mw.Api( { useUS: true } );
+
+               // We need to respond to the request for token first, otherwise the other requests won't be sent
+               // until after the server.respond call, which confuses sinon terribly. This sucks a lot.
+               api.getToken( 'options' );
+               this.server.respond(
+                       /meta=tokens&type=csrf/,
+                       [ 200, { 'Content-Type': 'application/json' },
+                               '{ "query": { "tokens": { "csrftoken": "+\\\\" } } }' ]
+               );
+
+               api.saveOptions( {} ).done( function () {
+                       assert.ok( true, 'Request completed: empty case' );
+               } );
+               api.saveOptions( { foo: 'bar' } ).done( function () {
+                       assert.ok( true, 'Request completed: simple' );
+               } );
+               api.saveOptions( { foo: 'bar', baz: 'quux' } ).done( function () {
+                       assert.ok( true, 'Request completed: two options' );
+               } );
+               api.saveOptions( { foo: 'bar|quux', bar: 'a|b|c', baz: 'quux' } ).done( function () {
+                       assert.ok( true, 'Request completed: bundleable with unit separator' );
+               } );
+               api.saveOptions( { foo: 'bar|quux', bar: 'a|b|c', 'baz=baz': 'quux' } ).done( function () {
+                       assert.ok( true, 'Request completed: not bundleable with unit separator' );
+               } );
+               api.saveOptions( { foo: null } ).done( function () {
+                       assert.ok( true, 'Request completed: reset an option' );
+               } );
+               api.saveOptions( { 'foo|bar=quux': null } ).done( function () {
+                       assert.ok( true, 'Request completed: reset an option, not bundleable' );
+               } );
+
+               // Requests are POST, match requestBody instead of url
+               this.server.respond( function ( request ) {
+                       switch ( request.requestBody ) {
+                               // simple
+                               case 'action=options&format=json&formatversion=2&change=foo%3Dbar&token=%2B%5C':
+                               // two options
+                               case 'action=options&format=json&formatversion=2&change=foo%3Dbar%7Cbaz%3Dquux&token=%2B%5C':
+                               // bundleable with unit separator
+                               case 'action=options&format=json&formatversion=2&change=%1Ffoo%3Dbar%7Cquux%1Fbar%3Da%7Cb%7Cc%1Fbaz%3Dquux&token=%2B%5C':
+                               // not bundleable with unit separator
+                               case 'action=options&format=json&formatversion=2&optionname=baz%3Dbaz&optionvalue=quux&token=%2B%5C':
+                               case 'action=options&format=json&formatversion=2&change=%1Ffoo%3Dbar%7Cquux%1Fbar%3Da%7Cb%7Cc&token=%2B%5C':
+                               // reset an option
+                               case 'action=options&format=json&formatversion=2&change=foo&token=%2B%5C':
+                               // reset an option, not bundleable
+                               case 'action=options&format=json&formatversion=2&optionname=foo%7Cbar%3Dquux&token=%2B%5C':
+                                       assert.ok( true, 'Repond to ' + request.requestBody );
+                                       request.respond( 200, { 'Content-Type': 'application/json' },
+                                               '{ "options": "success" }' );
+                                       break;
+                               default:
+                                       assert.ok( false, 'Unexpected request: ' + request.requestBody );
+                       }
+               } );
+       } );
 }( mediaWiki ) );
index 991725b..886e2b6 100644 (file)
@@ -38,6 +38,8 @@
                        'A < B',
                        'A > B',
                        'A | B',
+                       'A \t B',
+                       'A \n B',
                        // URL encoding
                        'A%20B',
                        'A%23B',
                assert.equal( title.getPrefixedText(), '.foo' );
        } );
 
-       QUnit.test( 'Transformation', 11, function ( assert ) {
+       QUnit.test( 'Transformation', 12, function ( assert ) {
                var title;
 
                title = new mw.Title( 'File:quux pif.jpg' );
                assert.equal( title.toText(), 'User:HAshAr' );
                assert.equal( title.getNamespaceId(), 2, 'Case-insensitive namespace prefix' );
 
-               // Don't ask why, it's the way the backend works. One space is kept of each set.
-               title = new mw.Title( 'Foo  __  \t __ bar' );
+               title = new mw.Title( 'Foo \u00A0\u1680\u180E\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u2028\u2029\u202F\u205F\u3000 bar' );
                assert.equal( title.getMain(), 'Foo_bar', 'Merge multiple types of whitespace/underscores into a single underscore' );
 
+               title = new mw.Title( 'Foo\u200E\u200F\u202A\u202B\u202C\u202D\u202Ebar' );
+               assert.equal( title.getMain(), 'Foobar', 'Strip Unicode bidi override characters' );
+
                // Regression test: Previously it would only detect an extension if there is no space after it
                title = new mw.Title( 'Example.js  ' );
                assert.equal( title.getExtension(), 'js', 'Space after an extension is stripped' );