Merge "Links update class cleanups"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 1 Sep 2016 00:25:09 +0000 (00:25 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 1 Sep 2016 00:25:09 +0000 (00:25 +0000)
79 files changed:
includes/EditPage.php
includes/HttpFunctions.php
includes/MediaWiki.php
includes/MediaWikiServices.php
includes/WebRequest.php
includes/api/ApiQueryAllDeletedRevisions.php
includes/api/i18n/de.json
includes/api/i18n/diq.json
includes/api/i18n/fr.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/sv.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/LBFactory.php
includes/db/loadbalancer/LBFactoryMulti.php
includes/db/loadbalancer/LBFactorySimple.php
includes/db/loadbalancer/LoadBalancer.php
includes/filerepo/ForeignAPIRepo.php
includes/filerepo/file/LocalFile.php
includes/installer/i18n/diq.json
includes/installer/i18n/ne.json
includes/jobqueue/JobRunner.php
includes/libs/objectcache/WANObjectCache.php
includes/objectcache/SqlBagOStuff.php
includes/parser/Parser.php
includes/parser/ParserOutput.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/SpecialEmailuser.php
includes/user/User.php
languages/i18n/ang.json
languages/i18n/ar.json
languages/i18n/be-tarask.json
languages/i18n/bn.json
languages/i18n/diq.json
languages/i18n/dty.json
languages/i18n/eo.json
languages/i18n/eu.json
languages/i18n/gu.json
languages/i18n/io.json
languages/i18n/jv.json
languages/i18n/ka.json
languages/i18n/kk-cyrl.json
languages/i18n/lv.json
languages/i18n/ne.json
languages/i18n/nl.json
languages/i18n/oc.json
languages/i18n/ru.json
languages/i18n/sv.json
languages/i18n/te.json
languages/i18n/ur.json
languages/i18n/vi.json
languages/i18n/vo.json
resources/Resources.php
resources/src/mediawiki/api/messages.js
resources/src/mediawiki/mediawiki.Title.js
resources/src/mediawiki/mediawiki.js
resources/src/mediawiki/mediawiki.toc.js
tests/phpunit/includes/TitleTest.php
tests/phpunit/includes/db/DatabaseTest.php
tests/phpunit/includes/libs/objectcache/WANObjectCacheTest.php
tests/qunit/suites/resources/mediawiki/mediawiki.Title.test.js

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 54b057a..2ca5e1b 100644 (file)
@@ -599,7 +599,7 @@ class MWHttpRequest {
         * Returns the value of the given response header.
         *
         * @param string $header
-        * @return string
+        * @return string|null
         */
        public function getResponseHeader( $header ) {
                if ( !$this->respHeaders ) {
index cded064..1add24a 100644 (file)
@@ -822,16 +822,18 @@ class MediaWiki {
 
                $runJobsLogger = LoggerFactory::getInstance( 'runJobs' );
 
+               // Fall back to running the job(s) while the user waits if needed
                if ( !$this->config->get( 'RunJobsAsync' ) ) {
-                       // Fall back to running the job here while the user waits
                        $runner = new JobRunner( $runJobsLogger );
-                       $runner->run( [ 'maxJobs'  => $n ] );
+                       $runner->run( [ 'maxJobs' => $n ] );
                        return;
                }
 
+               // Do not send request if there are probably no jobs
                try {
-                       if ( !JobQueueGroup::singleton()->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) {
-                               return; // do not send request if there are probably no jobs
+                       $group = JobQueueGroup::singleton();
+                       if ( !$group->queuesHaveJobs( JobQueueGroup::TYPE_DEFAULT ) ) {
+                               return;
                        }
                } catch ( JobQueueError $e ) {
                        MWExceptionHandler::logException( $e );
@@ -845,8 +847,7 @@ class MediaWiki {
 
                $errno = $errstr = null;
                $info = wfParseUrl( $this->config->get( 'CanonicalServer' ) );
-               MediaWiki\suppressWarnings();
-               $host = $info['host'];
+               $host = $info ? $info['host'] : null;
                $port = 80;
                if ( isset( $info['scheme'] ) && $info['scheme'] == 'https' ) {
                        $host = "tls://" . $host;
@@ -855,48 +856,60 @@ class MediaWiki {
                if ( isset( $info['port'] ) ) {
                        $port = $info['port'];
                }
-               $sock = fsockopen(
+
+               MediaWiki\suppressWarnings();
+               $sock = $host ? fsockopen(
                        $host,
                        $port,
                        $errno,
                        $errstr,
-                       // If it takes more than 100ms to connect to ourselves there
-                       // is a problem elsewhere.
-                       0.1
-               );
+                       // If it takes more than 100ms to connect to ourselves there is a problem...
+                       0.100
+               ) : false;
                MediaWiki\restoreWarnings();
-               if ( !$sock ) {
+
+               $invokedWithSuccess = true;
+               if ( $sock ) {
+                       $special = SpecialPageFactory::getPage( 'RunJobs' );
+                       $url = $special->getPageTitle()->getCanonicalURL( $query );
+                       $req = (
+                               "POST $url HTTP/1.1\r\n" .
+                               "Host: {$info['host']}\r\n" .
+                               "Connection: Close\r\n" .
+                               "Content-Length: 0\r\n\r\n"
+                       );
+
+                       $runJobsLogger->info( "Running $n job(s) via '$url'" );
+                       // Send a cron API request to be performed in the background.
+                       // Give up if this takes too long to send (which should be rare).
+                       stream_set_timeout( $sock, 2 );
+                       $bytes = fwrite( $sock, $req );
+                       if ( $bytes !== strlen( $req ) ) {
+                               $invokedWithSuccess = false;
+                               $runJobsLogger->error( "Failed to start cron API (socket write error)" );
+                       } else {
+                               // Do not wait for the response (the script should handle client aborts).
+                               // Make sure that we don't close before that script reaches ignore_user_abort().
+                               $start = microtime( true );
+                               $status = fgets( $sock );
+                               $sec = microtime( true ) - $start;
+                               if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) {
+                                       $invokedWithSuccess = false;
+                                       $runJobsLogger->error( "Failed to start cron API: received '$status' ($sec)" );
+                               }
+                       }
+                       fclose( $sock );
+               } else {
+                       $invokedWithSuccess = false;
                        $runJobsLogger->error( "Failed to start cron API (socket error $errno): $errstr" );
-                       // Fall back to running the job here while the user waits
-                       $runner = new JobRunner( $runJobsLogger );
-                       $runner->run( [ 'maxJobs'  => $n ] );
-                       return;
                }
 
-               $special = SpecialPageFactory::getPage( 'RunJobs' );
-               $url = $special->getPageTitle()->getCanonicalURL( $query );
-               $req = (
-                       "POST $url HTTP/1.1\r\n" .
-                       "Host: {$info['host']}\r\n" .
-                       "Connection: Close\r\n" .
-                       "Content-Length: 0\r\n\r\n"
-               );
+               // Fall back to running the job(s) while the user waits if needed
+               if ( !$invokedWithSuccess ) {
+                       $runJobsLogger->warning( "Jobs switched to blocking; Special:RunJobs disabled" );
 
-               $runJobsLogger->info( "Running $n job(s) via '$url'" );
-               // Send a cron API request to be performed in the background.
-               // Give up if this takes too long to send (which should be rare).
-               stream_set_timeout( $sock, 2 );
-               $bytes = fwrite( $sock, $req );
-               if ( $bytes !== strlen( $req ) ) {
-                       $runJobsLogger->error( "Failed to start cron API (socket write error)" );
-               } else {
-                       // Do not wait for the response (the script should handle client aborts).
-                       // Make sure that we don't close before that script reaches ignore_user_abort().
-                       $status = fgets( $sock );
-                       if ( !preg_match( '#^HTTP/\d\.\d 202 #', $status ) ) {
-                               $runJobsLogger->error( "Failed to start cron API: received '$status'" );
-                       }
+                       $runner = new JobRunner( $runJobsLogger );
+                       $runner->run( [ 'maxJobs'  => $n ] );
                }
-               fclose( $sock );
        }
 }
index 49891df..f621867 100644 (file)
@@ -16,6 +16,7 @@ use MediaWiki\Linker\LinkRenderer;
 use MediaWiki\Linker\LinkRendererFactory;
 use MediaWiki\Services\SalvageableService;
 use MediaWiki\Services\ServiceContainer;
+use MediaWiki\Services\NoSuchServiceException;
 use MWException;
 use ObjectCache;
 use SearchEngine;
@@ -198,7 +199,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 1e27f98..5492737 100644 (file)
@@ -709,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 a559683..3073a95 100644 (file)
@@ -55,6 +55,14 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
 
                $result = $this->getResult();
 
+               // If the user wants no namespaces, they get no pages.
+               if ( $params['namespace'] === [] ) {
+                       if ( $resultPageSet === null ) {
+                               $result->addValue( 'query', $this->getModuleName(), [] );
+                       }
+                       return;
+               }
+
                // This module operates in two modes:
                // 'user': List deleted revs by a certain user
                // 'all': List all deleted revs in NS
@@ -153,10 +161,10 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
                if ( $mode == 'all' ) {
                        if ( $params['namespace'] !== null ) {
                                $namespaces = $params['namespace'];
-                               $this->addWhereFld( 'ar_namespace', $namespaces );
                        } else {
                                $namespaces = MWNamespace::getValidNamespaces();
                        }
+                       $this->addWhereFld( 'ar_namespace', $namespaces );
 
                        // For from/to/prefix, we have to consider the potential
                        // transformations of the title in all specified namespaces.
@@ -447,7 +455,7 @@ class ApiQueryAllDeletedRevisions extends ApiQueryRevisionsBase {
                return [
                        'action=query&list=alldeletedrevisions&adruser=Example&adrlimit=50'
                                => 'apihelp-query+alldeletedrevisions-example-user',
-                       'action=query&list=alldeletedrevisions&adrdir=newer&adrlimit=50'
+                       'action=query&list=alldeletedrevisions&adrdir=newer&adrnamespace=0&adrlimit=50'
                                => 'apihelp-query+alldeletedrevisions-example-ns-main',
                ];
        }
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 4b42964..da74509 100644 (file)
        "api-help-param-deprecated": "Obsolète.",
        "api-help-param-required": "Ce paramètre est obligatoire.",
        "api-help-datatypes-header": "Type de données",
-       "api-help-datatypes": "Certains types de paramètre dans les requêtes de l’API nécessitent plus d’explication :\n;boolean\n:Les paramètres booléens fonctionnent comme des cases à cocher HTML : si le paramètre est spécifié, quelle que soit sa valeur, il est considéré comme vrai. Pour une valeur fausse, enlever complètement le paramètre.\n;timestamp\n:Les horodatages peuvent être spécifiés sous différentes formes. Date et heure ISO 8601 est recommandé. Toutes les heures sont en UTC, tout fuseau horaire inclus est ignoré.\n:* Date et heure ISO 8601, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (la ponctuation et <kbd>Z</kbd> sont facultatifs)\n:* Date et heure ISO 8601 avec fractions de seconde (ignorées), <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> (tirets, deux-points et <kbd>Z</kbd> sont facultatifs)\n:* Format MédiaWiki, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Format numérique générique, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (fuseau horaire facultatif en <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, ou <kbd>-<var>##</var></kbd> sont ignorés)\n:* Format EXIF, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*Format RFC 2822 (le fuseau horaire est facultatif), <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Format RFC 850 (le fuseau horaire est facultatif), <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Format ctime C, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* Secondes depuis 1970-01-01T00:00:00Z sous forme d’entier de 1 à 13 chiffres (sans <kbd>0</kbd>)\n:* La chaîne <kbd>now</kbd>",
+       "api-help-datatypes": "Les entrées dans MédiaWiki doivent être en UTF-8 à la norme NFC. MédiaWiki peut tenter de convertir d’autres types d’entrée, mais cela peut faire échouer certaines opérations (comme les [[Special:ApiHelp/edit|modifications]] avec contrôles MD5) to fail.\n\nCertains types de paramètre dans les requêtes de l’API nécessitent plus d’explication :\n;boolean\n:Les paramètres booléens fonctionnent comme des cases à cocher HTML : si le paramètre est spécifié, quelle que soit sa valeur, il est considéré comme vrai. Pour une valeur fausse, enlever complètement le paramètre.\n;timestamp\n:Les horodatages peuvent être spécifiés sous différentes formes. Date et heure ISO 8601 est recommandé. Toutes les heures sont en UTC, tout fuseau horaire inclus est ignoré.\n:* Date et heure ISO 8601, <kbd><var>2001</var>-<var>01</var>-<var>15</var>T<var>14</var>:<var>56</var>:<var>00</var>Z</kbd> (la ponctuation et <kbd>Z</kbd> sont facultatifs)\n:* Date et heure ISO 8601 avec fractions de seconde (ignorées), <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> (tirets, deux-points et <kbd>Z</kbd> sont facultatifs)\n:* Format MédiaWiki, <kbd><var>2001</var><var>01</var><var>15</var><var>14</var><var>56</var><var>00</var></kbd>\n:* Format numérique générique, <kbd><var>2001</var>-<var>01</var>-<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd> (fuseau horaire facultatif en <kbd>GMT</kbd>, <kbd>+<var>##</var></kbd>, ou <kbd>-<var>##</var></kbd> sont ignorés)\n:* Format EXIF, <kbd><var>2001</var>:<var>01</var>:<var>15</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:*Format RFC 2822 (le fuseau horaire est facultatif), <kbd><var>Mon</var>, <var>15</var> <var>Jan</var> <var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Format RFC 850 (le fuseau horaire est facultatif), <kbd><var>Monday</var>, <var>15</var>-<var>Jan</var>-<var>2001</var> <var>14</var>:<var>56</var>:<var>00</var></kbd>\n:* Format ctime C, <kbd><var>Mon</var> <var>Jan</var> <var>15</var> <var>14</var>:<var>56</var>:<var>00</var> <var>2001</var></kbd>\n:* Secondes depuis 1970-01-01T00:00:00Z sous forme d’entier de 1 à 13 chiffres (sans <kbd>0</kbd>)\n:* La chaîne <kbd>now</kbd>",
        "api-help-param-type-limit": "Type : entier ou <kbd>max</kbd>",
        "api-help-param-type-integer": "Type : {{PLURAL:$1|1=entier|2=liste d’entiers}}",
        "api-help-param-type-boolean": "Type : booléen ([[Special:ApiHelp/main#main/datatypes|détails]])",
        "api-help-param-type-timestamp": "Type : {{PLURAL:$1|1=horodatage|2=liste d’horodatages}} ([[Special:ApiHelp/main#main/datatypes|formats autorisés]])",
        "api-help-param-type-user": "Type : {{PLURAL:$1|1=nom d’utilisateur|2=liste de noms d’utilisateur}}",
-       "api-help-param-list": "{{PLURAL:$1|1=Une des valeurs suivantes|2=Valeurs (séparées par <kbd>{{!}}</kbd>)}} : $2",
+       "api-help-param-list": "{{PLURAL:$1|1=Une des valeurs suivantes|2=Valeurs (séparées par <kbd>{{!}}</kbd> ou [[Special:ApiHelp/main#main/datatypes|autre]])}} : $2",
        "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Doit être vide|Peut être vide, ou $2}}",
        "api-help-param-limit": "Pas plus de $1 autorisé.",
        "api-help-param-limit2": "Pas plus de $1 autorisé ($2 pour les robots).",
        "api-help-param-integer-max": "{{PLURAL:$1|1=La valeur ne doit pas être supérieure|2=Les valeurs ne doivent pas être supérieures}} à $3.",
        "api-help-param-integer-minmax": "{{PLURAL:$1|1=La valeur doit|2=Les valeurs doivent}} être entre $2 et $3.",
        "api-help-param-upload": "Doit être envoyé comme un fichier importé utilisant multipart/form-data.",
-       "api-help-param-multi-separate": "Valeurs séparées par <kbd>|</kbd>.",
+       "api-help-param-multi-separate": "Valeurs séparées par <kbd>|</kbd> ou [[Special:ApiHelp/main#main/datatypes|autre]].",
        "api-help-param-multi-max": "Le nombre maximal de valeurs est {{PLURAL:$1|$1}} ({{PLURAL:$2|$2}} pour les robots).",
        "api-help-param-default": "Par défaut : $1",
        "api-help-param-default-empty": "Par défaut : <span class=\"apihelp-empty\">(vide)</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..bf94fe3 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アドレスを一覧表示します。",
        "api-help-param-deprecated": "廃止予定です。",
        "api-help-param-required": "このパラメーターは必須です。",
        "api-help-datatypes-header": "データ型",
-       "api-help-param-list": "{{PLURAL:$1|1=値 (次の値のいずれか1つ)|2=値 (<kbd>{{!}}</kbd>で区切る)}}: $2",
+       "api-help-param-list": "{{PLURAL:$1|1=å\80¤ (次ã\81®å\80¤ã\81®ã\81\84ã\81\9aã\82\8cã\81\8b\81¤)|2=å\80¤ (<kbd>{{!}}</kbd>ã\82\82ã\81\97ã\81\8fã\81¯[[Special:ApiHelp/main#main/datatypes|å\88¥ã\81®æ\96\87å­\97å\88\97]]ã\81§å\8cºå\88\87ã\82\8b)}}: $2",
        "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=空欄にしてください|空欄にするか、または $2}}",
        "api-help-param-integer-min": "{{PLURAL:$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": "è¤\87æ\95°ã\81®å\80¤ã\81¯ <kbd>|</kbd> ã\82\82ã\81\97ã\81\8fã\81¯[[Special:ApiHelp/main#main/datatypes|å\88¥ã\81®æ\96\87å­\97å\88\97]]ã\81§å\8cºå\88\87ã\81£ã\81¦ã\81\8fã\81 ã\81\95ã\81\84ã\80\82",
        "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 e285130..6e85779 100644 (file)
@@ -51,7 +51,7 @@
        "apihelp-feedrecentchanges-param-tagfilter": "Filtrar per balisa.",
        "apihelp-filerevert-param-comment": "Telecargar lo comentari.",
        "apihelp-filerevert-param-archivename": "Nom d’archiu de la revision de restablir.",
-       "apihelp-import-param-summary": "Importar lo resumit.",
+       "apihelp-import-param-summary": "Resumit de l’importacion de l’entrada de jornal.",
        "apihelp-import-param-xml": "Fichièr XML telecargat.",
        "apihelp-login-param-name": "Nom d'utilizaire.",
        "apihelp-login-param-password": "Senhal.",
@@ -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 3205248..35389b0 100644 (file)
        "api-help-parameters": "{{PLURAL:$1|Parameter|Parametrar}}:",
        "api-help-param-deprecated": "Föråldrad.",
        "api-help-param-required": "Denna parameter är obligatorisk.",
-       "api-help-param-list": "{{PLURAL:$1|1=Ett av följande värden|2=Värden (separerade med <kbd>{{!}}</kbd>)}}: $2",
+       "api-help-param-list": "{{PLURAL:$1|1=Ett av följande värden|2=Värden (separerade med <kbd>{{!}}</kbd> eller [[Special:ApiHelp/main#main/datatypes|alternativ]])}}: $2",
        "api-help-param-list-can-be-empty": "{{PLURAL:$1|0=Måste vara tom|Kan vara tom, eller $2}}",
        "api-help-param-limit": "Inte mer än $1 tillåts.",
-       "api-help-param-limit2": "Inte mer än $1 ($2 för robotar) tillåts."
+       "api-help-param-limit2": "Inte mer än $1 ($2 för robotar) tillåts.",
+       "api-help-param-multi-separate": "Separera värden med <kbd>|</kbd> eller [[Special:ApiHelp/main#main/datatypes|alternativ]]."
 }
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..1019e72 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() );
        }
 
@@ -441,6 +441,10 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
+       public function setTransactionListener( $name, callable $callback = null ) {
+               return $this->__call( __FUNCTION__, func_get_args() );
+       }
+
        public function startAtomic( $fname = __METHOD__ ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
@@ -477,8 +481,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..a864f0f 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 = '';
@@ -69,8 +74,10 @@ abstract class DatabaseBase implements IDatabase {
        protected $mTrxPreCommitCallbacks = [];
        /** @var array[] List of (callable, method name) */
        protected $mTrxEndCallbacks = [];
-       /** @var bool Whether to suppress triggering of post-commit callbacks */
-       protected $suppressPostCommitCallbacks = false;
+       /** @var array[] Map of (name => (callable, method name)) */
+       protected $mTrxRecurringCallbacks = [];
+       /** @var bool Whether to suppress triggering of transaction end callbacks */
+       protected $mTrxEndCallbacksSuppressed = false;
 
        /** @var string */
        protected $mTablePrefix;
@@ -102,7 +109,6 @@ abstract class DatabaseBase implements IDatabase {
         * @var int
         */
        protected $mTrxLevel = 0;
-
        /**
         * Either a short hexidecimal string if a transaction is active or ""
         *
@@ -110,7 +116,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 +125,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 +135,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 +142,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 +149,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 +430,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 +837,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 +859,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 +983,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 +1007,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.
@@ -992,6 +1065,7 @@ abstract class DatabaseBase implements IDatabase {
                try {
                        // Handle callbacks in mTrxEndCallbacks
                        $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+                       $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
                        return null;
                } catch ( Exception $e ) {
                        // Already logged; move on...
@@ -2571,16 +2645,24 @@ abstract class DatabaseBase implements IDatabase {
                }
        }
 
+       final public function setTransactionListener( $name, callable $callback = null ) {
+               if ( $callback ) {
+                       $this->mTrxRecurringCallbacks[$name] = [ $callback, wfGetCaller() ];
+               } else {
+                       unset( $this->mTrxRecurringCallbacks[$name] );
+               }
+       }
+
        /**
-        * Whether to disable running of post-commit callbacks
+        * Whether to disable running of post-COMMIT/ROLLBACK callbacks
         *
         * This method should not be used outside of Database/LoadBalancer
         *
         * @param bool $suppress
         * @since 1.28
         */
-       final public function setPostCommitCallbackSupression( $suppress ) {
-               $this->suppressPostCommitCallbacks = $suppress;
+       final public function setTrxEndCallbackSuppression( $suppress ) {
+               $this->mTrxEndCallbacksSuppressed = $suppress;
        }
 
        /**
@@ -2593,7 +2675,7 @@ abstract class DatabaseBase implements IDatabase {
         * @throws Exception
         */
        public function runOnTransactionIdleCallbacks( $trigger ) {
-               if ( $this->suppressPostCommitCallbacks ) {
+               if ( $this->mTrxEndCallbacksSuppressed ) {
                        return;
                }
 
@@ -2663,6 +2745,38 @@ abstract class DatabaseBase implements IDatabase {
                }
        }
 
+       /**
+        * Actually run any "transaction listener" callbacks.
+        *
+        * This method should not be used outside of Database/LoadBalancer
+        *
+        * @param integer $trigger IDatabase::TRIGGER_* constant
+        * @throws Exception
+        * @since 1.20
+        */
+       public function runTransactionListenerCallbacks( $trigger ) {
+               if ( $this->mTrxEndCallbacksSuppressed ) {
+                       return;
+               }
+
+               /** @var Exception $e */
+               $e = null; // first exception
+
+               foreach ( $this->mTrxRecurringCallbacks as $callback ) {
+                       try {
+                               list( $phpCallback ) = $callback;
+                               $phpCallback( $trigger, $this );
+                       } catch ( Exception $ex ) {
+                               MWExceptionHandler::logException( $ex );
+                               $e = $e ?: $ex;
+                       }
+               }
+
+               if ( $e instanceof Exception ) {
+                       throw $e; // re-throw any first exception
+               }
+       }
+
        final public function startAtomic( $fname = __METHOD__ ) {
                if ( !$this->mTrxLevel ) {
                        $this->begin( $fname, self::TRANSACTION_INTERNAL );
@@ -2739,6 +2853,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
@@ -2801,6 +2918,7 @@ abstract class DatabaseBase implements IDatabase {
                }
 
                $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
+               $this->runTransactionListenerCallbacks( self::TRIGGER_COMMIT );
        }
 
        /**
@@ -2846,6 +2964,7 @@ abstract class DatabaseBase implements IDatabase {
                $this->mTrxIdleCallbacks = []; // clear
                $this->mTrxPreCommitCallbacks = []; // clear
                $this->runOnTransactionIdleCallbacks( self::TRIGGER_ROLLBACK );
+               $this->runTransactionListenerCallbacks( self::TRIGGER_ROLLBACK );
        }
 
        /**
@@ -2863,6 +2982,18 @@ abstract class DatabaseBase implements IDatabase {
                }
        }
 
+       public function clearSnapshot( $fname = __METHOD__ ) {
+               if ( $this->writesOrCallbacksPending() || $this->explicitTrxActive() ) {
+                       // This only flushes transactions to clear snapshots, not to write data
+                       throw new DBUnexpectedError(
+                               $this,
+                               "$fname: Cannot COMMIT to clear snapshot because writes are pending."
+                       );
+               }
+
+               $this->commit( $fname, self::FLUSHING_INTERNAL );
+       }
+
        public function explicitTrxActive() {
                return $this->mTrxLevel && ( $this->mTrxAtomicLevels || !$this->mTrxAutomatic );
        }
@@ -2967,17 +3098,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..5f543c3 100644 (file)
@@ -35,9 +35,9 @@
 interface IDatabase {
        /** @var int Callback triggered immediately due to no active transaction */
        const TRIGGER_IDLE = 1;
-       /** @var int Callback triggered by commit */
+       /** @var int Callback triggered by COMMIT */
        const TRIGGER_COMMIT = 2;
-       /** @var int Callback triggered by rollback */
+       /** @var int Callback triggered by ROLLBACK */
        const TRIGGER_ROLLBACK = 3;
 
        /** @var string Transaction is requested by regular caller outside of the DB layer */
@@ -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.
@@ -200,6 +205,7 @@ interface IDatabase {
        /**
         * Returns true if there is a transaction open with possible write
         * queries or transaction pre-commit/idle callbacks waiting on it to finish.
+        * This does *not* count recurring callbacks, e.g. from setTransactionListener().
         *
         * @return bool
         */
@@ -210,10 +216,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
@@ -1267,7 +1274,7 @@ interface IDatabase {
         * This is useful for combining cooperative locks and DB transactions.
         *
         * The callback takes one argument:
-        * How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_ROLLBACK)
+        *   - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_ROLLBACK)
         *
         * @param callable $callback
         * @return mixed
@@ -1289,7 +1296,7 @@ interface IDatabase {
         * Updates will execute in the order they were enqueued.
         *
         * The callback takes one argument:
-        * How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_IDLE)
+        *   - How the transaction ended (IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_IDLE)
         *
         * @param callable $callback
         * @since 1.20
@@ -1312,6 +1319,23 @@ interface IDatabase {
         */
        public function onTransactionPreCommitOrIdle( callable $callback );
 
+       /**
+        * Run a callback each time any transaction commits or rolls back
+        *
+        * The callback takes two arguments:
+        *   - IDatabase::TRIGGER_COMMIT or IDatabase::TRIGGER_ROLLBACK
+        *   - This IDatabase object
+        * Callbacks must commit any transactions that they begin.
+        *
+        * Registering a callback here will not affect writesOrCallbacks() pending
+        *
+        * @param string $name Callback name
+        * @param callable|null $callback Use null to unset a listener
+        * @return mixed
+        * @since 1.28
+        */
+       public function setTransactionListener( $name, callable $callback = null );
+
        /**
         * Begin an atomic section of statements
         *
@@ -1485,9 +1509,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 dfa4c29..2e6f602 100644 (file)
@@ -44,8 +44,12 @@ abstract class LBFactory implements DestructibleService {
 
        /** @var mixed */
        protected $ticket;
+       /** @var string|bool String if a requested DBO_TRX transaction round is active */
+       protected $trxRoundId = false;
        /** @var string|bool Reason all LBs are read-only or false if not */
        protected $readOnlyReason = false;
+       /** @var callable[] */
+       protected $replicationWaitCallbacks = [];
 
        const SHUTDOWN_NO_CHRONPROT = 1; // don't save ChronologyProtector positions (for async code)
 
@@ -229,9 +233,18 @@ abstract class LBFactory implements DestructibleService {
         * This allows for custom transaction rounds from any outer transaction scope.
         *
         * @param string $fname
+        * @throws DBTransactionError
         * @since 1.28
         */
        public function beginMasterChanges( $fname = __METHOD__ ) {
+               if ( $this->trxRoundId !== false ) {
+                       throw new DBTransactionError(
+                               null,
+                               "Transaction round '{$this->trxRoundId}' already started."
+                       );
+               }
+               $this->trxRoundId = $fname;
+               // Set DBO_TRX flags on all appropriate DBs
                $this->forEachLBCallMethod( 'beginMasterChanges', [ $fname ] );
        }
 
@@ -256,19 +269,20 @@ abstract class LBFactory implements DestructibleService {
         * @throws Exception
         */
        public function commitMasterChanges( $fname = __METHOD__, array $options = [] ) {
-               // Perform all pre-commit callbacks, aborting on failure
-               $this->forEachLBCallMethod( 'runMasterPreCommitCallbacks' );
-               // Perform all pre-commit checks, aborting on failure
+               // Run pre-commit callbacks and suppress post-commit callbacks, aborting on failure
+               $this->forEachLBCallMethod( 'finalizeMasterChanges' );
+               $this->trxRoundId = false;
+               // Perform pre-commit checks, aborting on failure
                $this->forEachLBCallMethod( 'approveMasterChanges', [ $options ] );
                // Log the DBs and methods involved in multi-DB transactions
                $this->logIfMultiDbTransaction();
-               // Actually perform the commit on all master DB connections
+               // Actually perform the commit on all master DB connections and revert DBO_TRX
                $this->forEachLBCallMethod( 'commitMasterChanges', [ $fname ] );
                // Run all post-commit callbacks
                /** @var Exception $e */
                $e = null; // first callback exception
                $this->forEachLB( function ( LoadBalancer $lb ) use ( &$e ) {
-                       $ex = $lb->runMasterPostCommitCallbacks();
+                       $ex = $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_COMMIT );
                        $e = $e ?: $ex;
                } );
                // Commit any dangling DBO_TRX transactions from callbacks on one DB to another DB
@@ -285,7 +299,13 @@ abstract class LBFactory implements DestructibleService {
         * @since 1.23
         */
        public function rollbackMasterChanges( $fname = __METHOD__ ) {
+               $this->trxRoundId = false;
+               $this->forEachLBCallMethod( 'suppressTransactionEndCallbacks' );
                $this->forEachLBCallMethod( 'rollbackMasterChanges', [ $fname ] );
+               // Run all post-rollback callbacks
+               $this->forEachLB( function ( LoadBalancer $lb ) {
+                       $lb->runMasterPostTrxCallbacks( IDatabase::TRIGGER_ROLLBACK );
+               } );
        }
 
        /**
@@ -384,6 +404,10 @@ abstract class LBFactory implements DestructibleService {
                        'ifWritesSince' => null
                ];
 
+               foreach ( $this->replicationWaitCallbacks as $callback ) {
+                       $callback();
+               }
+
                // Figure out which clusters need to be checked
                /** @var LoadBalancer[] $lbs */
                $lbs = [];
@@ -436,6 +460,23 @@ abstract class LBFactory implements DestructibleService {
                }
        }
 
+       /**
+        * Add a callback to be run in every call to waitForReplication() before waiting
+        *
+        * Callbacks must clear any transactions that they start
+        *
+        * @param string $name Callback name
+        * @param callable|null $callback Use null to unset a callback
+        * @since 1.28
+        */
+       public function setWaitForReplicationListener( $name, callable $callback = null ) {
+               if ( $callback ) {
+                       $this->replicationWaitCallbacks[$name] = $callback;
+               } else {
+                       unset( $this->replicationWaitCallbacks[$name] );
+               }
+       }
+
        /**
         * Get a token asserting that no transaction writes are active
         *
@@ -530,6 +571,15 @@ abstract class LBFactory implements DestructibleService {
                } );
        }
 
+       /**
+        * @param LoadBalancer $lb
+        */
+       protected function initLoadBalancer( LoadBalancer $lb ) {
+               if ( $this->trxRoundId !== false ) {
+                       $lb->beginMasterChanges( $this->trxRoundId ); // set DBO_TRX
+               }
+       }
+
        /**
         * Close all open database connections on all open load balancers.
         * @since 1.28
index f201081..c0f6509 100644 (file)
@@ -313,7 +313,7 @@ class LBFactoryMulti extends LBFactory {
         * @return LoadBalancer
         */
        private function newLoadBalancer( $template, $loads, $groupLoads, $readOnlyReason ) {
-               return new LoadBalancer( [
+               $lb = new LoadBalancer( [
                        'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
                        'loadMonitor' => $this->loadMonitorClass,
                        'readOnlyReason' => $readOnlyReason,
@@ -321,6 +321,10 @@ class LBFactoryMulti extends LBFactory {
                        'srvCache' => $this->srvCache,
                        'wanCache' => $this->wanCache
                ] );
+
+               $this->initLoadBalancer( $lb );
+
+               return $lb;
        }
 
        /**
index c7c4752..c06b5b1 100644 (file)
@@ -133,7 +133,7 @@ class LBFactorySimple extends LBFactory {
        }
 
        private function newLoadBalancer( array $servers ) {
-               return new LoadBalancer( [
+               $lb = new LoadBalancer( [
                        'servers' => $servers,
                        'loadMonitor' => $this->loadMonitorClass,
                        'readOnlyReason' => $this->readOnlyReason,
@@ -141,6 +141,10 @@ class LBFactorySimple extends LBFactory {
                        'srvCache' => $this->srvCache,
                        'wanCache' => $this->wanCache
                ] );
+
+               $this->initLoadBalancer( $lb );
+
+               return $lb;
        }
 
        /**
index 65cd3b3..30dbfc5 100644 (file)
@@ -51,6 +51,8 @@ class LoadBalancer {
        private $srvCache;
        /** @var WANObjectCache */
        private $wanCache;
+       /** @var TransactionProfiler */
+       protected $trxProfiler;
 
        /** @var bool|DatabaseBase Database connection that caused a problem */
        private $mErrorConnection;
@@ -68,9 +70,8 @@ class LoadBalancer {
        private $readOnlyReason = false;
        /** @var integer Total connections opened */
        private $connsOpened = 0;
-
-       /** @var TransactionProfiler */
-       protected $trxProfiler;
+       /** @var string|bool String if a requested DBO_TRX transaction round is active */
+       private $trxRoundId = false;
 
        /** @var integer Warn when this many connection are held */
        const CONN_HELD_WARN_THRESHOLD = 10;
@@ -864,6 +865,9 @@ class LoadBalancer {
                        $this->getLazyConnectionRef( DB_MASTER, [], $db->getWikiID() )
                );
                $db->setTransactionProfiler( $this->trxProfiler );
+               if ( $this->trxRoundId !== false ) {
+                       $this->applyTransactionRoundFlags( $db );
+               }
 
                return $db;
        }
@@ -1059,24 +1063,47 @@ class LoadBalancer {
        /**
         * Commit transactions on all open connections
         * @param string $fname Caller name
+        * @throws DBExpectedError
         */
        public function commitAll( $fname = __METHOD__ ) {
-               $this->forEachOpenConnection( function ( DatabaseBase $conn ) use ( $fname ) {
-                       $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
-               } );
+               $failures = [];
+
+               $restore = ( $this->trxRoundId !== false );
+               $this->trxRoundId = false;
+               $this->forEachOpenConnection(
+                       function ( DatabaseBase $conn ) use ( $fname, $restore, &$failures ) {
+                               try {
+                                       $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
+                               } catch ( DBError $e ) {
+                                       MWExceptionHandler::logException( $e );
+                                       $failures[] = "{$conn->getServer()}: {$e->getMessage()}";
+                               }
+                               if ( $restore && $conn->getLBInfo( 'master' ) ) {
+                                       $this->undoTransactionRoundFlags( $conn );
+                               }
+                       }
+               );
+
+               if ( $failures ) {
+                       throw new DBExpectedError(
+                               null,
+                               "Commit failed on server(s) " . implode( "\n", array_unique( $failures ) )
+                       );
+               }
        }
 
        /**
         * Perform all pre-commit callbacks that remain part of the atomic transactions
-        * and disable any post-commit callbacks until runMasterPostCommitCallbacks()
+        * and disable any post-commit callbacks until runMasterPostTrxCallbacks()
         * @since 1.28
         */
-       public function runMasterPreCommitCallbacks() {
+       public function finalizeMasterChanges() {
                $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
-                       // Any error will cause all DB transactions to be rolled back together.
+                       // Any error should cause all DB transactions to be rolled back together
+                       $conn->setTrxEndCallbackSuppression( false );
                        $conn->runOnTransactionPreCommitCallbacks();
-                       // Defer post-commit callbacks until COMMIT finishes for all DBs.
-                       $conn->setPostCommitCallbackSupression( true );
+                       // Defer post-commit callbacks until COMMIT finishes for all DBs
+                       $conn->setTrxEndCallbackSuppression( true );
                } );
        }
 
@@ -1101,7 +1128,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,
@@ -1129,55 +1156,96 @@ class LoadBalancer {
         * This allows for custom transaction rounds from any outer transaction scope.
         *
         * @param string $fname
+        * @throws DBExpectedError
         * @since 1.28
         */
        public function beginMasterChanges( $fname = __METHOD__ ) {
-               $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $fname ) {
-                       if ( $conn->writesOrCallbacksPending() ) {
-                               throw new DBTransactionError(
-                                       $conn,
-                                       "Transaction with pending writes still active."
-                               );
-                       } elseif ( $conn->trxLevel() ) {
-                               $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
-                       }
-                       if ( $conn->getFlag( DBO_DEFAULT ) ) {
-                               // DBO_TRX is controlled entirely by CLI mode presence with DBO_DEFAULT.
-                               // Force DBO_TRX even in CLI mode since a commit round is expected soon.
-                               $conn->setFlag( DBO_TRX, $conn::REMEMBER_PRIOR );
-                               $conn->onTransactionResolution( function () use ( $conn ) {
-                                       $conn->restoreFlags( $conn::RESTORE_PRIOR );
-                               } );
-                       } else {
-                               // Config has explicitly requested DBO_TRX be either on or off; respect that.
-                               // This is useful for things like blob stores which use auto-commit mode.
+               if ( $this->trxRoundId !== false ) {
+                       throw new DBTransactionError(
+                               null,
+                               "$fname: Transaction round '{$this->trxRoundId}' already started."
+                       );
+               }
+               $this->trxRoundId = $fname;
+
+               $failures = [];
+               $this->forEachOpenMasterConnection(
+                       function ( DatabaseBase $conn ) use ( $fname, &$failures ) {
+                               $conn->setTrxEndCallbackSuppression( true );
+                               try {
+                                       $conn->clearSnapshot( $fname );
+                               } catch ( DBError $e ) {
+                                       MWExceptionHandler::logException( $e );
+                                       $failures[] = "{$conn->getServer()}: {$e->getMessage()}";
+                               }
+                               $conn->setTrxEndCallbackSuppression( false );
+                               $this->applyTransactionRoundFlags( $conn );
                        }
-               } );
+               );
+
+               if ( $failures ) {
+                       throw new DBExpectedError(
+                               null,
+                               "$fname: Flush failed on server(s) " . implode( "\n", array_unique( $failures ) )
+                       );
+               }
        }
 
        /**
         * Issue COMMIT on all master connections where writes where done
         * @param string $fname Caller name
+        * @throws DBExpectedError
         */
        public function commitMasterChanges( $fname = __METHOD__ ) {
-               $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $fname ) {
-                       if ( $conn->writesOrCallbacksPending() ) {
-                               $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
+               $failures = [];
+
+               $restore = ( $this->trxRoundId !== false );
+               $this->trxRoundId = false;
+               $this->forEachOpenMasterConnection(
+                       function ( DatabaseBase $conn ) use ( $fname, $restore, &$failures ) {
+                               try {
+                                       if ( $conn->writesOrCallbacksPending() ) {
+                                               $conn->commit( $fname, $conn::FLUSHING_ALL_PEERS );
+                                       } elseif ( $restore ) {
+                                               $conn->clearSnapshot( $fname );
+                                       }
+                               } catch ( DBError $e ) {
+                                       MWExceptionHandler::logException( $e );
+                                       $failures[] = "{$conn->getServer()}: {$e->getMessage()}";
+                               }
+                               if ( $restore ) {
+                                       $this->undoTransactionRoundFlags( $conn );
+                               }
                        }
-               } );
+               );
+
+               if ( $failures ) {
+                       throw new DBExpectedError(
+                               null,
+                               "$fname: Commit failed on server(s) " . implode( "\n", array_unique( $failures ) )
+                       );
+               }
        }
 
        /**
-        * Issue all pending post-commit callbacks
+        * Issue all pending post-COMMIT/ROLLBACK callbacks
+        * @param integer $type IDatabase::TRIGGER_* constant
         * @return Exception|null The first exception or null if there were none
         * @since 1.28
         */
-       public function runMasterPostCommitCallbacks() {
+       public function runMasterPostTrxCallbacks( $type ) {
                $e = null; // first exception
-               $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( &$e ) {
-                       $conn->setPostCommitCallbackSupression( false );
+               $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) use ( $type, &$e ) {
+                       $conn->clearSnapshot( __METHOD__ ); // clear no-op transactions
+
+                       $conn->setTrxEndCallbackSuppression( false );
+                       try {
+                               $conn->runOnTransactionIdleCallbacks( $type );
+                       } catch ( Exception $ex ) {
+                               $e = $e ?: $ex;
+                       }
                        try {
-                               $conn->runOnTransactionIdleCallbacks( $conn::TRIGGER_COMMIT );
+                               $conn->runTransactionListenerCallbacks( $type );
                        } catch ( Exception $ex ) {
                                $e = $e ?: $ex;
                        }
@@ -1193,29 +1261,51 @@ class LoadBalancer {
         * @since 1.23
         */
        public function rollbackMasterChanges( $fname = __METHOD__ ) {
-               $failedServers = [];
-
-               $masterIndex = $this->getWriterIndex();
-               foreach ( $this->mConns as $conns2 ) {
-                       if ( empty( $conns2[$masterIndex] ) ) {
-                               continue;
-                       }
-                       /** @var DatabaseBase $conn */
-                       foreach ( $conns2[$masterIndex] as $conn ) {
-                               if ( $conn->trxLevel() && $conn->writesOrCallbacksPending() ) {
-                                       try {
-                                               $conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
-                                       } catch ( DBError $e ) {
-                                               MWExceptionHandler::logException( $e );
-                                               $failedServers[] = $conn->getServer();
-                                       }
+               $restore = ( $this->trxRoundId !== false );
+               $this->trxRoundId = false;
+               $this->forEachOpenMasterConnection(
+                       function ( DatabaseBase $conn ) use ( $fname, $restore ) {
+                               if ( $conn->writesOrCallbacksPending() ) {
+                                       $conn->rollback( $fname, $conn::FLUSHING_ALL_PEERS );
+                               }
+                               if ( $restore ) {
+                                       $this->undoTransactionRoundFlags( $conn );
                                }
                        }
+               );
+       }
+
+       /**
+        * Suppress all pending post-COMMIT/ROLLBACK callbacks
+        * @return Exception|null The first exception or null if there were none
+        * @since 1.28
+        */
+       public function suppressTransactionEndCallbacks() {
+               $this->forEachOpenMasterConnection( function ( DatabaseBase $conn ) {
+                       $conn->setTrxEndCallbackSuppression( true );
+               } );
+       }
+
+       /**
+        * @param DatabaseBase $conn
+        */
+       private function applyTransactionRoundFlags( DatabaseBase $conn ) {
+               if ( $conn->getFlag( DBO_DEFAULT ) ) {
+                       // DBO_TRX is controlled entirely by CLI mode presence with DBO_DEFAULT.
+                       // Force DBO_TRX even in CLI mode since a commit round is expected soon.
+                       $conn->setFlag( DBO_TRX, $conn::REMEMBER_PRIOR );
+                       // If config has explicitly requested DBO_TRX be either on or off by not
+                       // setting DBO_DEFAULT, then respect that. Forcing no transactions is useful
+                       // for things like blob stores (ExternalStore) which want auto-commit mode.
                }
+       }
 
-               if ( $failedServers ) {
-                       throw new DBExpectedError( null, "Rollback failed on server(s) " .
-                               implode( ', ', array_unique( $failedServers ) ) );
+       /**
+        * @param DatabaseBase $conn
+        */
+       private function undoTransactionRoundFlags( DatabaseBase $conn ) {
+               if ( $conn->getFlag( DBO_DEFAULT ) ) {
+                       $conn->restoreFlags( $conn::RESTORE_PRIOR );
                }
        }
 
index b48191f..8619ba6 100644 (file)
@@ -63,8 +63,8 @@ class ForeignAPIRepo extends FileRepo {
        /** @var array */
        protected $mFileExists = [];
 
-       /** @var array */
-       private $mQueryCache = [];
+       /** @var string */
+       private $mApiBase;
 
        /**
         * @param array|null $info
@@ -397,7 +397,8 @@ class ForeignAPIRepo extends FileRepo {
                        }
                        /* There is a new Commons file, or existing thumbnail older than a month */
                }
-               $thumb = self::httpGet( $foreignUrl );
+
+               $thumb = self::httpGet( $foreignUrl, 'default', [], $mtime );
                if ( !$thumb ) {
                        wfDebug( __METHOD__ . " Could not download thumb\n" );
 
@@ -413,7 +414,11 @@ class ForeignAPIRepo extends FileRepo {
                        return $foreignUrl;
                }
                $knownThumbUrls[$sizekey] = $localUrl;
-               $cache->set( $key, $knownThumbUrls, $this->apiThumbCacheExpiry );
+
+               $ttl = $mtime
+                       ? $cache->adaptiveTTL( $mtime, $this->apiThumbCacheExpiry )
+                       : $this->apiThumbCacheExpiry;
+               $cache->set( $key, $knownThumbUrls, $ttl );
                wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache \n" );
 
                return $localUrl;
@@ -506,9 +511,12 @@ class ForeignAPIRepo extends FileRepo {
         * @param string $url
         * @param string $timeout
         * @param array $options
+        * @param integer|bool &$mtime Resulting Last-Modified UNIX timestamp if received
         * @return bool|string
         */
-       public static function httpGet( $url, $timeout = 'default', $options = [] ) {
+       public static function httpGet(
+               $url, $timeout = 'default', $options = [], &$mtime = false
+       ) {
                $options['timeout'] = $timeout;
                /* Http::get */
                $url = wfExpandUrl( $url, PROTO_HTTP );
@@ -524,6 +532,9 @@ class ForeignAPIRepo extends FileRepo {
                $status = $req->execute();
 
                if ( $status->isOK() ) {
+                       $mtime = wfTimestampOrNull( TS_UNIX, $req->getResponseHeader( 'Last-Modified' ) );
+                       $mtime = $mtime ?: false;
+
                        return $req->getContent();
                } else {
                        $logger = LoggerFactory::getInstance( 'http' );
@@ -531,6 +542,7 @@ class ForeignAPIRepo extends FileRepo {
                                $status->getWikiText( false, false, 'en' ),
                                [ 'caller' => 'ForeignAPIRepo::httpGet' ]
                        );
+
                        return false;
                }
        }
@@ -548,7 +560,7 @@ class ForeignAPIRepo extends FileRepo {
         * @param string $target Used in cache key creation, mostly
         * @param array $query The query parameters for the API request
         * @param int $cacheTTL Time to live for the memcached caching
-        * @return null
+        * @return string|null
         */
        public function httpGetCached( $target, $query, $cacheTTL = 3600 ) {
                if ( $this->mApiBase ) {
@@ -557,28 +569,23 @@ class ForeignAPIRepo extends FileRepo {
                        $url = $this->makeUrl( $query, 'api' );
                }
 
-               if ( !isset( $this->mQueryCache[$url] ) ) {
-                       $data = ObjectCache::getMainWANInstance()->getWithSetCallback(
-                               $this->getLocalCacheKey( get_class( $this ), $target, md5( $url ) ),
-                               $cacheTTL,
-                               function () use ( $url ) {
-                                       return ForeignAPIRepo::httpGet( $url );
+               $cache = ObjectCache::getMainWANInstance();
+               return $cache->getWithSetCallback(
+                       $this->getLocalCacheKey( get_class( $this ), $target, md5( $url ) ),
+                       $cacheTTL,
+                       function ( $curValue, &$ttl ) use ( $url, $cache ) {
+                               $html = self::httpGet( $url, 'default', [], $mtime );
+                               if ( $html !== false ) {
+                                       $ttl = $mtime ? $cache->adaptiveTTL( $mtime, $ttl ) : $ttl;
+                               } else {
+                                       $ttl = $cache->adaptiveTTL( $mtime, $ttl );
+                                       $html = null; // caches negatives
                                }
-                       );
 
-                       if ( !$data ) {
-                               return null;
-                       }
-
-                       if ( count( $this->mQueryCache ) > 100 ) {
-                               // Keep the cache from growing infinitely
-                               $this->mQueryCache = [];
-                       }
-
-                       $this->mQueryCache[$url] = $data;
-               }
-
-               return $this->mQueryCache[$url];
+                               return $html;
+                       },
+                       [ 'pcTTL' => $cache::TTL_PROC_LONG ]
+               );
        }
 
        /**
index 40141c9..8d25726 100644 (file)
@@ -279,8 +279,9 @@ class LocalFile extends File {
 
        /**
         * Save the file metadata to memcached
+        * @param array $cacheSetOpts Result of Database::getCacheSetOptions()
         */
-       private function saveToCache() {
+       private function saveToCache( array $cacheSetOpts ) {
                $this->load();
 
                $key = $this->getCacheKey();
@@ -308,9 +309,14 @@ class LocalFile extends File {
                }
 
                // Cache presence for 1 week and negatives for 1 day
-               $ttl = $this->fileExists ? 86400 * 7 : 86400;
-               $opts = Database::getCacheSetOptions( $this->repo->getSlaveDB() );
-               ObjectCache::getMainWANInstance()->set( $key, $cacheVal, $ttl, $opts );
+               $wanCache = ObjectCache::getMainWANInstance();
+               if ( $this->fileExists ) {
+                       $ttl = $wanCache::TTL_WEEK;
+                       $ttl = $wanCache->adaptiveTTL( wfTimestamp( TS_UNIX, $this->timestamp ), $ttl );
+               } else {
+                       $ttl = $wanCache::TTL_DAY;
+               }
+               $wanCache->set( $key, $cacheVal, $ttl, $cacheSetOpts );
        }
 
        /**
@@ -546,8 +552,9 @@ class LocalFile extends File {
        function load( $flags = 0 ) {
                if ( !$this->dataLoaded ) {
                        if ( ( $flags & self::READ_LATEST ) || !$this->loadFromCache() ) {
+                               $opts = Database::getCacheSetOptions( $this->repo->getSlaveDB() );
                                $this->loadFromDB( $flags );
-                               $this->saveToCache();
+                               $this->saveToCache( $opts );
                        }
                        $this->dataLoaded = true;
                }
index c9ebe32..6fa270a 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": "Bar ke",
        "config-page-complete": "Temamyayo",
        "config-page-restart": "Barkerdışi fına ser kı",
        "config-page-readme": "Mı bıwane",
index fca2977..7c50e82 100644 (file)
@@ -6,13 +6,14 @@
                        "सरोज कुमार ढकाल",
                        "Ganesh Paudel",
                        "बिप्लब आनन्द",
-                       "Nirjal stha"
+                       "Nirjal stha",
+                       "राम प्रसाद जोशी"
                ]
        },
        "config-desc": "मेडियाविकिको लागि स्थापक",
        "config-title": "मेडिया विकि $1 स्थापना",
        "config-information": "जानकारी",
-       "config-localsettings-badkey": "तपाà¤\87लà¥\87 à¤¦à¤¿à¤¨à¥\81 à¤­à¤\8fà¤\95à¥\8b à¤\95à¥\81नà¥\8dà¤\9cà¥\80 à¤\97लत à¤\9b ।",
+       "config-localsettings-badkey": "तपाà¤\88à¤\82लà¥\87 à¤¦à¤¿à¤\8fà¤\95à¥\8b à¤\95à¥\81à¤\82à¤\9cà¥\80 à¤®à¤¿à¤²à¥\87न ।",
        "config-your-language": "तपाईंको भाषा:",
        "config-your-language-help": "इन्स्टल गर्दा उपयोग गर्ने भाषा छान्नुहोस् ।",
        "config-wiki-language": "विकि भाषाहरू",
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 c40c819..0d7da91 100644 (file)
@@ -1042,6 +1042,43 @@ class WANObjectCache implements IExpiringStore, LoggerAwareInterface {
                return $this->cache->getQoS( $flag );
        }
 
+       /**
+        * Get a TTL that is higher for objects that have not changed recently
+        *
+        * This is useful for keys that get explicit purges and DB or purge relay
+        * lag is a potential concern (especially how it interacts with CDN cache)
+        *
+        * Example usage:
+        * @code
+        *     // Last-modified time of page
+        *     $mtime = wfTimestamp( TS_UNIX, $page->getTimestamp() );
+        *     // Get adjusted TTL. If $mtime is 3600 seconds ago and $minTTL/$factor left at
+        *     // defaults, then $ttl is 3600 * .2 = 720. If $minTTL was greater than 720, then
+        *     // $ttl would be $minTTL. If $maxTTL was smaller than 720, $ttl would be $maxTTL.
+        *     $ttl = $cache->adaptiveTTL( $mtime, $cache::TTL_DAY );
+        * @endcode
+        *
+        * @param integer|float $mtime UNIX timestamp
+        * @param integer $maxTTL Maximum TTL (seconds)
+        * @param integer $minTTL Minimum TTL (seconds); Default: 30
+        * @param float $factor Value in the range (0,1); Default: .2
+        * @return integer Adaptive TTL
+        * @since 1.28
+        */
+       public function adaptiveTTL( $mtime, $maxTTL, $minTTL = 30, $factor = .2 ) {
+               if ( is_float( $mtime ) ) {
+                       $mtime = (int)$mtime; // ignore fractional seconds
+               }
+
+               if ( !is_int( $mtime ) || $mtime <= 0 ) {
+                       return $minTTL; // no last-modified time provided
+               }
+
+               $age = time() - $mtime;
+
+               return (int)min( $maxTTL, max( $minTTL, $factor * $age ) );
+       }
+
        /**
         * Do the actual async bus purge of a key
         *
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 035baac..b4d9c70 100644 (file)
@@ -395,7 +395,8 @@ class Parser {
         * @param int $revid Number to pass in {{REVISIONID}}
         * @return ParserOutput A ParserOutput
         */
-       public function parse( $text, Title $title, ParserOptions $options,
+       public function parse(
+               $text, Title $title, ParserOptions $options,
                $linestart = true, $clearState = true, $revid = null
        ) {
                /**
@@ -462,6 +463,10 @@ class Parser {
                        }
                }
 
+               # Done parsing! Compute runtime adaptive expiry if set
+               $this->mOutput->finalizeAdaptiveCacheExpiry();
+
+               # Warn if too many heavyweight parser functions were used
                if ( $this->mExpensiveFunctionCount > $this->mOptions->getExpensiveParserFunctionLimit() ) {
                        $this->limitationWarn( 'expensive-parserfunction',
                                $this->mExpensiveFunctionCount,
@@ -3144,14 +3149,17 @@ class Parser {
                                                $context->setUser( User::newFromName( '127.0.0.1', false ) );
                                        }
                                        $context->setLanguage( $this->mOptions->getUserLangObj() );
-                                       $ret = SpecialPageFactory::capturePath( $title, $context, $this->getLinkRenderer() );
+                                       $ret = SpecialPageFactory::capturePath(
+                                               $title, $context, $this->getLinkRenderer() );
                                        if ( $ret ) {
                                                $text = $context->getOutput()->getHTML();
                                                $this->mOutput->addOutputPageMetadata( $context->getOutput() );
                                                $found = true;
                                                $isHTML = true;
                                                if ( $specialPage && $specialPage->maxIncludeCacheTime() !== false ) {
-                                                       $this->mOutput->updateCacheExpiry( $specialPage->maxIncludeCacheTime() );
+                                                       $this->mOutput->updateRuntimeAdaptiveExpiry(
+                                                               $specialPage->maxIncludeCacheTime()
+                                                       );
                                                }
                                        }
                                } elseif ( MWNamespace::isNonincludable( $title->getNamespace() ) ) {
index f052812..9dfa97c 100644 (file)
@@ -209,9 +209,21 @@ class ParserOutput extends CacheTime {
        /** @var integer|null Assumed rev ID for {{REVISIONID}} if no revision is set */
        private $mSpeculativeRevId;
 
+       /** @var integer Upper bound of expiry based on parse duration */
+       private $mMaxAdaptiveExpiry = INF;
+
        const EDITSECTION_REGEX =
                '#<(?:mw:)?editsection page="(.*?)" section="(.*?)"(?:/>|>(.*?)(</(?:mw:)?editsection>))#';
 
+       // finalizeAdaptiveCacheExpiry() uses TTL = MAX( m * PARSE_TIME + b, MIN_AR_TTL)
+       // Current values imply that m=3933.333333 and b=-333.333333
+       // See https://www.nngroup.com/articles/website-response-times/
+       const PARSE_FAST_SEC = .100; // perceived "fast" page parse
+       const PARSE_SLOW_SEC = 1.0; // perceived "slow" page parse
+       const FAST_AR_TTL = 60; // adaptive TTL for "fast" pages
+       const SLOW_AR_TTL = 3600; // adaptive TTL for "slow" pages
+       const MIN_AR_TTL = 15; // min adaptive TTL (for sanity, pool counter, and edit stashing)
+
        public function __construct( $text = '', $languageLinks = [], $categoryLinks = [],
                $unused = false, $titletext = ''
        ) {
@@ -1037,9 +1049,41 @@ class ParserOutput extends CacheTime {
        }
 
        /**
-        * Save space for serialization by removing useless values
-        * @return array
+        * Lower the runtime adaptive TTL to at most this value
+        *
+        * @param integer $ttl
+        * @since 1.28
+        */
+       public function updateRuntimeAdaptiveExpiry( $ttl ) {
+               $this->mMaxAdaptiveExpiry = min( $ttl, $this->mMaxAdaptiveExpiry );
+               $this->updateCacheExpiry( $ttl );
+       }
+
+       /**
+        * Call this when parsing is done to lower the TTL based on low parse times
+        *
+        * @since 1.28
         */
+       public function finalizeAdaptiveCacheExpiry() {
+               if ( is_infinite( $this->mMaxAdaptiveExpiry ) ) {
+                       return; // not set
+               }
+
+               $runtime = $this->getTimeSinceStart( 'wall' );
+               if ( is_float( $runtime ) ) {
+                       $slope = ( self::SLOW_AR_TTL - self::FAST_AR_TTL )
+                               / ( self::PARSE_SLOW_SEC - self::PARSE_FAST_SEC );
+                       // SLOW_AR_TTL = PARSE_SLOW_SEC * $slope + $point
+                       $point = self::SLOW_AR_TTL - self::PARSE_SLOW_SEC * $slope;
+
+                       $adaptiveTTL = min(
+                               max( $slope * $runtime + $point, self::MIN_AR_TTL ),
+                               $this->mMaxAdaptiveExpiry
+                       );
+                       $this->updateCacheExpiry( $adaptiveTTL );
+               }
+       }
+
        public function __sleep() {
                return array_diff(
                        array_keys( get_object_vars( $this ) ),
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 fb1943f..06be7bc 100644 (file)
@@ -388,13 +388,29 @@ class SpecialEmailUser extends UnlistedSpecialPage {
                        // unless they are emailing themselves, in which case one
                        // copy of the message is sufficient.
                        if ( $data['CCMe'] && $to != $from ) {
-                               $cc_subject = $context->msg( 'emailccsubject' )->rawParams(
+                               $ccTo = $from;
+                               $ccFrom = $from;
+                               $ccSubject = $context->msg( 'emailccsubject' )->rawParams(
                                        $target->getName(), $subject )->text();
-
-                               // target and sender are equal, because this is the CC for the sender
-                               Hooks::run( 'EmailUserCC', [ &$from, &$from, &$cc_subject, &$text ] );
-
-                               $ccStatus = UserMailer::send( $from, $from, $cc_subject, $text );
+                               $ccText = $text;
+
+                               Hooks::run( 'EmailUserCC', [ &$ccTo, &$ccFrom, &$ccSubject, &$ccText ] );
+
+                               if ( $config->get( 'UserEmailUseReplyTo' ) ) {
+                                       $mailFrom = new MailAddress(
+                                               $config->get( 'PasswordSender' ),
+                                               wfMessage( 'emailsender' )->inContentLanguage()->text()
+                                       );
+                                       $replyTo = $ccFrom;
+                               } else {
+                                       $mailFrom = $ccFrom;
+                                       $replyTo = null;
+                               }
+
+                               $ccStatus = UserMailer::send(
+                                       $ccTo, $mailFrom, $ccSubject, $ccText, [
+                                               'replyTo' => $replyTo,
+                               ] );
                                $status->merge( $ccStatus );
                        }
 
index a9ccc4e..4ec8d54 100644 (file)
@@ -473,7 +473,7 @@ class User implements IDBAccessObject {
                $data = $cache->getWithSetCallback(
                        $this->getCacheKey( $cache ),
                        $cache::TTL_HOUR,
-                       function ( $oldValue, &$ttl, array &$setOpts ) {
+                       function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
                                $setOpts += Database::getCacheSetOptions( wfGetDB( DB_SLAVE ) );
                                wfDebug( "User: cache miss for user {$this->mId}\n" );
 
@@ -486,6 +486,8 @@ class User implements IDBAccessObject {
                                        $data[$name] = $this->$name;
                                }
 
+                               $ttl = $cache->adaptiveTTL( wfTimestamp( TS_UNIX, $this->mTouched ), $ttl );
+
                                return $data;
 
                        },
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..fdbce6e 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": "كل السجلات العامة",
        "exif-compression-34712": "جيه بي إي جي2000",
        "exif-copyrighted-true": "محفوظ الحقوق",
        "exif-copyrighted-false": "حالة حقوق النشر غير مُعرّفة",
+       "exif-photometricinterpretation-0": "أسود وأبيض (الأبيض هو 0)",
        "exif-photometricinterpretation-1": "أسود وأبيض (الأسود 0)",
        "exif-photometricinterpretation-2": "آر جي بي",
+       "exif-photometricinterpretation-3": "لوح الألوان",
+       "exif-photometricinterpretation-4": "قناع الشفافية",
+       "exif-photometricinterpretation-5": "مفصول (ربما CMYK)",
        "exif-photometricinterpretation-6": "واي سب سر",
+       "exif-photometricinterpretation-32803": "مصفوفة فلترة الألوان",
+       "exif-photometricinterpretation-34892": "خام خطي",
        "exif-unknowndate": "تاريخ غير معروف",
        "exif-orientation-1": "عادي",
        "exif-orientation-2": "مقلوبة أفقياً",
        "autoredircomment": "تحويل إلى [[$1]]",
        "autosumm-new": "أنشأ الصفحة ب'$1'",
        "autosumm-newblank": "أنشأ صفحة فارغة",
-       "size-bytes": "$1 بايت",
+       "size-bytes": "$1 {{PLURAL:$1|بايت}}",
        "size-kilobytes": "$1 كيلوبايت",
        "size-megabytes": "$1 ميجابايت",
        "size-gigabytes": "$1 جيجابايت",
        "size-exabytes": "$1 إكسابايت",
        "size-zetabytes": "$1 زيتابايت",
        "size-yottabytes": "$1 يوتابايت",
+       "size-pixel": "$1 {{PLURAL:$1|بكسل}}",
        "bitrate-bits": "$1بيت لكل ثانية",
        "bitrate-kilobits": "$1كيلوبيت لكل ثانية",
        "bitrate-megabits": "$1ميجابيت لكل ثانية",
index 1dfc717..5b295a5 100644 (file)
        "timezone-local": "Мясцовы",
        "duplicate-defaultsort": "Папярэджаньне: Ключ сартыроўкі па змоўчваньні «$2» замяняе папярэдні ключ сартыроўкі па змоўчваньні «$1».",
        "duplicate-displaytitle": "<strong>Папярэджаньне:</strong> назва для адлюстраваньня «$2» перапісвае ранейшую назву для адлюстраваньня «$1».",
+       "restricted-displaytitle": "<strong>Увага:</strong> назва для адлюстраваньня «$1» была праігнараваная, бо яна не супадае зь цяперашняй назвай старонкі.",
        "invalid-indicator-name": "<strong>Памылка:</strong> атрыбут <code>name</code> індыкатараў статусу старонкі ня мусіць быць пустым.",
        "version": "Вэрсія",
        "version-extensions": "Усталяваныя пашырэньні",
        "api-error-nomodule": "Унутраная памылка: ня выбраны модуль загрузкі.",
        "api-error-ok-but-empty": "Унутраная памылка: няма адказу ад сэрвэра.",
        "api-error-overwrite": "Замена існуючага файла забароненая.",
+       "api-error-ratelimited": "Вы спрабуеце загрузіць за кароткі час болей файлаў, чым дазваляе вікі.\nКалі ласка, паспрабуйце яшчэ раз празь некалькі хвілінаў.",
        "api-error-stashfailed": "Унутраная памылка: сэрвэр ня змог захаваць часовы файл.",
        "api-error-publishfailed": "Унутраная памылка: сэрвэр ня змог захаваць часловы файл.",
        "api-error-stasherror": "Падчас загрузкі файла ў сховішча адбылася памылка.",
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..73371df 100644 (file)
        "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-user-pages": "Pelê karberi",
        "prefs-personal": "Pela karberi",
        "prefs-rc": "Vurriyayışê peyêni",
        "prefs-watchlist": "Lista seyrkerdışi",
        "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",
-       "rcshowhideminor-show": "Bıasne",
+       "rcshowhideminor-show": "Bımocne",
        "rcshowhideminor-hide": "Bınımne",
        "rcshowhidebots": "botan $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",
        "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",
        "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": "Barkerdışi",
        "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..3415e29 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": "लुकाइएका श्रेणीहरू धेखाउन्या",
        "updated": "नौला",
        "note": "'''सूचना:'''",
        "continue-editing": "सम्पादन क्षेत्रमी जाओ",
-       "editing": "$1 à¤¸à¤®à¥\8dपादन à¤\97रिà¤\81दà¥\88",
+       "editing": "$1 सम्पादन गरिदै",
        "creating": "$1 बनाइँदै",
-       "editingsection": "$1 (à¤\96णà¥\8dड) à¤¸à¤®à¥\8dपादन à¤\97रिà¤\81दà¥\88",
+       "editingsection": "$1 (खण्ड) सम्पादन गरिदै",
        "editingcomment": "$1 सम्पादन गर्दै(नयाँ खण्ड)",
        "editconflict": "सम्पादन बाँझ्यो: $1",
        "yourtext": "तमरा पाठहरू",
        "whatlinkshere-links": "← लिंकहरू",
        "whatlinkshere-hideredirs": "$1 अनुप्रेषित हुन्छ",
        "whatlinkshere-hidetrans": "$1 पारदर्शन",
-       "whatlinkshere-hidelinks": "$1 à¤²à¤¿à¤\99à¥\8dà¤\95हरà¥\81",
+       "whatlinkshere-hidelinks": "$1 à¤²à¤¿à¤\99à¥\8dà¤\95हरà¥\82",
        "whatlinkshere-hideimages": "$1 फाइल लिंकहरू",
        "whatlinkshere-filters": "छानियाका",
        "ipbreason-dropdown": "* ब्लक गर्नुका समान्य कारणहरू\n** झूटो सूचना दियाको\n** पानानबठे सामाग्रीहरू हटायाको\n** बाहिरी जालक्षेत्र (sites)सित नचाहिंदो लिङ्क गर्याको \n** पानानमी बकवास/गाली-गलौच हाल्याको\n** भै धेकाउने व्यवहार/उत्पीडन (सताउने कार्य) गर्याको\n** धेरै गलत खाताहरू बनायाको\n** प्रयोगकर्ता नाम अस्वीकार्य",
index 6e92c24..a514940 100644 (file)
        "watchthis": "Atenti ĉi tiun paĝon",
        "savearticle": "Konservi paĝon",
        "savechanges": "Konservi ŝanĝojn",
-       "publishpage": "Publikigi paĝon",
+       "publishpage": "Eldoni paĝon",
        "publishchanges": "Eldoni ŝanĝojn",
        "preview": "Antaŭrigardo",
        "showpreview": "Antaŭrigardo",
        "grant-group-high-volume": "Efektivigi ampleksegajn agojn",
        "grant-group-customization": "Personecigoj kaj preferoj",
        "grant-group-administration": "Efektivigi administrajn agojn",
+       "grant-group-private-information": "Aliru privatan datumon pri vi",
        "grant-group-other": "Diversaj aktivecoj",
        "grant-blockusers": "Bloki kaj malbloki uzantojn",
        "grant-createaccount": "Krei kontojn",
        "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 240be82..083585f 100644 (file)
        "passwordreset-email": "E-mail helbidea:",
        "passwordreset-emailtitle": "{{SITENAME}}-rako kontuaren xehetasunak",
        "passwordreset-emailelement": "Erabiltzaile izena: \n$1\n\nBehin-behineko pasahitza: \n$2",
-       "passwordreset-emailsentemail": "Hau zure konturako erregistratuta dagoen helbide elektronikoa baldin bada, mezu elektronikoa bidaliko da zure pasahitza berrezartzeko.",
+       "passwordreset-emailsentemail": "Hau zure kontuarekin lotuta dagoen helbide elektronikoa baldin bada, mezu elektronikoa bidaliko da zure pasahitza berrezartzeko.",
        "changeemail": "Aldatu edo kendu e-mail helbidea",
        "changeemail-header": "Bete ezazu inprimaki hau, zure helbide elektronikoa aldatzeko. Zure kontuari helbide elektronikorik elkartuta ez izatea nahi baduzu, utz ezazu hutsik helbide elektroniko berria, inprimakia bidaltzen duzunean.",
        "changeemail-no-info": "Orrialde honetara zuzenean sartzeko izena eman behar duzu.",
        "accmailtext": "[[User talk:$1|$1]]-entzako ausaz sortutako pasahitza $2-(r)a bidali da.\n\n''[[Special:ChangePassword|pasahitz aldaketa]]'' orrialdean alda daiteke, behin barruan sartuta.",
        "newarticle": "(Berria)",
        "newarticletext": "Orrialde hau ez da existitzen oraindik. Orrialde sortu nahi baduzu, beheko koadroan idazten hasi zaitezke (ikus [$1 laguntza orrialdea] informazio gehiagorako). Hona nahi gabe etorri bazara, nabigatzaileko '''atzera''' botoian klik egin.",
-       "anontalkpagetext": "----''Orrialde hau konturik sortu ez edo erabiltzen ez duen erabiltzaile anonimo baten eztabaida orria da.\nBere IP helbidea erabili beharko da beraz identifikatzeko.\nErabiltzaile batek baino gehiagok IP bera erabil dezakete ordea.\nErabiltzaile anonimoa bazara eta zurekin zerikusirik ez duten mezuak jasotzen badituzu, mesedez [[Special:CreateAccount|Izena eman]] edo [[Special:UserLogin|saioa hasi]] etorkizunean horrelakoak gerta ez daitezen.''",
+       "anontalkpagetext": "<em>Orrialde hau konturik sortu ez edo erabiltzen ez duen erabiltzaile anonimo baten eztabaida orria da.</em>\nBere IP helbidea erabili beharko da beraz identifikatzeko.\nErabiltzaile batek baino gehiagok IP bera erabil dezakete ordea.\nErabiltzaile anonimoa bazara eta zurekin zerikusirik ez duten mezuak jasotzen badituzu, mesedez [[Special:CreateAccount|Izena eman]] edo [[Special:UserLogin|saioa hasi]] etorkizunean horrelakoak gerta ez daitezen.",
        "noarticletext": "Oraindik ez dago testurik orri honetan.\nEdukiz hornitzeko, aukera hauek dituzu: beste orri batzuetan [[Special:Search/{{PAGENAME}}|orri izenburu hau bilatzea]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} lotutako logak bilatzea],\nedo [{{fullurl:{{FULLPAGENAME}}|action=edit}} orri hau sortzea]</span>.",
        "noarticletext-nopermission": "Une honetan ez dago testurik orrialde honetan.\nBeste orrialdeetan [[Special:Search/{{PAGENAME}}|izenburu hau bilatu dezakezu]],\nedo <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} erlazionatutako erregistroak bilatu]</span>, baina ez duzu orrialde hau sortzeko baimenik.",
        "userpage-userdoesnotexist": "\"<nowiki>$1</nowiki>\" lankidea ez dago erregistatuta. Mesedez, konprobatu orri hau editatu/sortu nahi duzun.",
        "preferences": "Hobespenak",
        "mypreferences": "Hobespenak",
        "prefs-edits": "Aldaketa kopurua:",
-       "prefsnologintext2": "Mesedez $1 zure hobespenak aldatzeko.",
+       "prefsnologintext2": "Mesedez saioa hasi zure hobespenak aldatzeko.",
        "prefs-skin": "Itxura",
        "skin-preview": "Aurrebista",
        "datedefault": "Hobespenik ez",
        "uploadstash": "Gordailu bat igo",
        "uploadstash-clear": "Kodetutako fitxategiak izkutatu",
        "uploadstash-nofiles": "Ez duzu kodetutako fitxategirik.",
-       "uploadstash-errclear": "Fitxategiak ezabatzeak ez du arrakastarik izan.",
+       "uploadstash-errclear": "Fitxategiak ezabatzeak akatsa eman du.",
        "uploadstash-refresh": "Fitxategien zerrenda eguneratu",
        "img-auth-accessdenied": "Sarbide ukatua",
        "img-auth-nopathinfo": "PATH_INFO falta da.\nZure zerbitzaria ez dago informazio hau pasatzeko konfiguratuta.\nCGI-oinarriduna izan daiteke, img_auth onartzen ez duena.\nIkusi https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "nolicense": "Hautatu gabe",
        "licenses-edit": "Aldatu lizentzien aukerak",
        "license-nopreview": "(Aurreikuspenik ez)",
-       "upload_source_url": " (baliozko URL publikoa)",
-       "upload_source_file": " (zure ordenagailuko fitxategi bat)",
+       "upload_source_url": "(zuk aukeratutako fitxategi baliozko URL publikorako)",
+       "upload_source_file": "(zure ordenagailuko fitxategi bat)",
        "listfiles-delete": "ezabatu",
        "listfiles-summary": "Orri berezi honek igotako fitxategi guztiak erakusten ditu.",
        "listfiles_search_for": "Irudiaren izenagatik bilatu:",
        "tooltip-feed-rss": "Orrialde honen RSS jarioa",
        "tooltip-feed-atom": "Orrialde honen atom jarioa",
        "tooltip-t-contributions": "{{GENDER:$1|Lankide honen}} ekarpen zerrenda ikusi",
-       "tooltip-t-emailuser": "Lankide honi e-posta mezua bidali",
+       "tooltip-t-emailuser": "{{GENDER:$1|Lankide honi}} e-posta mezua bidali",
        "tooltip-t-info": "Orrialde honi buruzko informazio gehiago",
        "tooltip-t-upload": "Irudiak edo media fitxategiak igo",
        "tooltip-t-specialpages": "Orri berezi guztien zerrenda",
index 47d668d..f587eac 100644 (file)
        "revertmove": "પૂર્વવત",
        "delete_and_move_text": "== પાનું દૂર કરવાની જરૂર છે  ==\nલક્ષ્ય પાનું  \"[[:$1]]\" પહેલેથી અસ્તિત્વમાં છે.\nશું તમે આને હટાવીને સ્થળાંતર કરવાનો માર્ગ મોકળો કરવા માંગો છો?",
        "delete_and_move_confirm": "હા, આ પાનું હટાવો",
-       "delete_and_move_reason": "હટાવવાનું કામ આગળ વધાવવા ભૂંસી દેવાયુ \"[[$1]]\"",
+       "delete_and_move_reason": "\"[[$1]]\"થી ખસેડીને માહિતી અહિં લાવવા માટે ભૂંસી દેવાયું.",
        "selfmove": "સ્રોત ને લક્ષ્ય શીર્ષકો સમાન છે;\nપાના ને તેવા જ નામ ધરાવતા પાના પર પુનઃ સ્થાપન નહીં કરી શકાય.",
        "immobile-source-namespace": "\"$1\" નામાસ્થળમાં પાના ન ખસેડી શાકાયા",
        "immobile-target-namespace": "\"$1\" નામાસ્થળમાં પાના ન ખસેડી શાકાયા",
index d781023..c4788aa 100644 (file)
@@ -12,7 +12,8 @@
                        "Wyvernoid",
                        "לערי ריינהארט",
                        "아라",
-                       "Macofe"
+                       "Macofe",
+                       "Robin van der Vliet"
                ]
        },
        "tog-underline": "Sub-strekizez ligili:",
        "minoredit": "Ico esas mikra redaktajo",
        "watchthis": "Surveyar ica pagino",
        "savearticle": "Registragar pagino",
+       "publishpage": "Publikigar pagino",
+       "publishchanges": "Publikigar chanji",
        "preview": "Previdar",
        "showpreview": "Previdar",
        "showdiff": "Montrez chanji",
index 3aabbe6..71603bd 100644 (file)
        "img-auth-accessdenied": "Aksès ditulak",
        "img-auth-nopathinfo": "Kélangan PATH_INFO.\nSasana Sampéyan durung disetèl kanggo ngliwati inpormasi iki.\nMungkin amarga abasis-CGI lan ora bisa nyengkuyung img_auth.\nDelok https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "img-auth-notindir": "Alur sing dijaluk dudu dirèktori unggah kakonpigurasi.",
-       "img-auth-badtitle": "Ora bisa mbangun judhul sah saka \"$1\".",
+       "img-auth-badtitle": "Ora bisa ngyasa sesirah sing sah saka \"$1\".",
        "img-auth-nologinnWL": "Sampéyan durung mlebu log lan \"$1\" ora nèng daptar putih.",
        "img-auth-nofile": "Berkas \"$1\" ora ana.",
        "img-auth-isdir": "Sampéyan lagi njajal ngaksès dirèktori \"$1\".\nNamung aksès berkas sing dililakaké.",
        "unusedimages": "Berkas sing ora dienggo",
        "wantedcategories": "Kategori sing diperlokaké",
        "wantedpages": "Kaca sing dipèngini",
-       "wantedpages-badtitle": "Judhul ora valid ing sèt asil: $1",
+       "wantedpages-badtitle": "Sesirah ora sah ing omboyakan kasil: $1",
        "wantedfiles": "Berkas sing diperlokaké",
        "wantedfiletext-cat": "Berkas iki dianggo nanging ora ana. Berkas saka panyimpenan asing mungkin kadaptar tinimbang ana kasunyatan. Saben ''positip salah'' bakal <del>diorèk</del>. Lan, kaca sing nyartakaké berkas sing ora ana bakal kadaptar nèng [[:$1]].",
        "wantedfiletext-nocat": "Berkas iki dianggo nanging ora ana. Berkas saka panyimpenan asing mungkin kadaptar tinimbang ana kasunyatan. Saben ''positip salah'' bakal <del>diorèk</del>.",
        "protectedpages-expiry": "Kadaluwarsa",
        "protectedpages-reason": "Alesan",
        "protectedtitles": "Sesirah direksa",
-       "protectedtitlesempty": "Ora ana irah-irahan utawa judhul sing direksa karo paramèter-paramèter iki.",
+       "protectedtitlesempty": "Ora ana sesirah sing saiki kareksa mawa paramèter iki.",
        "listusers": "Daftar panganggo",
        "listusers-editsonly": "Tampilaké mung panganggo sing nduwèni kontribusi",
        "listusers-creationsort": "Urut miturut tanggal digawé",
        "delete_and_move_text": "Kaca jujugan \"[[:$1]]\" wis ana.\nApa sampéyan kersa mbusak iku supaya kacané bisa dilih?",
        "delete_and_move_confirm": "Ya, busak kaca iku.",
        "delete_and_move_reason": "Dibusak kanggo jaga-jaga ananing pamindhahan saka \"[[$1]]\"",
-       "selfmove": "Pangalihan kaca ora bisa dilakoni amerga irah-irahan utawa judhul sumber lan tujuané padha.",
+       "selfmove": "Sesirah sumber lan tujuan padha;\nora bisa ngalih nyang tujuan sing padha.",
        "immobile-source-namespace": "Ora bisa mindhahaké kaca jroning bilik jeneng \"$1\"",
        "immobile-target-namespace": "Ora bisa mindhahaké kaca menyang bilik jeneng \"$1\"",
        "immobile-target-namespace-iw": "Pranala interwiki dudu target sing sah kanggo pamindhahan kaca.",
        "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 8ec2e36..0037459 100644 (file)
        "noindex-category": "არ არსებობს ინდექსირებული გვერდები",
        "broken-file-category": "გვერდები ფაილების არასწორი ბმულებით",
        "categoryviewer-pagedlinks": "($1) ($2)",
+       "category-header-numerals": "$1–$2",
        "about": "შესახებ",
        "article": "სტატია",
        "newwindow": "(ახალ ფანჯარაში)",
        "tagline": "{{SITENAME}} გვერდიდან",
        "help": "დახმარება",
        "search": "ძიება",
+       "search-ignored-headings": "#<!-- დატოვეთ ეს ხაზი უცვლელად --> <pre>\n# სათაურები, რომლებთაც ძიება დააგნორებს.\n# ცვლილებები აისახება როგორც კი სათაურის მქონე გვერდს ინდექსირება გაუკეთდება.\n# შეგიძლიათ გააკეთოთ გვერდის ძალით ხელახალი ინდექსირება null edit-ის გაკეთებით.\n# სინტაქსი შემდეგია:\n#   * ყველაფერი \"#\" სიმბოლოდან ხაზის ბოლმდე კომენტარია.\n#   * ყველა არა-ცარიელი ხაზი is the exact title to ignore, case and everything.\nწყაროები\nრესურსები ინტერნეტში\nიხილეთ აგრეთვე\n #</pre> <!-- დატოვეთ ეს ხაზი უცვლელად -->",
        "searchbutton": "ძიება",
        "go": "სტატია",
        "searcharticle": "გვერდი",
        "passwordreset-emailelement": "მომხმარებლის სახელი: \n$1\n\nდროებითი პაროლი: \n$2",
        "passwordreset-emailsentemail": "თუ ეს მეილი თქვენს ანგარიშთანაა დაკავშირებული, გაიგზავნება პაროლის თავიდან დასაყენებელი ელექტრონული ფოსტა.",
        "passwordreset-emailsentusername": "თუ არსებობს მეილი, რომელიც ამ ანგარიშთანაა დაკავშირებული, გაიგზავნება პაროლის თავიდან დასაყენებელი ელექტრონული ფოსტა.",
-       "passwordreset-emailsent-capture": "ქვემოთ ნაჩვენები პაროლის თავიდან დასაყენებელი წერილი გაიგზავნა.",
-       "passwordreset-emailerror-capture": "ქვემოთ მოცემულია შექმნილი პაროლის დასაყენებელი წერილი, რომლის გაგზავნაც {{GENDER:$2|მომხმარებელთან}} ვერ მოხერხდა: $1 გამო",
        "passwordreset-emailsent-capture2": "პაროლის გაუქმების შესახებ {{PLURAL:$1|მეილი|მეილები}} გაიგზავნა. {{PLURAL:$1|სახელი და პაროლი|სახელებისა და პაროლების სია}} არის ნაჩვენები ქვემოთ.",
        "passwordreset-emailerror-capture2": "{{GENDER:$2|მომხმარებელთან}} მეილის გაგზავნა ვერ მოხერხდა: $1 {{PLURAL:$3|სახელი და პაროლი|სახელებისა და პაროლების სია}} არის ნაჩვენები ქვემოთ.",
        "passwordreset-nocaller": "გამომძახებელი უნდა იყოს მიწოდებული",
        "passwordreset-nodata": "არც მომხმარებლის სახელი და არც ელ-ფოსტის მისამართი არ იყო მოწოდებული",
        "changeemail": "ელ-ფოსტის მისამართის შეცვლა ან წაშლა",
        "changeemail-header": "შეავსეთ ეს ფორმა მეილის შესაცვლელად. თუ გსურთ თქვენი ანგარიში არ იყოს დაკავშირებული არცერთ მეილთან, ახალი მეილის მისამართის ველი დატოვეთ ცარიელი.",
-       "changeemail-passwordrequired": "ამ ცვლილების დასადასტურებლად დაგჭირდებათ პაროლის შეყვანა.",
        "changeemail-no-info": "თქვენ ავტირიზებული უნდა იყოთ ამ გვერდთან უშუალო წვდომისთვის.",
        "changeemail-oldemail": "ელ-ფოსტის ამჟამინდელი მისამართი:",
        "changeemail-newemail": "ახალი ელ-ფოსტის მისამართი:",
        "content-model-css": "CSS",
        "content-json-empty-object": "ცარიელი ობიექტი",
        "content-json-empty-array": "ცარიელი ტაბლო",
+       "deprecated-self-close-category": "გვერდები, რომლებიც იყენებენ არავალიდურ თვითდახურვად HTML ტეგებს",
        "duplicate-args-warning": "<strong>გაფრთხილება:</strong> [[:$1]] იძახებს [[:$2]]-ის \"$3\" პარამეტრის ერთზე მეტ მნიშვნელობას. აისახება მხოლოდ ბოლოს გამოყენებული მნიშვნელობა.",
        "duplicate-args-category": "გვერდები, რომლებიც იყენებენ დუბლიკატ არგუმენტებს თარგების გამოძახებისას",
        "duplicate-args-category-desc": "გვერდები, რომლებიც იყენებენ დუბლიკატ არგუმენტებს თარგების გამოძახებისას, როგორებიც არის <code><nowiki>{{foo|bar=1|bar=2}}</nowiki></code> ან <code><nowiki>{{foo|bar|1=bar}}</nowiki></code>.",
        "undo-nochange": "როგორც ჩანს, რედაქტირება უკვე გაუქმდა.",
        "undo-summary": "[[Special:Contributions/$2|$2-ის]]([[User talk:$2|განხილვა]]) ცვლილებების გაუქმება (№$1)",
        "undo-summary-username-hidden": "ცვლილების გაუქმება $1, მომხმარებლის მიერ, რომლის სახელი დამალულია",
-       "cantcreateaccounttitle": "ანგარიშის შექმნა ვერ ხერხდება",
        "cantcreateaccount-text": "ამ IP-მისამართიდან აიკრძალა (<b>$1</b>) მომხმარებელ [[User:$3|$3]]-ის მიერ.\n\n$3 -ემ ამგვარი ახსნა : ''$2''",
        "cantcreateaccount-range-text": "{{GENDER:$3|მომხმარებელმა}} [[User:$3|$3]] ანგარიშის ან IP-მისამართის $1 შექმნისთვის {{GENDER:$3|დაადო}} აკრძალვა <strong>$1</strong>, თქვენი IP-მისამართის ჩათვლით ($4).\n\nმითითებულია შემდეგი მიზეზი: $2.",
        "viewpagelogs": "ამ გვერდისთვის სარეგისტრაციო ჟურნალების ჩვენება",
        "grant-highvolume": "დიდი მოცულობით რედაქტირება",
        "grant-oversight": "მომხმარებლებისა და შესწორებების დამალვა",
        "grant-patrol": "გვერდების რედაქტირებების შემოწმება",
+       "grant-privateinfo": "პირად ინფორმაციაზე წვდომა",
        "grant-protect": "გვერდების და დაცვა და დაცვის მოხსნა",
        "grant-rollback": "გვერდების რედაქტირებების სწრაფი გაუქმება",
        "grant-sendemail": "გაგუგზავნე ელექტრონული ფოსტა სხვა მომხმარებლებს",
        "rightslogtext": "მომხმარებელთა უფლებების ცვლილებათა ჟურბალი",
        "action-read": "ამ გვერდის კითხვა",
        "action-edit": "ამ გვერდის რედაქტირება",
-       "action-createpage": "á\83\92á\83\95á\83\94á\83 á\83\93á\83\94á\83\91ის შექმნა",
-       "action-createtalk": "á\83\92á\83\90á\83\9cá\83®á\83\98á\83\9aá\83\95á\83\98á\83¡ á\83\92á\83\95á\83\94á\83 á\83\93á\83\94á\83\91ის შექმნა",
+       "action-createpage": "á\83\90á\83\9b á\83\92á\83\95á\83\94á\83 á\83\93ის შექმნა",
+       "action-createtalk": "á\83\90á\83\9b á\83\92á\83\90á\83\9cá\83®á\83\98á\83\9aá\83\95á\83\98á\83¡ á\83\92á\83\95á\83\94á\83 á\83\93ის შექმნა",
        "action-createaccount": "ამ ანგარიშის შექმნა",
        "action-autocreateaccount": "გარე მომხმარებლის ანგარიშის ავტომატურად შექმნა",
        "action-history": "ამ გვერდის ისტორიის ნახვა",
        "undeletedrevisions": "$1 ვერსია აღდგენილია",
        "undeletedrevisions-files": "$1 ვერსია და $2 ფაილი აღდგენილია",
        "undeletedfiles": "$1 ფაილი აღდგენილია",
-       "cannotundelete": "á\83¬á\83\90á\83¨á\83\9aá\83\98á\83¡ á\83\92á\83\90á\83£á\83¥á\83\9bá\83\94á\83\91á\83\90 á\83\95á\83\94á\83  á\83\92á\83\90á\83\9cá\83®á\83\9dá\83 á\83ªá\83\98á\83\94á\83\9aá\83\93á\83\90\n$1",
+       "cannotundelete": "á\83\96á\83\9dá\83\92á\83\98á\83\94á\83 á\83\97á\83\98 á\83\90á\83\9c á\83§á\83\95á\83\94á\83\9aá\83\90 á\83¬á\83\90á\83¨á\83\9aá\83\98á\83¡ á\83\92á\83\90á\83£á\83¥á\83\9bá\83\94á\83\91á\83\90 á\83\95á\83\94á\83  á\83\92á\83\90á\83\9cá\83®á\83\9dá\83 á\83ªá\83\98á\83\94á\83\9aá\83\93á\83\90:\n$1",
        "undeletedpage": "'''$1 აღდგენილია'''\n\nუკანასკნელი წაშლილთა და აღდგენის სია შეგიძლიათ ნახოთ [[Special:Log/delete|წაშლილთა სიაში]].",
        "undelete-header": "ბოლოს წაშლილი გვერდების სიის ნახვა შეიძლება [[Special:Log/delete|წაშლათა ჟურნალში]].",
        "undelete-search-title": "წაშლილი გვერდების ძიება",
        "undelete-error-long": "ფაილის აღდგენისას წარმოიშვა შეცდომები\n\n$1",
        "undelete-show-file-confirm": "დარწმუნებული ხართ, რომ გსურთ ფაილ <nowiki>$1</nowiki>-ის წაშლილი ვერსიის ხილვა $2 $3-დან?",
        "undelete-show-file-submit": "ჰო",
+       "undelete-revision-row2": "$1 ($2) $3 . . $4 $5 $6 $7 $8",
        "namespace": "სახელთა სივრცე:",
        "invert": "ყველა, მონიშნულის გარდა",
        "tooltip-invert": "მონიშნეთ ეს უჯრა, რათა დამალოთ გვერდების ცვლილებები არჩეული სახელთა სივრცის ფარგლებში (და მასთან დაკავშირებულ სახელთა სივრცეში, თუ მსგავსი რამ მითითებულია)",
        "sp-contributions-newbies-sub": "ახალბედებისთვის",
        "sp-contributions-newbies-title": "ბოლოს დარეგისტრირებულ მომხმარებელთა წვლილი",
        "sp-contributions-blocklog": "ბლოკირების ისტორია",
-       "sp-contributions-suppresslog": "მომხმარებლის წაშლილი წვლილი",
-       "sp-contributions-deleted": "მომხმარებლის წაშლილი შესწოებები",
+       "sp-contributions-suppresslog": "{{GENDER:$1|მომხმარებლის}} წაშლილი წვლილი",
+       "sp-contributions-deleted": "{{GENDER:$1|მომხმარებლის}} წაშლილი შესწოებები",
        "sp-contributions-uploads": "ატვირთვები",
        "sp-contributions-logs": "ჟურნალები",
        "sp-contributions-talk": "განხილვა",
        "sp-contributions-username": "IP მისამართი ან მომხმარებლის სახელი:",
        "sp-contributions-toponly": "აჩვენე მხოლოდ ბოლო ვერსიები",
        "sp-contributions-newonly": "აჩვენე მხოლოდ ცვლილებები, რომელიც წარმოადგენს გვერდის შექმნილს",
+       "sp-contributions-hideminor": "მცირე რედაქტირებების დამალვა",
        "sp-contributions-submit": "ძიება",
        "whatlinkshere": "ბმული გვერდზე",
        "whatlinkshere-title": "გვერდები, რომლებიც შეიცავენ „$1“-ის ბმულებს",
        "ipb-unblock": "მომხმარებლის სახელზე ან IP მისამართზე ბლოკის მოხსნა",
        "ipb-blocklist": "იხილე არსებული ბლოკირებები",
        "ipb-blocklist-contribs": "მომხმარებელ {{GENDER:$1|$1}} წვლილი",
+       "ipb-blocklist-duration-left": "დარჩა $1",
        "unblockip": "მომხმარებელზე ბლოკის მოხსნა",
        "unblockiptext": "გამოიყენეთ ქვემოთ მოცემული ფორმულარი, რათა  დაბლოკილი IP მისამართი ან მომხმარებლის სახელი აღადგინოთ.",
        "ipusubmit": "ამ ბლოკის მოხსნა",
        "lockdbsuccesstext": "პროექტის მონაცემთა ბაზა დაიბლოკა.<br />\nარ დაგავიწყდეთ [[Special:UnlockDB|ბლოკის მოხსნა]] მონაცემთა ბაზასთან სამუშაოების გატარების შემდეგ.",
        "unlockdbsuccesstext": "მომაცემთა ბაზაზე ბლოკი მოიხსნა.",
        "lockfilenotwritable": "არ გაქვთ უფლება მონაცემთა ბაზის დაცვის ფაილის შესწორების. დასაბლოკად ან ბლოკის მოსახსნელად საჭიროა ფაილი ღია იყოს.",
+       "databaselocked": "მონაცემთა ბაზა უკვე ჩაკეტილია.",
        "databasenotlocked": "მონაცემთა ბაზა არაა ჩაკეტილი.",
        "lockedbyandtime": "($1 $2 $3)",
        "move-page": "$1 — გადატანა",
        "tooltip-ca-nstab-category": "გვერდის კატეგორიის ჩვენება",
        "tooltip-minoredit": "მონიშნე როგორც მცირე რედაქტირება [alt-i]",
        "tooltip-save": "თქვენი ცვლილებების შენახვა",
+       "tooltip-publish": "თქვენი ცვლილებების გამოქვეყნება",
        "tooltip-preview": "წინასწარ გადახედე ცვლილებებს, გთხოვთ გამოიყენოთ ეს შენახვამდე! [alt-p]",
        "tooltip-diff": "ტექსტში შეტანილი ცვლილებების ჩვენება. [alt-v]",
        "tooltip-compareselectedversions": "იხილეთ ამ გვერდის  ორ შერჩეულ ვერსიას შორის განსხვავებები.",
        "confirmemail_body_set": "ვიღაცამ, შესაძლოა თქვენ, IP მისამართით $1,\nპროექტში {{SITENAME}} შეცვალა ელ.ფოსტის მისამართი ანგარიშისათვის \"$2\" ამ მისამართით.\n\nიმის დასადასტურებლად, რომ ეს ანგარიში ნამდვილად თქვენ გეკუთვნით\nდა ელ.ფოსტის შესაძლებლობების  გასააქტიურებლად საიტზე {{SITENAME}}, გახსენით ეს ბმული თქვენს ბრაუზერში:\n\n$3\n\nთუ ეს თქვენ *არ* იყავით, მაშინ ელ.ფოსტის მისამართის დასტურების გასაუქმებლად, გადადით ამ ბმულზე:\n\n$5\n\nწერილის ვადის გასვლის თარიღია $4.",
        "confirmemail_invalidated": "ელ-ფოსტის დადასტურება გაუქმდა",
        "invalidateemail": "ელ-ფოსტის დადასტურების გაუქმება",
+       "notificationemail_subject_changed": "{{SITENAME}} რეგისტრირებული იმეილის მისამართები შეიცვალა",
+       "notificationemail_subject_removed": "{{SITENAME}} რეგისტრირებული იმეილის მისამართები წაიშალა",
        "scarytranscludedisabled": "[«Interwiki transcluding» გათიშულია]",
        "scarytranscludefailed": "[$1-თან დაკავშირების შეცდომა]",
        "scarytranscludefailed-httpstatus": "[ვერ მოხერხდა თარგის ჩატვირთვა $1-თვის: HTTP $2]",
        "confirm-watch-top": "დავამატო ეს გვერდი თქვენი კონტროლის სიას?",
        "confirm-unwatch-button": "დიახ",
        "confirm-unwatch-top": "მოვხსნა ეს გვერდი თქვენი კონტროლის სიიდან?",
+       "confirm-rollback-button": "კარგი",
        "semicolon-separator": ";&#32;",
        "comma-separator": ",&#32;",
        "colon-separator": ":&#32;",
        "logentry-protect-protect-cascade": "$1-მ {{GENDER:$2|დაიცვა}} $3 $4 [კასკადური]",
        "logentry-protect-modify": "$1-მ {{GENDER:$2|შეცვალა}} დაცვის დონე $3 $4-სთვის",
        "logentry-protect-modify-cascade": "$1 {{GENDER:$2|შეცვალა}} დაცვის დონე $3 $4 [კასკადური]",
-       "logentry-rights-rights": "á\83\9bá\83\9dá\83\9bá\83®á\83\9bá\83\90á\83 á\83\94á\83\91á\83\94á\83\9aá\83\9bá\83\90 $1 {{GENDER:$2|á\83¨á\83\94á\83£á\83ªá\83\95á\83\90á\83\9aá\83\90}} á\83¯á\83\92á\83£á\83¤á\83\98 $3-á\83¡ $4-დან $5-ზე",
+       "logentry-rights-rights": "á\83\9bá\83\9dá\83\9bá\83®á\83\9bá\83\90á\83 á\83\94á\83\91á\83\94á\83\9aá\83\9bá\83\90 $1 {{GENDER:$2|á\83¨á\83\94á\83ªá\83\95á\83\90á\83\9aá\83\90}} á\83¯á\83\92á\83£á\83¤á\83\98á\83¡ á\83¬á\83\94á\83\95á\83 á\83\9dá\83\91á\83\90 á\83\9bá\83\9dá\83\9bá\83®á\83\9bá\83\90á\83 á\83\94á\83\91á\83\9aá\83\98á\83¡á\83\90á\83\97á\83\95á\83\98á\83¡ {{GENDER:$6|$3}} $4-დან $5-ზე",
        "logentry-rights-rights-legacy": "მომხმარებელმა $1 {{GENDER:$2|შეცვალა}} ჯგუფის წევრობა $3-თვის",
        "logentry-rights-autopromote": "მომხმარებელი $1 ავტომატურად იქნა {{GENDER:$2|გადაყვანილი}} $4–დან $5–ში",
        "logentry-upload-upload": "მომხმარებელმა $1 {{GENDER:$2|ატვირთა}} $3",
        "sessionprovider-generic": "$1 სესიები",
        "sessionprovider-mediawiki-session-cookiesessionprovider": "cookie-სთან დაკავშირებული სესიები",
        "sessionprovider-nocookies": "შესაძლოა ქუქები გათიშულია. გთხოვთ ჩართეთ და სცადეთ განმეორებით.",
-       "randomrootpage": "შემთხვევითი ძირეული გვერდი"
+       "randomrootpage": "შემთხვევითი ძირეული გვერდი",
+       "log-action-filter-block": "ბლოკირების ტიპი:",
+       "log-action-filter-delete": "წაშლის ტიპი:",
+       "log-action-filter-import": "იმპორტის ტიპი:",
+       "log-action-filter-managetags": "ტეგის ცვლილების ტიპი:",
+       "log-action-filter-move": "გადატანის ტიპი:",
+       "log-action-filter-newusers": "ანგარიშის შექმნის ტიპი:",
+       "log-action-filter-patrol": "შემოწმების ტიპი:",
+       "log-action-filter-protect": "დაცვის ტიპი:",
+       "log-action-filter-rights": "უფლების შეცვლის ტიპი:",
+       "log-action-filter-upload": "ატვირთვის ტიპი:",
+       "log-action-filter-all": "ყველა",
+       "log-action-filter-block-block": "დაბლოკვა",
+       "log-action-filter-block-reblock": "ბლოკირების შეცვლა",
+       "log-action-filter-block-unblock": "განბლოკვა",
+       "log-action-filter-newusers-create2": "დარეგისტრირებული მომხმარებლის შექმნა",
+       "log-action-filter-newusers-autocreate": "ავტომატური შექმნა",
+       "log-action-filter-newusers-byemail": "პაროლით შექმნა, რომელიც გამოიგზავნა იმეილით",
+       "log-action-filter-patrol-autopatrol": "ავტომატური შემოწმება",
+       "log-action-filter-protect-protect": "დაცვა",
+       "log-action-filter-protect-unprotect": "დაცვის მოხსნა",
+       "log-action-filter-protect-move_prot": "დაცვა გადატანისაგან",
+       "log-action-filter-rights-autopromote": "ავტომატური შეცვლა",
+       "log-action-filter-upload-upload": "ახალი ატვირთვა",
+       "log-action-filter-upload-overwrite": "ხელახლა ატვირთვა",
+       "authmanager-authplugin-setpass-failed-title": "პაროლის ცვლილება ვერ განხორციელდა",
+       "authmanager-email-label": "ელ. ფოსტა",
+       "authmanager-email-help": "ელ. ფოსტის მისამართი",
+       "authmanager-realname-label": "ნამდვილი სახელი",
+       "authmanager-realname-help": "მომხმარებლის ნამდვილი სახელი",
+       "authprovider-resetpass-skip-label": "გამოტოვება",
+       "authprovider-resetpass-skip-help": "გამოტოვეთ პაროლის შეცვლის პროცესი.",
+       "specialpage-securitylevel-not-allowed-title": "არ არის ნებადართული",
+       "credentialsform-account": "ანგარიშის სახელი:"
 }
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 89742eb..25e8040 100644 (file)
        "recreate": "Izveidot no jauna",
        "confirm_purge_button": "Labi",
        "confirm-purge-top": "Iztīrīt šīs lapas kešu (''cache'')?",
+       "confirm-purge-bottom": "Lapas atjaunināšana iztīra kešatmiņu un liek parādīt lapas jaunāko versiju.",
        "confirm-watch-button": "Labi",
        "confirm-watch-top": "Pievienot šo lapu uzraugāmo lapu sarakstam?",
        "confirm-unwatch-button": "Labi",
index cd91aec..9636b64 100644 (file)
        "note": "'''सूचना:'''",
        "previewnote": "'''याद राख्नुहोस् यो केवल पूर्वावलोकन मात्र हो; तपाईंका परिवर्तनहरू संग्रहित भएका छैनन्!'''",
        "continue-editing": "सम्पादन क्षेत्रमा जानुहोस",
-       "previewconflict": "यस à¤ªà¥\82रà¥\8dवावलà¥\8bà¤\95नलà¥\87 à¤¸à¤\82पादन à¤\95à¥\8dषà¥\87तà¥\8dर à¤\95à¥\8b à¤®à¤¾à¤¥à¤¿à¤²à¥\8dलà¥\8b à¤­à¤¾à¤\97à¤\95à¥\8b à¤ªà¤¾à¤  à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रà¥\8dनà¥\87 à¤ à¤¾à¤\89à¤\81à¤\95à¥\8b à¤ªà¤¾à¤ à¤²à¤¾à¤\87 à¤¦à¥\87à¤\96ाà¤\89à¤\81à¤\9b à¤\85नि à¤¤à¤ªà¤¾à¤\87लà¥\87 à¤¯à¤¸à¤²à¤¾à¤\87 सेभ गरेपछि देखापर्छ।",
+       "previewconflict": "यस à¤ªà¥\82रà¥\8dवावलà¥\8bà¤\95नलà¥\87 à¤¸à¤®à¥\8dपादन à¤\95à¥\8dषà¥\87तà¥\8dर à¤\95à¥\8b à¤®à¤¾à¤¥à¤¿à¤²à¥\8dलà¥\8b à¤­à¤¾à¤\97à¤\95à¥\8b à¤ªà¤¾à¤  à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रà¥\8dनà¥\87 à¤ à¤¾à¤\89à¤\81à¤\95à¥\8b à¤ªà¤¾à¤ à¤²à¤¾à¤\87 à¤¦à¥\87à¤\96ाà¤\89à¤\81à¤\9b à¤\85नि à¤¤à¤ªà¤¾à¤\88à¤\82लà¥\87 à¤¯à¤¸à¤²à¤¾à¤\88 सेभ गरेपछि देखापर्छ।",
        "session_fail_preview": "'''माफ गर्नुहोस्! सत्र-आँकड़ा (session data) हराउनाले हामीले तपाईंको सम्पादन प्रक्रिया अघि बढाउन सकेनौं।.'''\nकृपया पुनः प्रयास गर्नुहोस्।\nयदि फेरि पनि काम भएन भनें, [[Special:UserLogout|बाहिर गई(लग आउट गरी)]]  फेरि प्रवेश गर्नुहोस्।",
-       "session_fail_preview_html": "'''माफ गर्नुहोला! सत्र को डेटा को नोकसान को कारण ले गर्दा तपाइको सम्पादन लाइ जारी राख्न सकिएन।'''\n\n''जावास्क्रिप्ट हमलाहरु रोक्नको लागि यो पूर्वावलोकन लाइ देखाइएको छैन किन कि {{SITENAME}} मा काँचो HTML को प्रयोग गर्न मिल्ने बनाइएको छ।''\n\n'''यदि यो एक वैध प्रयास हो भने, कृपया पुन: प्रयास गर्नुहोला.'''\nयदि अझै पनि काम गरेन भने [[Special:UserLogout|निर्गमन(logging out)]] र पुन:आगमन(login) गर्ने प्रयास गर्नुहोला।",
+       "session_fail_preview_html": "माफ गर्नुहोला ! सेशन डाटा नष्ट भएको कारण तपाईंको परिवर्तन शुरक्षित गर्न सकिएन ।\n\n<em>किनकी {{SITENAME}}मा raw HTML सक्षम छ, जावास्क्रिप्ट हमहरूबाट बचाउनको लागि झलक नहीं देखाइएको छैन ।</em>\n\n<strong>यदी यो तपाईंको वैध सम्पादन यत्न थियो भने कृपया पुनः प्रयास गर्नुहोस् ।</strong>\nयदी यस पनि यस्तै भयो भने कृपया [[Special:UserLogout|लग आउट]] गरेर पुनः लग इन गर्नुहोस् तथा तपाईंको ब्राउजरले यस साइटसँग कुकीजको अनुमति दिन्छ दिन्न जाँच गर्नुहोस् ।",
        "token_suffix_mismatch": "'''सम्पादन टोकनमा विराम चिह्न र वर्ण सम्बन्धित गड़बड़ीको कारण तपाईंको सम्पादन अस्वीकार गरिएको छ'''\nपृष्ठको पाठ बचाउन सम्पादन अस्वीकार गरिएको हो।\nयस्तो त्यसबेला हुन्छ जब तपाईंले बगी वेवमा आधारित अज्ञात प्रोक्सी सेवा प्रयोग गर्नुहुन्छ।",
        "edit_form_incomplete": "'''सम्पादनको केहि भाग सर्वरसम्म पुग्न सकेन, दुइपल्ट जाँच गर्नुहोस्, तपाईंको सम्पादन यथावत रहे पुनः प्रयास गर्नुहोस्'''",
-       "editing": "$1 à¤¸à¤®à¥\8dपादन à¤\97रिà¤\81दà¥\88",
+       "editing": "$1 सम्पादन गरिदै",
        "creating": "$1 बनाइँदै",
-       "editingsection": "$1 (à¤\96णà¥\8dड) à¤¸à¤®à¥\8dपादन à¤\97रिà¤\81दà¥\88",
+       "editingsection": "$1 (खण्ड) सम्पादन गरिदै",
        "editingcomment": "$1 सम्पादन गर्दै(नयाँ खण्ड)",
        "editconflict": "सम्पादन बाँझियो: $1",
        "explainconflict": "तपाईंले सम्पादन कार्य सुरु गरेपछि कसैले यस पृष्टलाई परिवर्तन गरेकोछ।\nमाथिल्लो पाठक्षेत्रमा पृष्ठको वर्तमान पाठ छ।\nतपाईंको परिवर्तन तल्लो भागमा दर्शाइएकोछ। \nतपाईंले गर्नुभएको परिवर्तनलाई वर्तमान पाठसित मिसाउनु पर्नेछ, यदि तपाईंले \"{{int:savearticle}}\" थिच्नु भयो भनें पाठको माथिल्लो भाग '''मात्र''' संग्रह गरिनेछ।",
        "double-redirect-fixed-maintenance": "[[$1]]बाट [[$2]]मा दोहोरो अनुप्रेषण स्वत तय गरिंदै।",
        "double-redirect-fixer": "अनुप्रेषण तय गर्ने",
        "brokenredirects": "टुटेका रिडाइरेक्टहरू",
-       "brokenredirectstext": "तलà¤\95ा à¤²à¤¿à¤\99à¥\8dà¤\95हरà¥\81 à¤²à¥\87 à¤¹à¥\81दà¥\88 à¤¨à¤­à¤\8fà¤\95ा à¤ªà¥\83षà¥\8dठहररसँग जोडिन्छन्:",
+       "brokenredirectstext": "तलà¤\95ा à¤²à¤¿à¤\99à¥\8dà¤\95हरà¥\82लà¥\87 à¤¹à¥\81à¤\81दà¥\88 à¤¨à¤­à¤\8fà¤\95ा à¤ªà¥\83षà¥\8dठहरà¥\82सँग जोडिन्छन्:",
        "brokenredirects-edit": "सम्पादन",
        "brokenredirects-delete": "मेट्ने",
        "withoutinterwiki": "भाषा नभएको पृष्ठहरू",
        "protectedpages-timestamp": "समय चिन्ह",
        "protectedpages-page": "पृष्ठ",
        "protectedpages-expiry": "सकिनेछ",
-       "protectedpages-performer": "पà¥\8dरयà¥\8bà¤\97à¤\95रà¥\8dता à¤¸à¥\81रà¤\95à¥\8dषित à¤\97रिà¤\81दà¥\88",
+       "protectedpages-performer": "प्रयोगकर्ता सुरक्षित गरिदै",
        "protectedpages-params": "सुरक्षा प्यारामेटर",
        "protectedpages-reason": "कारण",
        "protectedpages-submit": "पानाहरू देखाउनुहोस्",
        "watchlist-options": "निगरानि सूची विकल्प",
        "watching": "निगरानी गर्दै...",
        "unwatching": "निगरानीबाट हटाउँदै...",
-       "watcherrortext": "\"$1\"à¤\95à¥\8b à¤²à¤¾à¤\97ि à¤¤à¤ªà¤¾à¤\87à¤\81à¤\95à¥\8b à¤¨à¤¿à¤\97रानà¥\80 à¤¸à¥\81à¤\9aà¥\80 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रà¥\8dनà¥\87 à¤\95à¥\8dरममा à¤¯à¥\8cà¤\9fा à¤¤à¥\8dरà¥\81à¤\9fà¥\80 à¤­à¤\8fà¤\95à¥\8b à¤\9b।",
+       "watcherrortext": "\"$1\"à¤\95à¥\8b à¤²à¤¾à¤\97ि à¤¤à¤ªà¤¾à¤\88à¤\82à¤\95à¥\8b à¤¨à¤¿à¤\97रानà¥\80 à¤¸à¥\81à¤\9aà¥\80 à¤ªà¤°à¤¿à¤µà¤°à¥\8dतन à¤\97रà¥\8dनà¥\87 à¤\95à¥\8dरममा à¤\8fà¤\89à¤\9fा à¤¤à¥\8dरà¥\81à¤\9fà¥\80 à¤­à¤\8fà¤\95à¥\8b à¤\9b ।",
        "enotif_reset": "सबै पृष्ठहरू भनी दाग दिने",
        "enotif_impersonal_salutation": "{{SITENAME}} प्रयोगकर्ता",
        "enotif_subject_deleted": "{{SITENAME}} पृष्ठ $1 $2 ले {{GENDER:$2|मेटाउनु}} भयो ।",
        "whatlinkshere-links": "← लिंकहरू",
        "whatlinkshere-hideredirs": "$1 अनुप्रेषित हुन्छ",
        "whatlinkshere-hidetrans": "$1 पारदर्शन",
-       "whatlinkshere-hidelinks": "$1 à¤²à¤¿à¤\99à¥\8dà¤\95हरà¥\81",
+       "whatlinkshere-hidelinks": "$1 à¤²à¤¿à¤\99à¥\8dà¤\95हरà¥\82",
        "whatlinkshere-hideimages": "$1 फाइल लिंकहरू",
        "whatlinkshere-filters": "फिल्टरहरू",
        "whatlinkshere-submit": "जानुहोस्",
        "import-upload-filename": "फाइल नाम:",
        "import-comment": "टिप्पणी :",
        "importtext": "कृपया स्रोत विकिबाट फाइल निर्यात गर्नका लागि [[Special:Export|निर्यात सुविधा]]को प्रयोग गर्नुहोस। यसलाई आफ्नो कम्प्युटरमा सङ्ग्रह गरे यहाँ अपलोड गर्नुहोस।",
-       "importstart": "पà¥\83षà¥\8dठ à¤\86यात à¤\97रिà¤\81दà¥\88...",
+       "importstart": "पृष्ठ आयात गरिदै...",
        "import-revision-count": "$1 {{PLURAL:$1|पुनरावलोकन|पुनरावलोकनहरु}}",
        "importnopages": "आयातगर्नको लागि कुनै पृष्ठ छैन।",
        "imported-log-entries": "आयातित $1 {{PLURAL:$1|लग प्रविष्टी|लग प्रविष्टीहरू}}",
index 5c69a6d..7d99e09 100644 (file)
        "history": "Geschiedenis",
        "history_short": "Geschiedenis",
        "updatedmarker": "bewerkt sinds mijn laatste bezoek",
-       "printableversion": "Printervriendelijke versie",
+       "printableversion": "Printvriendelijke versie",
        "permalink": "Permanente koppeling",
        "print": "Afdrukken",
        "view": "Lezen",
        "log-action-filter-newusers": "Type accountaanmaak:",
        "log-action-filter-patrol": "Soort markering:",
        "log-action-filter-protect": "Soort beveiliging:",
+       "log-action-filter-rights": "Soort verandering van rechten:",
+       "log-action-filter-upload": "Soort upload:",
        "log-action-filter-all": "Alles",
        "log-action-filter-block-block": "Blokkade",
        "log-action-filter-block-reblock": "Aanpassing van blokkade",
        "log-action-filter-block-unblock": "Opheffing van blokkade",
        "log-action-filter-delete-delete": "Verwijderen van pagina",
        "log-action-filter-delete-restore": "Terugplaatsen van pagina",
+       "log-action-filter-managetags-create": "Aanmaken van label",
+       "log-action-filter-managetags-delete": "Verwijderen van label",
+       "log-action-filter-managetags-activate": "Activeren van label",
+       "log-action-filter-managetags-deactivate": "Deactiveren van label",
+       "log-action-filter-move-move": "Verplaatsing zonder overschrijven van doorverwijzingen",
+       "log-action-filter-move-move_redir": "Verplaatsing met overschrijven van doorverwijzingen",
        "log-action-filter-newusers-create": "Aangemaakt door een anonieme gebruiker",
        "log-action-filter-newusers-create2": "Aangemaakt door een geregistreerde gebruiker",
        "log-action-filter-newusers-autocreate": "Automatische aanmaak",
        "log-action-filter-protect-move_prot": "Beveiliging verplaatst",
        "log-action-filter-rights-rights": "Handmatige aanpassing",
        "log-action-filter-rights-autopromote": "Automatische aanpassing",
+       "log-action-filter-suppress-event": "Verbergen van logboekregel",
+       "log-action-filter-suppress-revision": "Verbergen van versie",
+       "log-action-filter-suppress-delete": "Verbergen van pagina",
        "log-action-filter-upload-upload": "Nieuwe upload",
        "log-action-filter-upload-overwrite": "Herupload",
        "authmanager-authn-autocreate-failed": "Het automatisch aanmaken van een lokaal account is mislukt: $1",
index c61a5c6..2ccdece 100644 (file)
        "right-applychangetags": "Aplicar [[Special:Tags|las balisas]] amb sas pròprias modificacions",
        "grant-generic": "ensemble de dreits « $1 »",
        "grant-blockusers": "Blocar e desblocar d'utilizaires",
-       "grant-patrol": "Marcar de paginas coma patrolhadas",
+       "grant-patrol": "Verificar las modificacions de paginas",
        "newuserlogpage": "Istoric de las creacions de comptes",
        "newuserlogpagetext": "Jornal de las creacions de comptes d'utilizaires.",
        "rightslog": "Istoric de las modificacions d'estatut",
        "newpageletter": "N",
        "boteditletter": "b",
        "number_of_watching_users_pageview": "[$1 {{PLURAL:$1|utilizaire seguent|utilizaires seguents}}]",
-       "rc_categories": "Limit de las categorias (separacion amb « | »)",
+       "rc_categories": "Limitar a las categorias (separadas per « | ») :",
        "rc_categories_any": "Una de las seleccionadas",
        "rc-change-size-new": "$1 {{PLURAL:$1|octet|octets}} aprèp cambiament",
        "newsectionsummary": "/* $1 */ seccion novèla",
        "undeletedrevisions": "{{PLURAL:$1|1 revision restablida|$1 revisions restablidas}}",
        "undeletedrevisions-files": "{{PLURAL:$1|1 revision|$1 revisions}} e {{PLURAL:$2|1 fichièr restablit|$2 fichièrs restablits}}",
        "undeletedfiles": "$1 {{PLURAL:$1|fichièr restablit|fichièrs restablits}}",
-       "cannotundelete": "Fracàs del restabliment :\n$1",
+       "cannotundelete": "Certanas o totas las restitucions an fracassat :\n$1",
        "undeletedpage": "<strong>La pagina $1 es estada restablida</strong>.\n\nConsultatz l’[[Special:Log/delete|istoric de las supressions]] per veire la lista de las supressions e dels restabliments recents.",
        "undelete-header": "Consultatz l’[[Special:Log/delete|istoric de las supressions]] per veire las paginas recentament suprimidas.",
        "undelete-search-title": "Recercar las paginas suprimidas",
        "tooltip-feed-rss": "Flux RSS per aquesta pagina",
        "tooltip-feed-atom": "Flux Atom per aquesta pagina",
        "tooltip-t-contributions": "Veire la lista de las contribucions d'{{GENDER:$1|aqueste utilizaire|aquesta utilizaira}}",
-       "tooltip-t-emailuser": "Mandar un corrièr electronic a aqueste utilizaire",
+       "tooltip-t-emailuser": "Mandar un corrièr electronic a {{GENDER:$1|aqueste utilizaire|aquesta utilizaira}}",
        "tooltip-t-info": "Mai d’informacion sus aquesta pagina",
        "tooltip-t-upload": "Mandar un imatge o fichièr mèdia sul servidor",
        "tooltip-t-specialpages": "Lista de totas las paginas especialas",
        "watchlistedit-raw-done": "Vòstra lista de seguiment es estada mesa a jorn.",
        "watchlistedit-raw-added": "{{PLURAL:$1|Una pagina es estada aponduda|$1 paginas son estadas apondudas}} :",
        "watchlistedit-raw-removed": "{{PLURAL:$1|Una pagina es estada levada|$1 paginas son estadas levadas}} :",
-       "watchlistedit-clear-title": "Lista de seguiment voidada",
+       "watchlistedit-clear-title": "Voidar la lista de seguiment",
        "watchlistedit-clear-legend": "Escafar la lista de seguiment",
        "watchlistedit-clear-explain": "Totes los títols seràn suprimits de vòstra lista de seguiment",
        "watchlistedit-clear-titles": "Títols :",
        "version-libraries-license": "Licéncia",
        "version-libraries-description": "Descripcion",
        "version-libraries-authors": "Autors",
-       "redirect": "Redirigit per fichièr, utilizaire, pagina o ID de revision.",
+       "redirect": "Redirigir per ID de fichièr, utilizaire, pagina, revision o jornal.",
        "redirect-submit": "Validar",
        "redirect-lookup": "Recèrca :",
        "redirect-value": "Valor :",
        "tags-edit-title": "Modificar las balisas",
        "tags-edit-manage-link": "Gerir las balisas",
        "tags-edit-existing-tags": "Balisas existentas :",
-       "tags-edit-existing-tags-none": "\"Pas cap\"",
+       "tags-edit-existing-tags-none": "<em>Pas cap</em>",
        "tags-edit-new-tags": "Balisas novèlas :",
        "tags-edit-add": "Apondre aquestas balisas :",
        "tags-edit-remove": "Suprimir aquestas balisas :",
index 810c6c5..cd4e304 100644 (file)
        "hide": "Скрыть",
        "show": "Показать",
        "minoreditletter": "м",
-       "newpageletter": "н",
+       "newpageletter": "Ð\9d",
        "boteditletter": "б",
        "unpatrolledletter": "!",
        "number_of_watching_users_pageview": "[$1 {{PLURAL:$1|наблюдающий участник|наблюдающих участника|наблюдающих участников}}]",
index c23dca5..05fa409 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.",
        "revdelete-selected-file": "{{PLURAL:$1|Vald filversion|Valda filversioner}} av [[:$2]]:",
        "logdelete-selected": "{{PLURAL:$1|Vald loggåtgärd|Valda loggåtgärder}}:",
        "revdelete-text-text": "Raderade sidversioner kommer fortfarande synas i sidans historik, men delar av innehållet kommer inte att vara tillgängligt offentligt.",
-       "revdelete-text-file": "Raderade filversioner kommer fortfarande synas i filens historik, men delar av innehållet kommer inte att bli tillgängligt offentligt.",
-       "logdelete-text": "Raderade logghändelser kommer fortfarande synas i loggarna, men delar av innehållet kommer inte att bli tillgängligt offentligt.",
+       "revdelete-text-file": "Raderade filversioner kommer fortfarande synas i filhistoriken, men delar av innehållet kommer att vara otillgängligt för allmänheten.",
+       "logdelete-text": "Raderade logghändelser kommer fortfarande synas i loggarna, men delar av innehållet kommer att vara otillgängligt för allmänheten.",
        "revdelete-text-others": "Andra administratörer kommer fortfarande att kunna komma åt det dolda innehållet och återställa det igen om inte ytterligare begränsningar används.",
        "revdelete-confirm": "Var god bekräfta att du vill göra detta, och att du förstår konsekvenserna, och att du gör så i enlighet med [[{{MediaWiki:Policy-url}}|policyn]].",
        "revdelete-suppress-text": "Undanhållande ska '''bara''' användas i följande fall:\n* Eventuell förolämpande information\n* Opassande personlig information\n*: ''hemadresser och telefonnummer, personnummer, etc.''",
        "search-relatedarticle": "Relaterad",
        "searchrelated": "relaterad",
        "searchall": "alla",
-       "showingresults": "Nedan visas upp till {{PLURAL:$1|'''1''' post|'''$1''' poster}} från och med nummer '''$2'''.",
+       "showingresults": "Nedan visas upp till {{PLURAL:$1|<strong>1</strong> resultat|<strong>$1</strong> resultat}} från och med nummer <strong>$2</strong>.",
        "showingresultsinrange": "Nedan visas upp till {{PLURAL:$3|<strong>1</strong> resultat|<strong>$1</strong> resultat}} mellan nummer <strong>$2</strong> och nummer <strong>$3</strong>.",
        "search-showingresults": "{{PLURAL:$4|Resultat <strong>$1</strong> av <strong>$3</strong>|Resultat <strong>$1 – $2</strong> av <strong>$3</strong>}}",
        "search-nonefound": "Inga resultat matchade frågan.",
        "email": "E-post",
        "prefs-help-realname": "Riktigt namn behöver inte anges.\nOm angivet, kan det komma att användas för att tillskriva dig ditt arbete.",
        "prefs-help-email": "Att ange e-postadress är valfritt, men gör det möjligt att få ditt lösenord mejlat till dig om du glömmer det.",
-       "prefs-help-email-others": "Du kan också välja att låta andra kontakta dig via e-post genom en länk på din användar- eller diskussionssida. Din e-postadress avslöjas inte när andra användare kontaktar dig.",
+       "prefs-help-email-others": "Du kan också välja att låta andra kontakta dig via e-post genom en länk på din användar- eller diskussionssida. \nDin e-postadress avslöjas inte när andra användare kontaktar dig.",
        "prefs-help-email-required": "E-postadress måste anges.",
        "prefs-info": "Grundläggande information",
        "prefs-i18n": "Internationalisering",
        "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",
        "filetype-mime-mismatch": "Filtillägget \".$1\" matchar inte med den identifierade MIME-typen för filen ($2).",
        "filetype-badmime": "Uppladdning av filer av MIME-typ \"$1\" är inte tillåtet.",
        "filetype-bad-ie-mime": "Kan inte ladda upp denna fil på grund av att Internet Explorer skulle upptäcka att den är \"$1\", vilket är en otillåten och möjligtvis farlig filtyp.",
-       "filetype-unwanted-type": "'''\".$1\"''' är en oönskad filtyp.\n{{PLURAL:$3|Föredragen filtyp|Föredragna filtyper}} är $2.",
-       "filetype-banned-type": "'''\".$1\"''' är inte {{PLURAL:$4|en tillåten filtyp|tillåtna filtyper}}.\n{{PLURAL:$3|Tillåtna filtyper|Tillåten filtyp}} är $2.",
+       "filetype-unwanted-type": "<strong>\".$1\"</strong> är en oönskad filtyp.\n{{PLURAL:$3|Föredragen filtyp|Föredragna filtyper}} är $2.",
+       "filetype-banned-type": "<strong>\".$1\"</strong> är inte {{PLURAL:$4|en tillåten filtyp|tillåtna filtyper}}.\n{{PLURAL:$3|Tillåten filtyp|Tillåtna filtyper}} är $2.",
        "filetype-missing": "Filnamnet saknar ändelse (t ex \".jpg\").",
        "empty-file": "Filen du skickade var tom.",
        "file-too-large": "Filen du skickade var för stor.",
        "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": "Det finns redan en fil med detta namn. Titta på <strong>[[:$1]]</strong>, om {{GENDER:|du}} inte är säker på att {{GENDER:|du}} vill ändra den.\n[[$1|thumb]]",
        "filepageexists": "Beskrivningssidan för denna fil har redan skapats på <strong>[[:$1]]</strong>, men just nu finns ingen fil med detta namn.\nDen sammanfattning du skriver här kommer inte visas på beskrivningssidan.\nFör att din sammanfattning ska visas där, så måste du redigera beskrivningssidan manuellt.\n[[$1|thumb]]",
        "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.",
+       "fileexists-thumbnail-yes": "Filen verkar vara en bild med förminskad storlek <em>(miniatyrbild)</em>. [[$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.",
-       "file-deleted-duplicate-notitle": "En identisk fil till den här filen har tidigare raderats och titeln har undanhållits.\nDu borde be någon som kan se undanhållen fildata att granska situationen innan du försöker ladda upp den.",
+       "file-deleted-duplicate": "En identisk fil till den här filen ([[:$1]]) har tidigare raderats. \nDu bör kontrollera den filens raderingshistorik innan du fortsätter att ladda upp den på nytt.",
+       "file-deleted-duplicate-notitle": "En identisk fil till den här filen har tidigare raderats och titeln har undanhållits.\nDu borde be någon som kan se undanhållen fildata att granska situationen innan du försöker ladda upp den på nytt.",
        "uploadwarning": "Uppladdningsvarning",
        "uploadwarning-text": "Var god och ändra filbeskrivningen nedanför och försök igen.",
        "savefile": "Spara fil",
        "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",
        "trackingcategories-nodesc": "Ingen beskrivning tillgänglig.",
        "trackingcategories-disabled": "Kategorin är inaktiverad",
        "mailnologin": "Ingen adress att skicka till",
-       "mailnologintext": "För att kunna skicka e-post till andra användare, måste du vara [[Special:UserLogin|inloggad]] och ha angivit en korrekt e-postadress i dina [[Special:Preferences|användarinställningar]].",
+       "mailnologintext": "För att kunna skicka e-post till andra användare måste du vara [[Special:UserLogin|inloggad]] och ha angivit en korrekt e-postadress i dina [[Special:Preferences|användarinställningar]].",
        "emailuser": "Skicka e-post till den här användaren",
        "emailuser-title-target": "Skicka e-post till denna {{GENDER:$1|användare}}",
        "emailuser-title-notarget": "E-postanvändare",
        "watchlistanontext": "Du måste logga in för att se eller redigera din bevakningslista.",
        "watchnologin": "Inte inloggad",
        "addwatch": "Lägg till i bevakningslistan",
-       "addedwatchtext": "\"[[:$1]]\" har lagts till i din [[Special:Watchlist|bevakningslista]].",
+       "addedwatchtext": "\"[[:$1]]\" och dess diskussionssida har lagts till i din [[Special:Watchlist|bevakningslista]].",
        "addedwatchtext-talk": "\"[[:$1]]\" och dess associerade sida har lagts till i din [[Special:Watchlist|bevakningslista]].",
        "addedwatchtext-short": "Sidan \"$1\" har lagts till i din bevakningslista.",
        "removewatch": "Ta bort från bevakningslistan",
        "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",
        "undelete-search-prefix": "Sidor som börjar med:",
        "undelete-search-submit": "Sök",
        "undelete-no-results": "Inga sidor med sådan titel hittades i arkivet över raderade sidor.",
-       "undelete-filename-mismatch": "Filversionen med tidsstämpeln $1 kan inte återställas: filnamnet stämmer inte.",
-       "undelete-bad-store-key": "Filversionen med tidsstämpeln $1 kan inte återställas: filen saknades före radering.",
+       "undelete-filename-mismatch": "Filversionen med tidsstämpeln $1 kan inte återställas: Filnamnet stämmer inte.",
+       "undelete-bad-store-key": "Filversionen med tidsstämpeln $1 kan inte återställas: Filen saknades före radering.",
        "undelete-cleanup-error": "Fel vid radering av den oanvända arkivfilen \"$1\".",
        "undelete-missing-filearchive": "Filen med arkiv-ID $1 kunde inte återställas eftersom den inte finns i databasen. Filen kanske redan har återställts.",
        "undelete-error": "Kunde inte återställa sidan",
        "undelete-error-short": "Fel vid filåterställning: $1",
-       "undelete-error-long": "Fel inträffade när vid återställning av filen:\n\n$1",
-       "undelete-show-file-confirm": "Är du säker på att du vill visa en raderad version av filen \"<nowiki>$1</nowiki>\" från den $2 kl $3?",
+       "undelete-error-long": "Fel inträffade vid återställning av filen:\n\n$1",
+       "undelete-show-file-confirm": "Är du säker på att du vill visa en raderad version av filen \"<nowiki>$1</nowiki>\" från den $2 kl. $3?",
        "undelete-show-file-submit": "Ja",
        "namespace": "Namnrymd:",
        "invert": "Invertera val",
        "tooltip-namespace_association": "Markera denna ruta för att även inkludera diskussions- eller ämnesnamnrymden som är associerad med den valda namnrymden",
        "blanknamespace": "(Huvudnamnrymden)",
        "contributions": "{{GENDER:$1|Användarbidrag}}",
-       "contributions-title": "Bidrag av $1",
+       "contributions-title": "Användarbidrag av $1",
        "mycontris": "Bidrag",
        "anoncontribs": "Bidrag",
        "contribsub2": "För {{GENDER:$3|$1}} ($2)",
        "sp-contributions-logs": "loggar",
        "sp-contributions-talk": "diskussion",
        "sp-contributions-userrights": "hantering av användarrättigheter",
-       "sp-contributions-blocked-notice": "Användaren är blockerad.\nOrsaken till senaste blockeringen kan ses nedan:",
+       "sp-contributions-blocked-notice": "Användaren är blockerad.\nDen senaste posten i blockeringsloggen visas nedan som referens:",
        "sp-contributions-blocked-notice-anon": "Denna IP-adress är för närvarande blockerad.\nDen senaste posten i blockeringsloggen visas nedan som referens:",
        "sp-contributions-search": "Sök efter användarbidrag",
        "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\"",
        "whatlinkshere-page": "Sida:",
        "linkshere": "Följande sidor länkar till <strong>[[:$1]]</strong>:",
-       "nolinkshere": "Inga sidor länkar till '''[[:$1]]'''.",
-       "nolinkshere-ns": "Inga sidor i den angivna namnrymden länkar till '''[[:$1]]'''.",
+       "nolinkshere": "Inga sidor länkar till <strong>[[:$1]]</strong>.",
+       "nolinkshere-ns": "Inga sidor i den angivna namnrymden länkar till <strong>[[:$1]]</strong>.",
        "isredirect": "omdirigeringssida",
        "istemplate": "inkluderad som mall",
        "isimage": "fillänk",
        "ipbemailban": "Hindra användaren från att skicka e-post",
        "ipbenableautoblock": "Blockera automatiskt den IP-adress som användaren använde senast, samt alla adresser som användaren försöker redigera ifrån",
        "ipbsubmit": "Blockera användaren",
-       "ipbother": "Annan tidsperiod:",
+       "ipbother": "Annan tid:",
        "ipboptions": "2 timmar:2 hours,1 dygn:1 day,3 dygn:3 days,1 vecka:1 week,2 veckor:2 weeks,1 månad:1 month,3 månader:3 months,6 månader:6 months,1 år:1 year,oändlig:infinite",
        "ipbhidename": "Dölj användarnamnet från redigeringar och listor",
        "ipbwatchuser": "Bevaka användarens användarsida och diskussionssida",
        "createaccountblock": "kontoregistrering blockerad",
        "emailblock": "e-post blockerad",
        "blocklist-nousertalk": "kan inte redigera sin egen diskussionssida",
-       "ipblocklist-empty": "Listan över blockerade IP-adresser är tom.",
+       "ipblocklist-empty": "Listan över blockeringar är tom.",
        "ipblocklist-no-results": "Den angivna IP-adressen eller användaren är inte blockerad.",
        "blocklink": "blockera",
        "unblocklink": "ta bort blockering",
        "blocklogpage": "Blockeringslogg",
        "blocklog-showlog": "Denna användare har blivit blockerad tidigare.\nBlockeringsloggen är tillgänglig nedan som referens:",
        "blocklog-showsuppresslog": "Denna användare har tidigare blivit blockerad och dold.\nUndanhållandeloggen visas nedan för referens:",
-       "blocklogentry": "blockerade [[$1]] med blockeringstid på $2 $3",
+       "blocklogentry": "blockerade [[$1]] med en varaktighet på $2 $3",
        "reblock-logentry": "ändrade blockeringsinställningar för [[$1]] med en varaktighet på $2 $3",
        "blocklogtext": "Detta är en logg över blockeringar och avblockeringar.\nAutomatiskt blockerade IP-adresser listas ej.\nSe [[Special:BlockList|blockeringslistan]] för en översikt av gällande blockeringar.",
-       "unblocklogentry": "tog bort blockering av \"$1\"",
+       "unblocklogentry": "tog bort blockering av $1",
        "block-log-flags-anononly": "bara oinloggade",
        "block-log-flags-nocreate": "hindrar kontoregistrering",
        "block-log-flags-noautoblock": "utan automatblockering",
        "block-log-flags-angry-autoblock": "utökad automatblockering aktiverad",
        "block-log-flags-hiddenname": "användarnamn dolt",
        "range_block_disabled": "Möjligheten för administratörer att blockera intervall av IP-adresser har stängts av.",
-       "ipb_expiry_invalid": "Ogiltig varaktighetstid.",
+       "ipb_expiry_invalid": "Ogiltig utgångstid.",
        "ipb_expiry_old": "Utgångstiden har redan passerat.",
        "ipb_expiry_temp": "För att dölja användarnamnet måste blockeringen vara permanent.",
        "ipb_hide_invalid": "Kan inte undanhålla detta konto; det har fler än {{PLURAL:$1|en redigering|$1 redigeringar}}.",
        "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",
        "immobile-source-page": "Denna sida är inte flyttbar.",
        "immobile-target-page": "Kan inte flytta till det målnamnet.",
        "bad-target-model": "Den önskade destinationen använder en annan innehållsmodell. Kan inte konvertera från $1 till $2.",
-       "imagenocrossnamespace": "Kan inte flytta filer till andra namnrymder än filnamnrymden",
-       "nonfile-cannot-move-to-file": "Kan inte flytta icke-fil till filnamnrymden",
-       "imagetypemismatch": "Den nya filändelsen motsvarar inte filtypen",
-       "imageinvalidfilename": "Önskat filnamn är ogiltigt",
+       "imagenocrossnamespace": "Kan inte flytta filer till andra namnrymder än filnamnrymden.",
+       "nonfile-cannot-move-to-file": "Kan inte flytta icke-fil till filnamnrymden.",
+       "imagetypemismatch": "Den nya filändelsen motsvarar inte filtypen.",
+       "imageinvalidfilename": "Önskat filnamn är ogiltigt.",
        "fix-double-redirects": "Uppdatera omdirigeringar som leder till den gamla titeln",
        "move-leave-redirect": "Lämna kvar en omdirigering",
        "protectedpagemovewarning": "'''Varning:''' Den här sidan har låsts så att endast användare med administratörsrättigheter kan flytta den.\nDen senaste loggposten tillhandahålls nedan som referens:",
        "exporttext": "Du kan exportera text och versionshistorik för en eller flera sidor i XML-format.\nFilen kan sedan importeras till en annan MediaWiki-wiki med hjälp av sidan [[Special:Import|importera]].\n\nExportera sidor genom att skriva in sidtitlarna i rutan här nedan.\nSkriv en titel per rad och välj om du du vill exportera alla versioner av texten med sidhistorik, eller om du enbart vill exportera den nuvarande versionen med information om den senaste redigeringen.\n\nI det senare fallet kan du även använda en länk, exempel [[{{#Special:Export}}/{{MediaWiki:Mainpage}}]] för sidan \"[[{{MediaWiki:Mainpage}}]]\".",
        "exportall": "Exportera alla sidor",
        "exportcuronly": "Inkludera endast den nuvarande versionen, inte hela historiken",
-       "exportnohistory": "----\n'''OBS:''' export av fullständig sidhistorik med hjälp av detta formulär har stängts av på grund av prestandaskäl.",
+       "exportnohistory": "----\n<strong>OBS:</strong> Export av fullständig sidhistorik med hjälp av detta formulär har stängts av på grund av prestandaskäl.",
        "exportlistauthors": "Inkludera en fullständig lista över bidragsgivare för varje sida",
        "export-submit": "Exportera",
        "export-addcattext": "Lägg till sidor från kategori:",
        "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:",
        "allmessagesname": "Namn",
        "allmessagesdefault": "Standardtext",
        "allmessagescurrent": "Nuvarande text",
-       "allmessagestext": "Detta är en lista över alla meddelanden i namnrymden MediaWiki.\nBesök [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation MediaWiki Localisation] eller [https://translatewiki.net translatewiki.net] om du vill bidra till översättningen av MediaWiki.",
+       "allmessagestext": "Detta är en lista över alla systemmeddelanden i namnrymden MediaWiki.\nBesök [https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation MediaWiki Localisation] eller [https://translatewiki.net translatewiki.net] om du vill bidra till översättningen av MediaWiki.",
        "allmessagesnotsupportedDB": "Den här sidan kan inte användas eftersom '''$wgUseDatabaseMessages''' är avstängd.",
        "allmessages-filter-legend": "Filtrera",
        "allmessages-filter": "Filtrera efter anpassningsgrad:",
        "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",
        "file-info-size": "$1 × $2 pixlar, filstorlek: $3, MIME-typ: $4",
        "file-info-size-pages": "$1 × $2 pixlar, filstorlek: $3, MIME-typ: $4, $5 {{PLURAL:$5|sida|sidor}}",
        "file-nohires": "Det finns ingen version med högre upplösning.",
-       "svg-long-desc": "SVG-fil, grundstorlek: $1 × $2 pixlar, filstorlek: $3",
+       "svg-long-desc": "SVG-fil, standardstorlek: $1 × $2 pixlar, filstorlek: $3",
        "svg-long-desc-animated": "Animerad SVG-fil, standardstorlek $1 × $2 pixlar, filstorlek: $3",
        "svg-long-error": "Felaktig SVG-fil: $1",
        "show-big-image": "Originalfil",
        "exif-gpsspeed-m": "Miles i timmen",
        "exif-gpsspeed-n": "Knop",
        "exif-gpsdestdistance-k": "Kilometer",
-       "exif-gpsdestdistance-m": "Mil",
+       "exif-gpsdestdistance-m": "Miles",
        "exif-gpsdestdistance-n": "Nautiska mil",
        "exif-gpsdop-excellent": "Utmärkt ($1)",
        "exif-gpsdop-good": "Bra ($1)",
        "confirmemail": "Bekräfta e-postadress",
        "confirmemail_noemail": "Du har inte angivit någon giltig e-postadress i dina [[Special:Preferences|inställningar]].",
        "confirmemail_text": "Innan du kan använda {{SITENAME}}s funktioner för e-post måste du bekräfta din e-postadress. Aktivera knappen nedan för att skicka en bekräftelsekod till din e-postadress. Mailet kommer att innehålla en länk, som innehåller en kod. Genom att klicka på den länken eller kopiera den till din webbläsares fönster för webbadresser, bekräftar du att din e-postadress fungerar.",
-       "confirmemail_pending": "En bekräftelsekod har redan skickats till din epostadress. Om du skapade ditt konto nyligen, så kanske du vill vänta några minuter innan du begär en ny kod.",
+       "confirmemail_pending": "En bekräftelsekod har redan skickats till din e-postadress. Om du skapade ditt konto nyligen, så kanske du vill vänta några minuter innan du begär en ny kod.",
        "confirmemail_send": "Skicka bekräftelsekod",
        "confirmemail_sent": "E-post med bekräftelse skickat.",
        "confirmemail_oncreate": "En bekräftelsekod skickades till din epostadress. Koden behövs inte för att logga in, men du behöver koden för att få tillgång till de epostbaserade funktionerna på wikin.",
        "confirmemail_body": "Någon, troligen du, har från IP-adressen $1 registrerat användarkontot \"$2\" med denna e-postadress på {{SITENAME}}.\n\nFör att bekräfta att detta konto verkligen är ditt, och för att aktivera funktionerna för e-post på {{SITENAME}}, öppna denna länk i din webbläsare:\n\n$3\n\nOm det *inte* är du som registrerat kontot, följ denna länk för att avbryta bekräftelsen av e-postadressen:\n\n$5\n\nDenna bekräftelsekod kommer inte att fungera efter $4.",
        "confirmemail_body_changed": "Någon, troligen du, har från IP-adressen $1\nregistrerat användarkontot \"$2\" med denna e-postadress på {{SITENAME}}.\n\nFör att bekräfta att detta konto verkligen är ditt, och för att aktivera\nfunktionerna för e-post på {{SITENAME}}, öppna denna länk i din webbläsare:\n\n$3\n\nOm det *inte* är du som registrerat kontot, följ denna länk\nför att avbryta bekräftelsen av e-postadressen:\n\n$5\n\nDenna bekräftelsekod kommer inte att fungera efter $4.",
        "confirmemail_body_set": "Någon, förmodligen du, från IP-adressen $1,\nhar angivit e-postadressen till kontot \"$2\" till den här adressen på {{SITENAME}}.\n\nFör att bekräfta att kontot verkligen tillhör dig, bör du aktivera e-postfunktionerna på {{SITENAME}}, öppna denna länk i din webbläsare:\n\n$3\n\nOm kontot *inte* tillhör dig, följ den här länken för att avbryta bekräftelsen av e-postadressen:\n\n$5\n\nDenna bekräftelsekod kommer att sluta fungera efter $4.",
-       "confirmemail_invalidated": "Bekräftelsen av e-postadressen har ogiltigförklarats",
+       "confirmemail_invalidated": "Bekräftelsen av e-postadressen har avbrutits",
        "invalidateemail": "Avbryt bekräftelse av e-postadress",
        "notificationemail_subject_changed": "Registrerad e-postadress på {{SITENAME}} har ändrats",
        "notificationemail_subject_removed": "Registrerad e-postadress på {{SITENAME}} har tagits bort",
        "table_pager_prev": "Föregående sida",
        "table_pager_first": "Första sidan",
        "table_pager_last": "Sista sidan",
-       "table_pager_limit": "Visa $1 poster per sida",
+       "table_pager_limit": "Visa $1 objekt per sida",
        "table_pager_limit_label": "Objekt per sida:",
        "table_pager_limit_submit": "Utför",
        "table_pager_empty": "Inga resultat",
        "lag-warn-high": "På grund av omfattande fördröjning i databasen visas kanske inte ändringar nyare än $1 {{PLURAL:$1|sekund|sekunder}} i den här listan.",
        "watchlistedit-normal-title": "Redigera bevakningslista",
        "watchlistedit-normal-legend": "Ta bort sidor från bevakningslistan",
-       "watchlistedit-normal-explain": "Titlar på din bevakningslista visas nedan.\nFör att ta bort en titel, markera rutan bredvid den och klicka på \"{{int:Watchlistedit-normal-submit}}\".\nDu kan också [[Special:EditWatchlist/raw|redigera listan i råformat]].",
+       "watchlistedit-normal-explain": "Sidor på din bevakningslista visas nedan.\nFör att ta bort en sida, markera rutan bredvid den och klicka på \"{{int:Watchlistedit-normal-submit}}\".\nDu kan också [[Special:EditWatchlist/raw|redigera listan i råformat]].",
        "watchlistedit-normal-submit": "Ta bort sidor",
        "watchlistedit-normal-done": "{{PLURAL:$1|1 sida|$1 sidor}} togs bort från din bevakningslista:",
        "watchlistedit-raw-title": "Redigera bevakningslistan i råformat",
        "watchlistedit-raw-legend": "Redigera bevakningslistan i råformat",
-       "watchlistedit-raw-explain": "Titlar på din bevakningslista visas nedan, och kan redigeras genom att lägga till och ta bort från listan;\nen titel per rad.\nNär du är klar klickar du på \"{{int:Watchlistedit-raw-submit}}\".\nDu kan också [[Special:EditWatchlist|använda standardeditorn]].",
+       "watchlistedit-raw-explain": "Sidor på din bevakningslista visas nedan, och kan redigeras genom att lägga till och ta bort från listan;\nen sida per rad.\nNär du är klar klickar du på \"{{int:Watchlistedit-raw-submit}}\".\nDu kan också [[Special:EditWatchlist|använda standardeditorn]].",
        "watchlistedit-raw-titles": "Sidor:",
        "watchlistedit-raw-submit": "Uppdatera bevakningslistan",
        "watchlistedit-raw-done": "Din bevakningslista har uppdaterats.",
        "watchlistedit-clear-title": "Rensa bevakningslistan",
        "watchlistedit-clear-legend": "Rensa bevakningslistan",
        "watchlistedit-clear-explain": "Alla titlar kommer att tas bort från din bevakningslista",
-       "watchlistedit-clear-titles": "Titlar:",
+       "watchlistedit-clear-titles": "Sidor:",
        "watchlistedit-clear-submit": "Rensa bevakningslistan (Detta är permanent!)",
        "watchlistedit-clear-done": "Din bevakningslista har rensats.",
-       "watchlistedit-clear-removed": "{{PLURAL:$1|1 titel|$1 titlar}} togs bort:",
+       "watchlistedit-clear-removed": "{{PLURAL:$1|1 sida|$1 sidor}} togs bort:",
        "watchlistedit-too-many": "Det finns för många sidor att visa här.",
        "watchlisttools-clear": "Rensa bevakningslistan",
        "watchlisttools-view": "Visa relevanta ändringar",
        "tags-activate": "aktivera",
        "tags-deactivate": "inaktivera",
        "tags-hitcount": "$1 {{PLURAL:$1|ändring|ändringar}}",
-       "tags-manage-no-permission": "Du har inte behörighet att hantera förändringstaggar.",
-       "tags-manage-blocked": "Du kan inte hantera ändringsmärken när du är blockerad.",
+       "tags-manage-no-permission": "Du har inte behörighet att hantera förändringsmärken.",
+       "tags-manage-blocked": "Du kan inte hantera förändringsmärken när du är blockerad.",
        "tags-create-heading": "Skapa ett nytt märke",
-       "tags-create-explanation": "Som standard, kommer nyskapade taggar att bli tillgängliga för användning av användare och botar.",
+       "tags-create-explanation": "Som standard, kommer nyskapade märken att bli tillgängliga för användning av användare och botar.",
        "tags-create-tag-name": "Märkesnamn:",
        "tags-create-reason": "Anledning:",
        "tags-create-submit": "Skapa",
        "tags-deactivate-reason": "Anledning:",
        "tags-deactivate-not-allowed": "Det är inte möjligt att inaktivera märket \"$1\".",
        "tags-deactivate-submit": "Inaktivera",
-       "tags-apply-no-permission": "Du har inte behörighet att tillämpa märken på dina ändringar",
+       "tags-apply-no-permission": "Du har inte behörighet att tillämpa ändringsmärken på dina ändringar.",
        "tags-apply-blocked": "Du kan inte ange ändringsmärken med dina ändringar medans du är blockerad.",
        "tags-apply-not-allowed-one": "Märket \"$1\" kan inte läggas till manuellt.",
        "tags-apply-not-allowed-multi": "Följande {{PLURAL:$2|märke|märken}} kan inte läggas till manuellt: $1",
        "log-name-managetags": "Märkeshanteringslogg",
        "log-description-managetags": "Denna sida innehåller administrativa [[Special:Tags|märke]]srelaterade uppgifter. Loggen innehåller bara åtgärder som utförts manuellt av en administratör; märken kan skapas eller raderas av wikins mjukvara utan att en post registreras i loggen.",
        "logentry-managetags-create": "$1 {{GENDER:$2|skapade}} märket \"$4\"",
-       "logentry-managetags-delete": "$1 {{GENDER:$2|raderade}} märket \"$4\" (borttagen från $5 {{PLURAL:$5|version eller loggpost|versioner och/eller loggposter}})",
+       "logentry-managetags-delete": "$1 {{GENDER:$2|raderade}} märket \"$4\" (borttagen från $5 {{PLURAL:$5|sidversion eller loggpost|sidversioner och/eller loggposter}})",
        "logentry-managetags-activate": "$1 {{GENDER:$2|aktiverade}} märket \"$4\" för användning av användare och botar.",
        "logentry-managetags-deactivate": "$1 {{GENDER:$2|inaktiverade}} märket \"$4\" för användning av användare och botar.",
        "log-name-tag": "Märkeslogg",
        "log-description-tag": "Denna sida visar när användare har lagt till eller tagit bort [[Special:Tags|märken]] från individuella sidversioner eller loggposter. Loggen registrerar inte handlingar där märken hanteras i redigeringar, raderingar eller liknande handlingar.",
        "logentry-tag-update-add-revision": "$1 {{GENDER:$2|lade till}} {{PLURAL:$7|märket|märkena}} $6 för sidversionen $4 av sidan $3",
-       "logentry-tag-update-add-logentry": "$1 {{GENDER:$2|lade till}} {{PLURAL:$7|märket|märkena}} $6 till loggposten $5 för siden $3",
+       "logentry-tag-update-add-logentry": "$1 {{GENDER:$2|lade till}} {{PLURAL:$7|märket|märkena}} $6 till loggposten $5 för sidan $3",
        "logentry-tag-update-remove-revision": "$1 {{GENDER:$2|tog bort}} {{PLURAL:$9|märket|märkena}} $8 från sidversionen $4 av sidan $3",
        "logentry-tag-update-remove-logentry": "$1 {{GENDER:$2|tog bort}} {{PLURAL:$9|märket|märkena}} $8 från loggposten $5 för sidan $3",
        "logentry-tag-update-revision": "$1 {{GENDER:$2|uppdaterade}} märken på sidversionen $4 för sidan $3 ({{PLURAL:$7|lade till}} $6; {{PLURAL:$9|tog bort}} $8)",
        "api-error-badaccess-groups": "Du får inte ladda upp filer till denna wiki.",
        "api-error-badtoken": "Internt fel: felaktig nyckel.",
        "api-error-blocked": "Du har blockerats från att redigera.",
-       "api-error-copyuploaddisabled": "Uppladdning via URL är inaktiverad på den här servern.",
+       "api-error-copyuploaddisabled": "Uppladdning via URL är inaktiverat på den här servern.",
        "api-error-duplicate": "Det finns redan {{PLURAL:$1|en annan fil|andra filer}} på webbplatsen med samma innehåll.",
        "api-error-duplicate-archive": "Det fanns redan {{PLURAL:$1|en annan fil|några andra filer}} på webbplatsen med samma innehåll, men {{PLURAL:$1|den har|de har}} raderats.",
        "api-error-empty-file": "Filen du skickade var tom.",
        "api-error-filename-tooshort": "Filnamnet är för kort.",
        "api-error-filetype-banned": "Denna typ av fil är förbjuden.",
        "api-error-filetype-banned-type": "$1 är inte {{PLURAL:$4|en tillåten filtyp|tillåtna filtyper}}. {{PLURAL:$3|Tillåten filtyp|Tillåtna filtyper}} är $2.",
-       "api-error-filetype-missing": "Filen saknar en filändelse.",
+       "api-error-filetype-missing": "Filnamnet saknar en filändelse.",
        "api-error-hookaborted": "Ändringen du försökte göra avbröts av en extension hook.",
        "api-error-http": "Internt fel: Det gick inte att ansluta till servern.",
        "api-error-illegal-filename": "Filnamnet är inte tillåtet.",
-       "api-error-internal-error": "Internt fel: något gick fel med bearbetningen av din uppladdning på wikin.",
+       "api-error-internal-error": "Internt fel: Något gick fel med bearbetningen av din uppladdning på wikin.",
        "api-error-invalid-file-key": "Internt fel: filen hittades inte i tillfällig lagring.",
-       "api-error-missingparam": "Internt fel: det saknas parametrar i begäran.",
-       "api-error-missingresult": "Internt fel: kunde inte avgöra om kopieringen lyckades.",
+       "api-error-missingparam": "Internt fel: Det saknas parametrar i begäran.",
+       "api-error-missingresult": "Internt fel: Kunde inte avgöra om kopieringen lyckades.",
        "api-error-mustbeloggedin": "Du måste vara inloggad för att kunna ladda upp filer.",
        "api-error-mustbeposted": "Det finns en bugg i detta program, det använder inte rätt HTTP-metod.",
        "api-error-noimageinfo": "Uppladdningen lyckades, men servern gav oss inte någon information om filen.",
-       "api-error-nomodule": "Internt fel: ingen uppladdningsmodul uppsatt.",
+       "api-error-nomodule": "Internt fel: Ingen uppladdningsmodul uppsatt.",
        "api-error-ok-but-empty": "Internt fel: Inget svar från servern.",
        "api-error-overwrite": "Det är inte tillåtet att skriva över en befintlig fil.",
-       "api-error-ratelimited": "Du försöker ladda upp fler filer inom en kort tidsrymd än denna wiki tillåter.\nFörsök igen om några minuter.",
+       "api-error-ratelimited": "Du försöker ladda upp fler filer inom en kortare tidsrymd än denna wiki tillåter.\nFörsök igen om några minuter.",
        "api-error-stashfailed": "Internt fel: servern kunde inte lagra temporär fil.",
        "api-error-publishfailed": "Internt fel: Servern kunde inte publicera temporär fil.",
        "api-error-stasherror": "Ett fel uppstod under uppladdningen av filen till mellanlagringsfilen.",
        "api-error-stashwrongowner": "Filen du försöker komma åt i det temporära lagringsutrymmet tillhör inte dig.",
        "api-error-stashnosuchfilekey": "Filnyckeln som du försökte komma åt i den temporära lagringsytan existerar inte.",
        "api-error-timeout": "Servern svarade inte inom förväntad tid.",
-       "api-error-unclassified": "Ett okänt fel uppstod",
-       "api-error-unknown-code": "Okänt fel: \"$1\"",
+       "api-error-unclassified": "Ett okänt fel uppstod.",
+       "api-error-unknown-code": "Okänt fel: \"$1\".",
        "api-error-unknown-error": "Internt fel: något gick fel när vi försökte ladda upp din fil.",
-       "api-error-unknown-warning": "Okänd varning: $1",
+       "api-error-unknown-warning": "Okänd varning: \"$1\".",
        "api-error-unknownerror": "Okänt fel: \"$1\".",
        "api-error-uploaddisabled": "Uppladdning är inaktiverad på denna wiki.",
        "api-error-verification-error": "Denna fil kan vara skadad eller har fel filändelse.",
        "expand_templates_output": "Expanderad kod",
        "expand_templates_xml_output": "XML-kod",
        "expand_templates_html_output": "Rå HTML-utdata",
-       "expand_templates_ok": "Expandera",
+       "expand_templates_ok": "OK",
        "expand_templates_remove_comments": "Ta bort kommentarer",
        "expand_templates_remove_nowiki": "Undertryck <nowiki> taggar i resultatet",
        "expand_templates_generate_xml": "Visa parseträd som XML",
        "pagelang-use-default": "Använd standardspråk",
        "pagelang-select-lang": "Välj språk",
        "pagelang-submit": "Skicka",
-       "right-pagelang": "Ändra sidans språk",
+       "right-pagelang": "Ändra sidspråk",
        "action-pagelang": "ändra sidspråket",
        "log-name-pagelang": "Språkändringslogg",
        "log-description-pagelang": "Detta är en logg över ändringar i sidspråken.",
        "special-characters-group-ipa": "IPA",
        "special-characters-group-symbols": "Symboler",
        "special-characters-group-greek": "Grekiska",
-       "special-characters-group-greekextended": "Grekiska utvidgad",
-       "special-characters-group-cyrillic": "Kyrilliskt",
+       "special-characters-group-greekextended": "Utökad grekiska",
+       "special-characters-group-cyrillic": "Kyrilliska",
        "special-characters-group-arabic": "Arabiska",
-       "special-characters-group-arabicextended": "Arabiska utökade",
+       "special-characters-group-arabicextended": "Utökad arabiska",
        "special-characters-group-persian": "Persiska",
        "special-characters-group-hebrew": "Hebreiska",
        "special-characters-group-bangla": "Bengali",
        "special-characters-group-gujarati": "Gujarati",
        "special-characters-group-devanagari": "Devenagari",
        "special-characters-group-thai": "Thai",
-       "special-characters-group-lao": "Laotisk",
+       "special-characters-group-lao": "Laotiska",
        "special-characters-group-khmer": "Khmer",
        "special-characters-title-endash": "tankstreck",
        "special-characters-title-emdash": "långt tankstreck",
        "log-action-filter-block": "Typ av blockering:",
        "log-action-filter-contentmodel": "Typ av innehållsmodellsändring:",
        "log-action-filter-delete": "Typ av radering:",
-       "log-action-filter-import": "Importeringstyp:",
+       "log-action-filter-import": "Typ av importering:",
        "log-action-filter-managetags": "Typ av märkeshanteringsåtgärd:",
-       "log-action-filter-move": "Flyttningstyp:",
+       "log-action-filter-move": "Typ av flyttning:",
        "log-action-filter-newusers": "Typ av kontoskapande:",
        "log-action-filter-patrol": "Typ av patrullering:",
        "log-action-filter-protect": "Typ av skydd:",
        "log-action-filter-rights": "Typ av rättighetsändring:",
-       "log-action-filter-suppress": "Censurtyp:",
+       "log-action-filter-suppress": "Typ av censur:",
        "log-action-filter-upload": "Typ av uppladdning:",
        "log-action-filter-all": "Alla",
        "log-action-filter-block-block": "Blockering",
        "log-action-filter-contentmodel-change": "Ändring av innehållsmodell",
        "log-action-filter-contentmodel-new": "Skapande av sida med icke-standardiserad innehållsmodell",
        "log-action-filter-delete-delete": "Radering av sida",
-       "log-action-filter-delete-restore": "Återställde sida",
+       "log-action-filter-delete-restore": "Återställning av sida",
        "log-action-filter-delete-event": "Radering av logg",
        "log-action-filter-delete-revision": "Radering av sidversion",
        "log-action-filter-import-interwiki": "Interwikiimport",
        "authmanager-email-label": "E-post",
        "authmanager-email-help": "E-postadress",
        "authmanager-realname-label": "Riktigt namn",
-       "authmanager-realname-help": "Användarens riktiga namnet",
+       "authmanager-realname-help": "Användarens riktiga namn",
        "authmanager-provider-password": "Lösenordsbaserad autentisering",
        "authmanager-provider-password-domain": "Lösenord- och domänbaserad autentisering",
        "authmanager-provider-temporarypassword": "Tillfälligt lösenord",
index 7f2f2ea..4dad914 100644 (file)
        "statistics-pages": "పేజీలు",
        "statistics-pages-desc": "ఈ వికీలోని అన్ని పేజీలు (చర్చా పేజీలు, దారిమార్పులు, మొదలైనవన్నీ కలుపుకొని).",
        "statistics-files": "ఎక్కించిన దస్త్రాలు",
-       "statistics-edits": "{{SITENAME}}ని మొదలుపెట్టినప్పటినుండి జరిగిన మార్పులు",
+       "statistics-edits": "{{SITENAME}} మొదలుపెట్టినప్పటినుండి జరిగిన మార్పులు",
        "statistics-edits-average": "పేజీకి సగటు మార్పులు",
        "statistics-users": "నమోదైన [[Special:ListUsers|వాడుకరులు]]",
        "statistics-users-active": "క్రియాశీల వాడుకరులు",
        "watchlist-submit": "చూపించు",
        "wlshowtime": "చూపించాల్సిన కాలం:",
        "wlshowhideminor": "చిన్న మార్పులు",
-       "wlshowhidebots": "బాట్లు",
+       "wlshowhidebots": "బాట్లు",
        "wlshowhideliu": "నమోదైన వాడుకరులు",
        "wlshowhideanons": "అజ్ఞాత వాడుకరులు",
        "wlshowhidemine": "నా మార్పులు",
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 fa7133a..c194377 100644 (file)
        "minoredit": "Votükam pülik",
        "watchthis": "Galädolöd padi at",
        "savearticle": "Dakipolöd padi",
+       "publishpage": "Dabükön padi",
+       "publishchanges": "Dabükön votükamis",
        "preview": "Büologed",
        "showpreview": "Jonolöd padalogoti",
        "showdiff": "Jonolöd votükamis",
        "undo-norev": "No eplöpos ad sädunön redakami at, bi no dabinon u pämoükon.",
        "undo-summary": "Äsädunon votükami $1 fa [[Special:Contributions/$2|$2]] ([[User talk:$2|Bespikapad]])",
        "undo-summary-username-hidden": "Sädunön revidi: $1 fa geban peklenädöl",
-       "cantcreateaccounttitle": "Kal no kanon pajafön",
        "cantcreateaccount-text": "Kalijaf se ladet-IP at ('''$1''') peblokon fa geban: [[User:$3|$3]].\n\nKod blokama fa el $3 pegivöl binon ''$2''",
        "viewpagelogs": "Jonön jenotalisedis pada at",
        "nohistory": "Pad at no labon redakamajenotemi.",
index ef56cd3..5f45176 100644 (file)
@@ -571,6 +571,7 @@ return [
        ],
        'jquery.ui.position' => [
                'deprecated' => true,
+               'targets' => [ 'mobile', 'desktop' ],
                'scripts' => 'resources/lib/jquery.ui/jquery.ui.position.js',
                'group' => 'jquery.ui',
        ],
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 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 af37162..78c674c 100644 (file)
                                cssBuffer = '',
                                cssBufferTimer = null,
                                cssCallbacks = $.Callbacks(),
-                               isIE9 = document.documentMode === 9;
+                               isIE9 = document.documentMode === 9,
+                               rAF = window.requestAnimationFrame || setTimeout;
 
                        function getMarker() {
                                if ( !marker ) {
                                        if ( !cssBuffer || cssText.slice( 0, '@import'.length ) !== '@import' ) {
                                                // Linebreak for somewhat distinguishable sections
                                                cssBuffer += '\n' + cssText;
-                                               // TODO: Using requestAnimationFrame would perform better by not injecting
-                                               // styles while the browser is busy painting.
                                                if ( !cssBufferTimer ) {
-                                                       cssBufferTimer = setTimeout( function () {
+                                                       cssBufferTimer = rAF( function () {
+                                                               // Wrap in anonymous function that takes no arguments
                                                                // Support: Firefox < 13
                                                                // Firefox 12 has non-standard behaviour of passing a number
                                                                // as first argument to a setTimeout callback.
                var loading = $.grep( mw.loader.getModuleNames(), function ( module ) {
                        return mw.loader.getState( module ) === 'loading';
                } );
-               // In order to use jQuery.when (which stops early if one of the promises got rejected)
-               // cast any loading failures into successes. We only need a callback, not the module.
-               loading = $.map( loading, function ( module ) {
-                       return mw.loader.using( module ).then( null, function () {
-                               return $.Deferred().resolve();
+               // We only need a callback, not any actual module. First try a single using()
+               // for all loading modules. If one fails, fall back to tracking each module
+               // separately via $.when(), this is expensive.
+               loading = mw.loader.using( loading ).then( null, function () {
+                       var all = $.map( loading, function ( module ) {
+                               return mw.loader.using( module ).then( null, function () {
+                                       return $.Deferred().resolve();
+                               } );
                        } );
+                       return $.when.apply( $, all );
                } );
-               $.when.apply( $, loading ).then( function () {
+               loading.then( function () {
                        mwPerformance.mark( 'mwLoadEnd' );
                        mw.hook( 'resourceloader.loadEnd' ).fire();
                } );
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 0751409..16297ad 100644 (file)
@@ -291,6 +291,56 @@ class DatabaseTest extends MediaWikiTestCase {
                $this->assertTrue( $called, 'Callback reached' );
        }
 
+       /**
+        * @covers DatabaseBase::setTransactionListener()
+        */
+       public function testTransactionListener() {
+               $db = $this->db;
+
+               $db->setTransactionListener( 'ping', function() use ( $db, &$called ) {
+                       $called = true;
+               } );
+
+               $called = false;
+               $db->begin( __METHOD__ );
+               $db->commit( __METHOD__ );
+               $this->assertTrue( $called, 'Callback reached' );
+
+               $called = false;
+               $db->begin( __METHOD__ );
+               $db->commit( __METHOD__ );
+               $this->assertTrue( $called, 'Callback still reached' );
+
+               $called = false;
+               $db->begin( __METHOD__ );
+               $db->rollback( __METHOD__ );
+               $this->assertTrue( $called, 'Callback reached' );
+
+               $db->setTransactionListener( 'ping', null );
+               $called = false;
+               $db->begin( __METHOD__ );
+               $db->commit( __METHOD__ );
+               $this->assertFalse( $called, 'Callback not reached' );
+       }
+
+       /**
+        * @covers DatabaseBase::clearSnapshot()
+        */
+       public function testClearSnapshot() {
+               $db = $this->db;
+
+               $db->clearSnapshot( __METHOD__ ); // ok
+               $db->clearSnapshot( __METHOD__ ); // ok
+
+               $db->setFlag( DBO_TRX, $db::REMEMBER_PRIOR );
+               $db->query( 'SELECT 1', __METHOD__ );
+               $this->assertTrue( (bool)$db->trxLevel(), "Transaction started." );
+               $db->clearSnapshot( __METHOD__ ); // ok
+               $db->restoreFlags( $db::RESTORE_PRIOR );
+
+               $this->assertFalse( (bool)$db->trxLevel(), "Transaction cleared." );
+       }
+
        public function testGetScopedLock() {
                $db = $this->db;
 
index 35005f5..aeb4666 100644 (file)
@@ -722,4 +722,27 @@ class WANObjectCacheTest extends MediaWikiTestCase {
                $wanCache->getWithSetCallback( 'p', 30, $valFunc );
                $wanCache->getCheckKeyTime( 'zzz' );
        }
+
+       /**
+        * @dataProvider provideAdaptiveTTL
+        * @covers WANObjectCache::adaptiveTTL()
+        */
+       public function testAdaptiveTTL( $ago, $maxTTL, $minTTL, $factor, $adaptiveTTL ) {
+               $mtime = is_int( $ago ) ? time() - $ago : $ago;
+               $margin = 5;
+               $ttl = $this->cache->adaptiveTTL( $mtime, $maxTTL, $minTTL, $factor );
+
+               $this->assertGreaterThanOrEqual( $adaptiveTTL - $margin, $ttl );
+               $this->assertLessThanOrEqual( $adaptiveTTL + $margin, $ttl );
+       }
+
+       public static function provideAdaptiveTTL() {
+               return [
+                       [ 3600, 900, 30, .2, 720 ],
+                       [ 3600, 500, 30, .2, 500 ],
+                       [ 3600, 86400, 800, .2, 800 ],
+                       [ false, 86400, 800, .2, 800 ],
+                       [ null, 86400, 800, .2, 800 ]
+               ];
+       }
 }
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' );