Merge "ObjectFactoryTest: Add tests for 'factory' option"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 17 Aug 2016 22:55:34 +0000 (22:55 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 17 Aug 2016 22:55:34 +0000 (22:55 +0000)
39 files changed:
RELEASE-NOTES-1.28
includes/EditPage.php
includes/GlobalFunctions.php
includes/MovePage.php
includes/api/ApiBase.php
includes/api/ApiLogin.php
includes/api/ApiMain.php
includes/api/ApiUpload.php
includes/auth/AuthManager.php
includes/auth/AuthenticationRequest.php
includes/auth/PrimaryAuthenticationProvider.php
includes/content/WikitextContentHandler.php
includes/db/DBConnRef.php
includes/db/Database.php
includes/db/DatabasePostgres.php
includes/db/IDatabase.php
includes/objectcache/SqlBagOStuff.php
includes/upload/UploadBase.php
includes/upload/UploadFromChunks.php
includes/upload/UploadFromStash.php
languages/i18n/az.json
languages/i18n/ckb.json
languages/i18n/cs.json
languages/i18n/diq.json
languages/i18n/gl.json
languages/i18n/he.json
languages/i18n/it.json
languages/i18n/kk-cyrl.json
languages/i18n/ko.json
languages/i18n/mr.json
languages/i18n/pt.json
languages/i18n/sl.json
languages/i18n/uk.json
languages/i18n/ur.json
languages/i18n/uz.json
languages/i18n/zh-hans.json
maintenance/cleanupCaps.php
tests/phpunit/includes/auth/AuthManagerTest.php
tests/phpunit/structure/ResourcesTest.php

index 48fe4ff..f6c3530 100644 (file)
@@ -108,6 +108,9 @@ changes to languages because of Phabricator reports.
   Use ...->stashFile()->getFileKey() instead.
 * "Public domain" was removed as a wiki license option from the installer, in
   favour of CC-0.
+* AuthenticationRequest::$required is now changed from REQUIRED to PRIMARY_REQUIRED
+  on requests needed by primary providers even if all primaries need them.
+  Primary providers are discouraged from returning multiple REQUIRED requests.
 
 == Compatibility ==
 
index ee06993..9b862b9 100644 (file)
@@ -1474,7 +1474,7 @@ class EditPage {
 
                        case self::AS_CANNOT_USE_CUSTOM_MODEL:
                        case self::AS_PARSE_ERROR:
-                               $wgOut->addWikiText( '<div class="error">' . $status->getWikiText() . '</div>' );
+                               $wgOut->addWikiText( '<div class="error">' . "\n" . $status->getWikiText() . '</div>' );
                                return true;
 
                        case self::AS_SUCCESS_NEW_ARTICLE:
@@ -1551,7 +1551,7 @@ class EditPage {
                                // is if an extension hook aborted from inside ArticleSave.
                                // Render the status object into $this->hookError
                                // FIXME this sucks, we should just use the Status object throughout
-                               $this->hookError = '<div class="error">' . $status->getWikiText() .
+                               $this->hookError = '<div class="error">' ."\n" . $status->getWikiText() .
                                        '</div>';
                                return true;
                }
index 7117f4c..0d66908 100644 (file)
@@ -811,7 +811,7 @@ function wfUrlProtocolsWithoutProtRel() {
  * 3) Adds a "delimiter" element to the array, either '://', ':' or '//' (see (2)).
  *
  * @param string $url A URL to parse
- * @return string[] Bits of the URL in an associative array, per PHP docs
+ * @return string[]|bool Bits of the URL in an associative array, per PHP docs, false on failure
  */
 function wfParseUrl( $url ) {
        global $wgUrlProtocols; // Allow all protocols defined in DefaultSettings/LocalSettings.php
index 70b6738..bc3305a 100644 (file)
@@ -256,7 +256,7 @@ class MovePage {
                $pageid = $this->oldTitle->getArticleID( Title::GAID_FOR_UPDATE );
                $protected = $this->oldTitle->isProtected();
 
-               // Do the actual move
+               // Do the actual move; if this fails, it will throw an MWException(!)
                $nullRevision = $this->moveToInternal( $user, $this->newTitle, $reason, $createRedirect );
 
                // Refresh the sortkey for this row.  Be careful to avoid resetting
@@ -412,7 +412,7 @@ class MovePage {
         *
         * @fixme This was basically directly moved from Title, it should be split into smaller functions
         * @param User $user the User doing the move
-        * @param Title $nt The page to move to, which should be a redirect or nonexistent
+        * @param Title $nt The page to move to, which should be a redirect or non-existent
         * @param string $reason The reason for the move
         * @param bool $createRedirect Whether to leave a redirect at the old title. Does not check
         *   if the user has the suppressredirect right
@@ -430,6 +430,29 @@ class MovePage {
                        $logType = 'move';
                }
 
+               if ( $moveOverRedirect ) {
+                       $overwriteMessage = wfMessage(
+                                       'delete_and_move_reason',
+                                       $this->oldTitle->getPrefixedText()
+                               )->text();
+                       $newpage = WikiPage::factory( $nt );
+                       $errs = [];
+                       $status = $newpage->doDeleteArticleReal(
+                               $overwriteMessage,
+                               /* $suppress */ false,
+                               $nt->getArticleId(),
+                               /* $commit */ false,
+                               $errs,
+                               $user
+                       );
+
+                       if ( !$status->isGood() ) {
+                               throw new MWException( 'Failed to delete page-move revision: ' . $status );
+                       }
+
+                       $nt->resetArticleID( false );
+               }
+
                if ( $createRedirect ) {
                        if ( $this->oldTitle->getNamespace() == NS_CATEGORY
                                && !wfMessage( 'category-move-redirect-override' )->inContentLanguage()->isDisabled()
@@ -484,19 +507,6 @@ class MovePage {
 
                $newpage = WikiPage::factory( $nt );
 
-               if ( $moveOverRedirect ) {
-                       $newid = $nt->getArticleID();
-                       $newcontent = $newpage->getContent();
-
-                       # Delete the old redirect. We don't save it to history since
-                       # by definition if we've got here it's rather uninteresting.
-                       # We have to remove it so that the next step doesn't trigger
-                       # a conflict on the unique namespace+title index...
-                       $dbw->delete( 'page', [ 'page_id' => $newid ], __METHOD__ );
-
-                       $newpage->doDeleteUpdates( $newid, $newcontent );
-               }
-
                # Save a null revision in the page's history notifying of the move
                $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true, $user );
                if ( !is_object( $nullRevision ) ) {
@@ -542,9 +552,7 @@ class MovePage {
                        );
                }
 
-               if ( !$moveOverRedirect ) {
-                       WikiPage::onArticleCreate( $nt );
-               }
+               WikiPage::onArticleCreate( $nt );
 
                # Recreate the redirect, this time in the other direction.
                if ( $redirectContent ) {
index b45eacb..4a1a520 100644 (file)
@@ -2458,6 +2458,7 @@ abstract class ApiBase extends ContextSource {
 
                // Build map of extension directories to extension info
                if ( self::$extensionInfo === null ) {
+                       $extDir = $this->getConfig()->get( 'ExtensionDirectory' );
                        self::$extensionInfo = [
                                realpath( __DIR__ ) ?: __DIR__ => [
                                        'path' => $IP,
@@ -2465,6 +2466,7 @@ abstract class ApiBase extends ContextSource {
                                        'license-name' => 'GPL-2.0+',
                                ],
                                realpath( "$IP/extensions" ) ?: "$IP/extensions" => null,
+                               realpath( $extDir ) ?: $extDir => null,
                        ];
                        $keep = [
                                'path' => null,
index 851252c..28937f7 100644 (file)
@@ -155,10 +155,14 @@ class ApiLogin extends ApiBase {
                                        $authRes = 'Failed';
                                        $message = $res->message;
                                        \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
-                                               ->info( __METHOD__ . ': Authentication failed: ' . $message->plain() );
+                                               ->info( __METHOD__ . ': Authentication failed: '
+                                               . $message->inLanguage( 'en' )->plain() );
                                        break;
 
                                default:
+                                       \MediaWiki\Logger\LoggerFactory::getInstance( 'authentication' )
+                                               ->info( __METHOD__ . ': Authentication failed due to unsupported response type: '
+                                               . $res->status, $this->getAuthenticationResponseLogData( $res ) );
                                        $authRes = 'Aborted';
                                        break;
                        }
@@ -273,4 +277,32 @@ class ApiLogin extends ApiBase {
        public function getHelpUrls() {
                return 'https://www.mediawiki.org/wiki/API:Login';
        }
+
+       /**
+        * Turns an AuthenticationResponse into a hash suitable for passing to Logger
+        * @param AuthenticationResponse $response
+        * @return array
+        */
+       protected function getAuthenticationResponseLogData( AuthenticationResponse $response ) {
+               $ret = [
+                       'status' => $response->status,
+               ];
+               if ( $response->message ) {
+                       $ret['message'] = $response->message->inLanguage( 'en' )->plain();
+               };
+               $reqs = [
+                       'neededRequests' => $response->neededRequests,
+                       'createRequest' => $response->createRequest,
+                       'linkRequest' => $response->linkRequest,
+               ];
+               foreach ( $reqs as $k => $v ) {
+                       if ( $v ) {
+                               $v = is_array( $v ) ? $v : [ $v ];
+                               $reqClasses = array_unique( array_map( 'get_class', $v ) );
+                               sort( $reqClasses );
+                               $ret[$k] = implode( ', ', $reqClasses );
+                       }
+               }
+               return $ret;
+       }
 }
index 0478027..565e829 100644 (file)
@@ -25,6 +25,8 @@
  * @defgroup API API
  */
 
+use MediaWiki\Logger\LoggerFactory;
+
 /**
  * This is the main API class, used for both external and internal processing.
  * When executed, it will create the requested formatter object,
@@ -206,7 +208,7 @@ class ApiMain extends ApiBase {
                                        $config->get( 'CrossSiteAJAXdomainExceptions' )
                                )
                        ) ) {
-                               MediaWiki\Logger\LoggerFactory::getInstance( 'cors' )->warning(
+                               LoggerFactory::getInstance( 'cors' )->warning(
                                        'Non-whitelisted CORS request with session cookies', [
                                                'origin' => $originHeader,
                                                'cookies' => $sessionCookies,
@@ -1453,6 +1455,7 @@ class ApiMain extends ApiBase {
        protected function setRequestExpectations( ApiBase $module ) {
                $limits = $this->getConfig()->get( 'TrxProfilerLimits' );
                $trxProfiler = Profiler::instance()->getTransactionProfiler();
+               $trxProfiler->setLogger( LoggerFactory::getInstance( 'DBPerformance' ) );
                if ( $this->getRequest()->hasSafeMethod() ) {
                        $trxProfiler->setExpectations( $limits['GET'], __METHOD__ );
                } elseif ( $this->getRequest()->wasPosted() && !$module->isWriteMode() ) {
index 6a70e5a..fc2fd59 100644 (file)
@@ -240,7 +240,7 @@ class ApiUpload extends ApiBase {
                                        'offset' => $this->mUpload->getOffset(),
                                ];
 
-                               $this->dieUsage( $status->getWikiText( false, false, 'en' ), 'stashfailed', 0, $extradata );
+                               $this->dieStatusWithCode( $status, 'stashfailed', $extradata );
                        }
                }
 
@@ -271,7 +271,7 @@ class ApiUpload extends ApiBase {
                                                $filekey,
                                                [ 'result' => 'Failure', 'stage' => 'assembling', 'status' => $status ]
                                        );
-                                       $this->dieUsage( $status->getWikiText( false, false, 'en' ), 'stashfailed' );
+                                       $this->dieStatusWithCode( $status, 'stashfailed' );
                                }
 
                                // The fully concatenated file has a new filekey. So remove
@@ -382,6 +382,28 @@ class ApiUpload extends ApiBase {
                $this->dieUsage( $parsed['info'], $parsed['code'], 0, $data );
        }
 
+       /**
+        * Like dieStatus(), but always uses $overrideCode for the error code, unless the code comes from
+        * IApiMessage.
+        *
+        * @param Status $status
+        * @param string $overrideCode Error code to use if there isn't one from IApiMessage
+        * @param array|null $moreExtraData
+        * @throws UsageException
+        */
+       public function dieStatusWithCode( $status, $overrideCode, $moreExtraData = null ) {
+               $extraData = null;
+               list( $code, $msg ) = $this->getErrorFromStatus( $status, $extraData );
+               $errors = $status->getErrorsByType( 'error' ) ?: $status->getErrorsByType( 'warning' );
+               if ( !( $errors[0]['message'] instanceof IApiMessage ) ) {
+                       $code = $overrideCode;
+               }
+               if ( $moreExtraData ) {
+                       $extraData += $moreExtraData;
+               }
+               $this->dieUsage( $msg, $code, 0, $extraData );
+       }
+
        /**
         * Select an upload module and set it to mUpload. Dies on failure. If the
         * request was a status request and not a true upload, returns false;
@@ -404,7 +426,7 @@ class ApiUpload extends ApiBase {
                        if ( !$progress ) {
                                $this->dieUsage( 'No result in status data', 'missingresult' );
                        } elseif ( !$progress['status']->isGood() ) {
-                               $this->dieUsage( $progress['status']->getWikiText( false, false, 'en' ), 'stashfailed' );
+                               $this->dieStatusWithCode( $progress['status'], 'stashfailed' );
                        }
                        if ( isset( $progress['status']->value['verification'] ) ) {
                                $this->checkVerification( $progress['status']->value['verification'] );
@@ -422,7 +444,7 @@ class ApiUpload extends ApiBase {
 
                if ( $this->mParams['chunk'] ) {
                        // Chunk upload
-                       $this->mUpload = new UploadFromChunks();
+                       $this->mUpload = new UploadFromChunks( $this->getUser() );
                        if ( isset( $this->mParams['filekey'] ) ) {
                                if ( $this->mParams['offset'] === 0 ) {
                                        $this->dieUsage( 'Cannot supply a filekey when offset is 0', 'badparams' );
index 50e370e..b8c536e 100644 (file)
@@ -2026,37 +2026,26 @@ class AuthManager implements LoggerAwareInterface {
 
                // Query them and merge results
                $reqs = [];
-               $allPrimaryRequired = null;
                foreach ( $providers as $provider ) {
                        $isPrimary = $provider instanceof PrimaryAuthenticationProvider;
-                       $thisRequired = [];
                        foreach ( $provider->getAuthenticationRequests( $providerAction, $options ) as $req ) {
                                $id = $req->getUniqueId();
 
-                               // If it's from a Primary, mark it as "primary-required" but
-                               // track it for later.
+                               // If a required request if from a Primary, mark it as "primary-required" instead
                                if ( $isPrimary ) {
                                        if ( $req->required ) {
-                                               $thisRequired[$id] = true;
                                                $req->required = AuthenticationRequest::PRIMARY_REQUIRED;
                                        }
                                }
 
-                               if ( !isset( $reqs[$id] ) || $req->required === AuthenticationRequest::REQUIRED ) {
+                               if (
+                                       !isset( $reqs[$id] )
+                                       || $req->required === AuthenticationRequest::REQUIRED
+                                       || $reqs[$id] === AuthenticationRequest::OPTIONAL
+                               ) {
                                        $reqs[$id] = $req;
                                }
                        }
-
-                       // Track which requests are required by all primaries
-                       if ( $isPrimary ) {
-                               $allPrimaryRequired = $allPrimaryRequired === null
-                                       ? $thisRequired
-                                       : array_intersect_key( $allPrimaryRequired, $thisRequired );
-                       }
-               }
-               // Any requests that were required by all primaries are required.
-               foreach ( (array)$allPrimaryRequired as $id => $dummy ) {
-                       $reqs[$id]->required = AuthenticationRequest::REQUIRED;
                }
 
                // AuthManager has its own req for some actions
index ff4d52e..f6f949e 100644 (file)
@@ -43,7 +43,8 @@ abstract class AuthenticationRequest {
        const REQUIRED = 1;
 
        /** Indicates that the request is required by a primary authentication
-        * provdier, but other primary authentication providers do not require it. */
+        * provdier. 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;
 
        /** @var string|null The AuthManager::ACTION_* constant this request was
index c44c8fc..35f3287 100644 (file)
@@ -57,6 +57,14 @@ interface PrimaryAuthenticationProvider extends AuthenticationProvider {
        /** Provider cannot create or link to accounts */
        const TYPE_NONE = 'none';
 
+       /**
+        * {@inheritdoc}
+        *
+        * Of the requests returned by this method, exactly one should have
+        * {@link AuthenticationRequest::$required} set to REQUIRED.
+        */
+       public function getAuthenticationRequests( $action, array $options );
+
        /**
         * Start an authentication flow
         *
index 9baf643..3ad7665 100644 (file)
@@ -154,7 +154,11 @@ class WikitextContentHandler extends TextContentHandler {
        protected function getFileText( Title $title ) {
                $file = wfLocalFile( $title );
                if ( $file && $file->exists() ) {
-                       return $file->getHandler()->getEntireText( $file );
+                       $handler = $file->getHandler();
+                       if ( !$handler ) {
+                               return null;
+                       }
+                       return $handler->getEntireText( $file );
                }
 
                return null;
index 53862b9..4a78363 100644 (file)
@@ -445,7 +445,7 @@ class DBConnRef implements IDatabase {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
-       public function begin( $fname = __METHOD__ ) {
+       public function begin( $fname = __METHOD__, $mode = IDatabase::TRANSACTION_EXPLICIT ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
index 78975ff..d492def 100644 (file)
@@ -815,7 +815,7 @@ abstract class DatabaseBase implements IDatabase {
                if ( !$this->mTrxLevel && $this->getFlag( DBO_TRX )
                        && $this->isTransactableQuery( $sql )
                ) {
-                       $this->begin( __METHOD__ . " ($fname)" );
+                       $this->begin( __METHOD__ . " ($fname)", self::TRANSACTION_INTERNAL );
                        $this->mTrxAutomatic = true;
                }
 
@@ -2203,7 +2203,7 @@ abstract class DatabaseBase implements IDatabase {
 
                $useTrx = !$this->mTrxLevel;
                if ( $useTrx ) {
-                       $this->begin( $fname );
+                       $this->begin( $fname, self::TRANSACTION_INTERNAL );
                }
                try {
                        # Update any existing conflicting row(s)
@@ -2221,7 +2221,7 @@ abstract class DatabaseBase implements IDatabase {
                        throw $e;
                }
                if ( $useTrx ) {
-                       $this->commit( $fname );
+                       $this->commit( $fname, self::TRANSACTION_INTERNAL );
                }
 
                return $ok;
@@ -2520,7 +2520,7 @@ abstract class DatabaseBase implements IDatabase {
                        $this->mTrxPreCommitCallbacks[] = [ $callback, wfGetCaller() ];
                } else {
                        // If no transaction is active, then make one for this callback
-                       $this->begin( __METHOD__ );
+                       $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
                        try {
                                call_user_func( $callback );
                                $this->commit( __METHOD__ );
@@ -2628,7 +2628,7 @@ abstract class DatabaseBase implements IDatabase {
 
        final public function startAtomic( $fname = __METHOD__ ) {
                if ( !$this->mTrxLevel ) {
-                       $this->begin( $fname );
+                       $this->begin( $fname, self::TRANSACTION_INTERNAL );
                        $this->mTrxAutomatic = true;
                        // If DBO_TRX is set, a series of startAtomic/endAtomic pairs will result
                        // in all changes being in one transaction to keep requests transactional.
@@ -2666,43 +2666,26 @@ abstract class DatabaseBase implements IDatabase {
                $this->endAtomic( $fname );
        }
 
-       final public function begin( $fname = __METHOD__ ) {
-               if ( $this->mTrxLevel ) { // implicit commit
+       final public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT ) {
+               // Protect against mismatched atomic section, transaction nesting, and snapshot loss
+               if ( $this->mTrxLevel ) {
                        if ( $this->mTrxAtomicLevels ) {
-                               // If the current transaction was an automatic atomic one, then we definitely have
-                               // a problem. Same if there is any unclosed atomic level.
                                $levels = implode( ', ', $this->mTrxAtomicLevels );
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       "Got explicit BEGIN from $fname while atomic section(s) $levels are open."
-                               );
+                               $msg = "Got explicit BEGIN from $fname while atomic section(s) $levels are open.";
+                               throw new DBUnexpectedError( $this, $msg );
                        } elseif ( !$this->mTrxAutomatic ) {
-                               // We want to warn about inadvertently nested begin/commit pairs, but not about
-                               // auto-committing implicit transactions that were started by query() via DBO_TRX
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       "$fname: Transaction already in progress (from {$this->mTrxFname}), " .
-                                               " performing implicit commit!"
-                               );
-                       } elseif ( $this->mTrxDoneWrites ) {
-                               // The transaction was automatic and has done write operations
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       "$fname: Automatic transaction with writes in progress" .
-                                               " (from {$this->mTrxFname}), performing implicit commit!\n"
-                               );
-                       }
-
-                       $this->runOnTransactionPreCommitCallbacks();
-                       $writeTime = $this->pendingWriteQueryDuration();
-                       $this->doCommit( $fname );
-                       if ( $this->mTrxDoneWrites ) {
-                               $this->mDoneWrites = microtime( true );
-                               $this->getTransactionProfiler()->transactionWritingOut(
-                                       $this->mServer, $this->mDBname, $this->mTrxShortId, $writeTime );
+                               $msg = "$fname: Explicit transaction already active (from {$this->mTrxFname}).";
+                               throw new DBUnexpectedError( $this, $msg );
+                       } else {
+                               // @TODO: make this an exception at some point
+                               $msg = "$fname: Implicit transaction already active (from {$this->mTrxFname}).";
+                               wfLogDBError( $msg );
+                               return; // join the main transaction set
                        }
-
-                       $this->runOnTransactionIdleCallbacks( self::TRIGGER_COMMIT );
+               } elseif ( $this->getFlag( DBO_TRX ) && $mode !== self::TRANSACTION_INTERNAL ) {
+                       // @TODO: make this an exception at some point
+                       wfLogDBError( "$fname: Implicit transaction expected (DBO_TRX set)." );
+                       return; // let any writes be in the main transaction
                }
 
                // Avoid fatals if close() was called
@@ -2742,7 +2725,7 @@ abstract class DatabaseBase implements IDatabase {
                        $levels = implode( ', ', $this->mTrxAtomicLevels );
                        throw new DBUnexpectedError(
                                $this,
-                               "Got COMMIT while atomic sections $levels are still open"
+                               "Got COMMIT while atomic sections $levels are still open."
                        );
                }
 
@@ -2752,18 +2735,17 @@ abstract class DatabaseBase implements IDatabase {
                        } elseif ( !$this->mTrxAutomatic ) {
                                throw new DBUnexpectedError(
                                        $this,
-                                       "$fname: Flushing an explicit transaction, getting out of sync!"
+                                       "$fname: Flushing an explicit transaction, getting out of sync."
                                );
                        }
                } else {
                        if ( !$this->mTrxLevel ) {
-                               wfWarn( "$fname: No transaction to commit, something got out of sync!" );
+                               wfWarn( "$fname: No transaction to commit, something got out of sync." );
                                return; // nothing to do
                        } elseif ( $this->mTrxAutomatic ) {
-                               throw new DBUnexpectedError(
-                                       $this,
-                                       "$fname: Explicit commit of implicit transaction."
-                               );
+                               // @TODO: make this an exception at some point
+                               wfLogDBError( "$fname: Explicit commit of implicit transaction." );
+                               return; // wait for the main transaction set commit round
                        }
                }
 
@@ -2796,14 +2778,19 @@ abstract class DatabaseBase implements IDatabase {
        }
 
        final public function rollback( $fname = __METHOD__, $flush = '' ) {
-               if ( $flush !== self::FLUSHING_INTERNAL && $flush !== self::FLUSHING_ALL_PEERS ) {
+               if ( $flush === self::FLUSHING_INTERNAL || $flush === self::FLUSHING_ALL_PEERS ) {
                        if ( !$this->mTrxLevel ) {
-                               wfWarn( "$fname: No transaction to rollback, something got out of sync!" );
                                return; // nothing to do
                        }
                } else {
                        if ( !$this->mTrxLevel ) {
+                               wfWarn( "$fname: No transaction to rollback, something got out of sync." );
                                return; // nothing to do
+                       } elseif ( $this->getFlag( DBO_TRX ) ) {
+                               throw new DBUnexpectedError(
+                                       $this,
+                                       "$fname: Expected mass rollback of all peer databases (DBO_TRX set)."
+                               );
                        }
                }
 
index 867aeb8..1ecdd26 100644 (file)
@@ -149,7 +149,7 @@ class SavepointPostgres {
                $this->didbegin = false;
                /* If we are not in a transaction, we need to be for savepoint trickery */
                if ( !$dbw->trxLevel() ) {
-                       $dbw->begin( "FOR SAVEPOINT" );
+                       $dbw->begin( "FOR SAVEPOINT", DatabasePostgres::TRANSACTION_INTERNAL );
                        $this->didbegin = true;
                }
        }
@@ -1207,7 +1207,7 @@ __INDEXATTR__;
         * @param string $desiredSchema
         */
        function determineCoreSchema( $desiredSchema ) {
-               $this->begin( __METHOD__ );
+               $this->begin( __METHOD__, self::TRANSACTION_INTERNAL );
                if ( $this->schemaExists( $desiredSchema ) ) {
                        if ( in_array( $desiredSchema, $this->getSchemas() ) ) {
                                $this->mCoreSchema = $desiredSchema;
index af024b8..fce5aa8 100644 (file)
@@ -40,6 +40,11 @@ interface IDatabase {
        /** @var int Callback triggered by rollback */
        const TRIGGER_ROLLBACK = 3;
 
+       /** @var string Transaction is requested by regular caller outside of the DB layer */
+       const TRANSACTION_EXPLICIT = '';
+       /** @var string Transaction is requested interally via DBO_TRX/startAtomic() */
+       const TRANSACTION_INTERNAL = 'implicit';
+
        /** @var string Transaction operation comes from service managing all DBs */
        const FLUSHING_ALL_PEERS = 'flush';
        /** @var string Transaction operation comes from the database class internally */
@@ -1362,9 +1367,10 @@ interface IDatabase {
         * automatically because of the DBO_TRX flag.
         *
         * @param string $fname
+        * @param string $mode A situationally valid IDatabase::TRANSACTION_* constant [optional]
         * @throws DBError
         */
-       public function begin( $fname = __METHOD__ );
+       public function begin( $fname = __METHOD__, $mode = self::TRANSACTION_EXPLICIT );
 
        /**
         * Commits a transaction previously started using begin().
index c48880f..5556dd8 100644 (file)
@@ -471,6 +471,27 @@ class SqlBagOStuff extends BagOStuff {
                return $ok;
        }
 
+       public function changeTTL( $key, $expiry = 0 ) {
+               list( $serverIndex, $tableName ) = $this->getTableByKey( $key );
+               try {
+                       $db = $this->getDB( $serverIndex );
+                       $db->update(
+                               $tableName,
+                               [ 'exptime' => $db->timestamp( $this->convertExpiry( $expiry ) ) ],
+                               [ 'keyname' => $key, 'exptime > ' . $db->addQuotes( $db->timestamp( time() ) ) ],
+                               __METHOD__
+                       );
+                       if ( $db->affectedRows() == 0 ) {
+                               return false;
+                       }
+               } catch ( DBError $e ) {
+                       $this->handleWriteError( $e, $serverIndex );
+                       return false;
+               }
+
+               return true;
+       }
+
        /**
         * @param IDatabase $db
         * @param string $exptime
index 1ec205c..ae16f70 100644 (file)
@@ -936,13 +936,8 @@ abstract class UploadBase {
         */
        public function tryStashFile( User $user, $isPartial = false ) {
                if ( !$isPartial ) {
-                       $props = $this->mFileProps;
-                       $error = null;
-                       Hooks::run( 'UploadStashFile', [ $this, $user, $props, &$error ] );
+                       $error = $this->runUploadStashFileHook( $user );
                        if ( $error ) {
-                               if ( !is_array( $error ) ) {
-                                       $error = [ $error ];
-                               }
                                return call_user_func_array( 'Status::newFatal', $error );
                        }
                }
@@ -954,6 +949,22 @@ abstract class UploadBase {
                }
        }
 
+       /**
+        * @param User $user
+        * @return array|null Error message and parameters, null if there's no error
+        */
+       protected function runUploadStashFileHook( User $user ) {
+               $props = $this->mFileProps;
+               $error = null;
+               Hooks::run( 'UploadStashFile', [ $this, $user, $props, &$error ] );
+               if ( $error ) {
+                       if ( !is_array( $error ) ) {
+                               $error = [ $error ];
+                       }
+               }
+               return $error;
+       }
+
        /**
         * If the user does not supply all necessary information in the first upload
         * form submission (either by accident or by design) then we may want to
index 247f608..6368db8 100644 (file)
@@ -38,12 +38,11 @@ class UploadFromChunks extends UploadFromFile {
        /**
         * Setup local pointers to stash, repo and user (similar to UploadFromStash)
         *
-        * @param User|null $user Default: null
+        * @param User $user
         * @param UploadStash|bool $stash Default: false
         * @param FileRepo|bool $repo Default: false
         */
-       public function __construct( $user = null, $stash = false, $repo = false ) {
-               // user object. sometimes this won't exist, as when running from cron.
+       public function __construct( User $user, $stash = false, $repo = false ) {
                $this->user = $user;
 
                if ( $repo ) {
@@ -162,7 +161,20 @@ class UploadFromChunks extends UploadFromFile {
                // Update the mTempPath and mLocalFile
                // (for FileUpload or normal Stash to take over)
                $tStart = microtime( true );
-               $this->mLocalFile = parent::doStashFile( $this->user );
+               // This is a re-implementation of UploadBase::tryStashFile(), we can't call it because we
+               // override doStashFile() with completely different functionality in this class...
+               $error = $this->runUploadStashFileHook( $this->user );
+               if ( $error ) {
+                       call_user_func_array( [ $status, 'fatal' ], $error );
+                       return $status;
+               }
+               try {
+                       $this->mLocalFile = parent::doStashFile( $this->user );
+               } catch ( UploadStashException $e ) {
+                       $status->fatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
+                       return $status;
+               }
+
                $tAmount = microtime( true ) - $tStart;
                $this->mLocalFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo())
                wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds." );
index 50bcbc4..1fbdb7d 100644 (file)
@@ -143,24 +143,6 @@ class UploadFromStash extends UploadBase {
                return $this->mFileProps['sha1'];
        }
 
-       /*
-        * protected function verifyFile() inherited
-        */
-
-       /**
-        * Stash the file.
-        *
-        * @param User $user
-        * @return UploadStashFile
-        */
-       protected function doStashFile( User $user = null ) {
-               // replace mLocalFile with an instance of UploadStashFile, which adds some methods
-               // that are useful for stashed files.
-               $this->mLocalFile = parent::doStashFile( $user );
-
-               return $this->mLocalFile;
-       }
-
        /**
         * Remove a temporarily kept file stashed by saveTempUploadedFile().
         * @return bool Success
index 026c3f8..3432c23 100644 (file)
        "alllogstext": "{{SITENAME}} üçün bütün mövcud qeydlərin birgə göstərişi.\nQeyd növü, istifadəçi adı və ya təsir edilmiş səhifəni seçməklə daha spesifik ola bilərsiniz.",
        "logempty": "Jurnalda uyğun qeyd tapılmadı.",
        "log-title-wildcard": "Bu mətnlə başlayan başlıqları axtar",
+       "checkbox-select": "Seçin: $1",
+       "checkbox-all": "Hamısı",
+       "checkbox-none": "Heç biri",
+       "checkbox-invert": "Çevir",
        "allpages": "Bütün səhifələr",
        "nextpage": "Sonrakı səhifə ($1)",
        "prevpage": "Əvvəlki səhifə ($1)",
index 052c3ce..1fb2401 100644 (file)
        "editlink": "دەستکاری",
        "viewsourcelink": "بینینی سەرچاوە",
        "editsectionhint": "دەستکاریکردنی بەش: $1",
-       "toc": "Ù\86اÙ\88Û\95Ú\95Û\86Ú©",
+       "toc": "Ù¾Û\8eرست",
        "showtoc": "نیشانیبدە",
        "hidetoc": "بیشارەوە",
        "collapsible-collapse": "کۆی بکەوە",
index 6e74495..7e9eecf 100644 (file)
        "grant-group-high-volume": "Velkoobjemové činnosti",
        "grant-group-customization": "Nastavení a přizpůsobení",
        "grant-group-administration": "Provádění správcovských činností",
-       "grant-group-private-information": "Zpřístupnit soukromá data o vás",
+       "grant-group-private-information": "Přístup k soukromým údajům o vás",
        "grant-group-other": "Různé činnosti",
        "grant-blockusers": "Blokovat a odblokovávat uživatele",
        "grant-createaccount": "Zakládat účty",
        "grant-highvolume": "Hromadné editace",
        "grant-oversight": "Skrývat uživatele a utajovat revize",
        "grant-patrol": "Patrolovat změny stránek",
+       "grant-privateinfo": "Přístup k soukromým údajům",
        "grant-protect": "Zamykat a odemykat stránky",
        "grant-rollback": "Vracet editace zpět",
        "grant-sendemail": "Posílat e-maily ostatním uživatelům",
        "uploadstash-errclear": "Soubory se nepodařilo vymazat.",
        "uploadstash-refresh": "Aktualizovat seznam souborů",
        "uploadstash-thumbnail": "zobrazit náhled",
+       "uploadstash-exception": "Načtený soubor se nepodařilo uložit do skrýše ($1): „$2“.",
        "invalid-chunk-offset": "Neplatný posun bloku",
        "img-auth-accessdenied": "Přístup odepřen",
        "img-auth-nopathinfo": "Chybí PATH_INFO.\nVáš server není nastaven tak, aby tuto informaci poskytoval.\nMožná funguje pomocí CGI a img_auth na něm nemůže fungovat.\nVizte https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
        "trackingcategories-name": "Název hlášení",
        "trackingcategories-desc": "Kritéria pro vložení do kategorie",
        "restricted-displaytitle-ignored": "Stránky s ignorovanými zobrazovanými názvy",
-       "restricted-displaytitle-ignored-desc": "Stránky obsahující příkaz <code><nowiki>{{DISPLAYTITLE}}</nowiki></code>, který se ignoruje, neboť není ekvivalentní skutečnému názvu stránky.",
+       "restricted-displaytitle-ignored-desc": "Stránka obsahuje příkaz <code><nowiki>{{DISPLAYTITLE}}</nowiki></code>, který se ignoruje, neboť není ekvivalentní skutečnému názvu stránky.",
        "noindex-category-desc": "Stránka není indexována roboty, protože obsahuje kouzelné slovo <code><nowiki>__NOINDEX__</nowiki></code> a je ve jmenném prostoru, ve kterém je tento příznak dovolen.",
        "index-category-desc": "Stránka obsahuje kouzelné slovo <code><nowiki>__INDEX__</nowiki></code> (a je ve jmenném prostoru, ve kterém je tento příznak dovolen), takže je indexována roboty, přestože by normálně nebyla.",
        "post-expand-template-inclusion-category-desc": "Stránka je po rozbalení všech šablon větší než <code>$wgMaxArticleSize</code>, takže některé šablony rozbaleny nebyly.",
index 0b39d6e..7ca8358 100644 (file)
        "anontalk": "Werênayış",
        "navigation": "Pusula",
        "and": "&#32;u",
-       "qbfind": "Bıvêne",
+       "qbfind": "Bıvin",
        "qbbrowse": "Çım ra viyarne",
        "qbedit": "Bıvurne",
        "qbpageoptions": "Ena pele",
        "view-foreign": "$1 de bıvêne",
        "edit": "Bıvurne",
        "edit-local": "Şınasnayışê lokali bıvurne",
-       "create": "Vıraze",
+       "create": "Bıvıraz",
        "create-local": "Şınasnayışê lokali cı ke",
        "editthispage": "Ena pele bıvurne",
        "create-this-page": "Na pele bınuse",
        "talkpagelinktext": "werênayış",
        "specialpage": "Pela xısusiye",
        "personaltools": "Hacetê şexsiy",
-       "articlepage": "Pela zerreki bıvêne",
+       "articlepage": "Pera zerreki bıvin",
        "talk": "Werênayış",
        "views": "Asayışi",
        "toolbox": "Haceti",
        "imagepage": "Pera dosya bıasne",
        "mediawikipage": "Pera mesaci bıasne",
        "templatepage": "Pera şabloni bıasne",
-       "viewhelppage": "Pela peşti bıvêne",
+       "viewhelppage": "Pera peşti bıvin",
        "categorypage": "Pela kategoriya bıasne",
        "viewtalkpage": "Werênayışi bıvêne",
        "otherlanguages": "Zıwananê binan de",
        "redirectedfrom": "($1 ra kırışı yê)",
        "redirectpagesub": "Pela berdışi",
        "redirectto": "Beno hetê:",
-       "lastmodifiedat": "Ena pele tewr peyên roca $1, saeta $2 de biye rocane.",
+       "lastmodifiedat": "Per roca $1, sehat $2 de biye neye.",
        "viewcount": "Ena pele {{PLURAL:$1|rae|$1 rey}} vêniya.",
        "protectedpage": "Pela pawıtiye",
        "jumpto": "Şo be:",
        "confirmable-yes": "Eya",
        "confirmable-no": "Nê",
        "thisisdeleted": "Bıvêne ya zi $1 peyser biya?",
-       "viewdeleted": "$1 bıvêne?",
+       "viewdeleted": "$1 bıvin?",
        "restorelink": "{{PLURAL:$1|jew vurnayış besteriya|$1 vurnayışi besteriyaye}}",
        "feedlinks": "Warikerdış:",
        "feed-invalid": "Qeydey cıresnayışê  beğşi nêvêreno.",
        "perfcached": "Datay cı ver hazır biye. No semedê ra nıkayin niyo! tewr zaf {{PLURAL:$1|netice|$1 netice}} debêno de",
        "perfcachedts": "Cêr de malumatê nımıteyi esti, demdê newe kerdışo peyın: $1. Tewr zaf {{PLURAL:$4|netice|$4 neticey cı}} debyayo de",
        "querypage-no-updates": "Rocanebiyayışê na pele nıka cadayiyê.\nDayiyi tiya nıka newe nêbenê.",
-       "viewsource": "Çımey bıvêne",
+       "viewsource": "Çemi bıvin",
        "viewsource-title": "Cı geyrayışê $1'i bıvin",
        "actionthrottled": "Kerden peysnaya",
        "actionthrottledtext": "Riyê tedbirê anti-spami ra,  wextê do kılmek de şıma nê fealiyeti nêşkenê zaf zêde bıkerê, şıma ki no hedi viyarna ra.\nÇend deqey ra tepeya reyna bıcerrebnên.",
        "nologinlink": "Yew hesab ake",
        "createaccount": "Hesab vıraze",
        "gotaccount": "Hesabê şıma esto? '''$1'''.",
-       "gotaccountlink": "Cı kewe",
+       "gotaccountlink": "Cıkewtış",
        "userlogin-resetlink": "Melumatê cıkewtışi xo vira kerdê?",
        "userlogin-resetpassword-link": "Parola xo kerda xo vira?",
        "userlogin-helplink2": "Heqa qeydbiyayışi de peşti bıgêrên",
        "botpasswords-label-appid": "Nameyê boti:",
        "botpasswords-label-create": "Vıraze",
        "botpasswords-label-update": "Rocane ke",
-       "botpasswords-label-cancel": "Bıtexelne",
+       "botpasswords-label-cancel": "İbtal ke",
        "botpasswords-label-delete": "Bestere",
        "botpasswords-label-resetpassword": "Parola raçarne",
        "botpasswords-label-grants-column": "Dayen",
        "resetpass_forbidden": "parolayi nêvuryayi",
        "resetpass-no-info": "şıma gani hesab akere u hona bıeşke bırese cı",
        "resetpass-submit-loggedin": "Parola bıvurne",
-       "resetpass-submit-cancel": "Bıtexelne",
+       "resetpass-submit-cancel": "İbtal ke",
        "resetpass-wrong-oldpass": "parolayo parola maqbul niyo.\nşıma ya parolaye xo vurnayo ya zi parolayo muwaqqat waşto.",
        "resetpass-recycled": "Parolaya şımaya newiye wa paroloya şımaya verêne ra ferqıne bo.",
        "resetpass-temp-emailed": "E postaya rışyayê yubkoda şıma ronıştış akerdo.  Ronıştışi xo temammkerdışi rê yu parolaya newi lazım a",
        "hr_tip": "Xeta verardiye (teserrufın bıgureyne/bıxebetne)",
        "summary": "Xulasa:",
        "subject": "Mewzu:",
-       "minoredit": "No yew vurnayışo werdiyo",
-       "watchthis": "Ena pele seyr ke",
-       "savearticle": "Pele qeyd ke",
+       "minoredit": "Vurriyayışo werdiyo",
+       "watchthis": "Seyr kı",
+       "savearticle": "Qeyd kı",
        "savechanges": "Vurnayışan qeyd ke",
        "publishpage": "Perer bıhesırne",
        "publishchanges": "Vurnayışa vıla ke",
        "accmailtext": "[[User talk:$1|$1]] parolayo ke raşt ameyo şırawiyo na adres $2.\n\nQey na hesabê newe parola, cıkewtış dıma şıma eşkeni na qısım de ''[[Special:ChangePassword|parola bıvurn]]'' bıvurni.",
        "newarticle": "(Newe)",
        "newarticletext": "To yew gıre tıkna be ra yew pela ke hewna çıniya.\nSeba afernayışê pele ra, qutiya metnê cêrêni bıgurene (seba melumati qaytê [$1 pela peşti] ke).\nEke be ğeletine ameya tiya, wa gocega <strong>peyser</strong>i programê xo de bıtıkne.",
-       "anontalkpagetext": "----''No pel, pel o karbero hesab a nêkerdeyan o, ya zi karbero hesab akerdeyan o labele pê hesabê xo nêkewto de. No sebeb ra ma IP adres şuxulneni û ney IP adresan herkes eşkeno bıvino. Eke şıma qayil niye ina bo xo ri [[Special:CreateAccount|yew hesab bıvıraze]] veyaxut [[Special:UserLogin|hesab akere]].''",
+       "anontalkpagetext": "----''Na per, perêk kı karbero hesab a nêkerdeyan o, ya zi karbero hesab akerdeyan o labele pê hesabê xo nêkewto de. No sebeb ra ma IP adres xebetneno û ney IP adresan herkes nêşeno bıvino. Eke şıma qayil niye ina bo xorê [[Special:CreateAccount|yew hesab bıvıraze]] veya xut [[Special:UserLogin|hesab akere]].''",
        "noarticletext": "Ena pele de hewna theba çıniyo.\nTı şenê zerreyê pelanê binan de [[Special:Search/{{PAGENAME}}|qandê  sernameyê ena pele cı geyre]],\n<span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} qeydan miyan de cı geyre],\nya zi [{{fullurl:{{FULLPAGENAME}}|action=edit}} ena pele vıraze]</span>.{{MediaWiki mesaca pera newi}}",
        "noarticletext-nopermission": "Ena pele de hewna theba çıniyo.\nTı şenay zerreyê pelanê binan de [[Special:Search/{{PAGENAME}}|seba sernameyê na pele cı geyre]], ya zi <span class=\"plainlinks\">[{{fullurl:{{#Special:Log}}|page={{FULLPAGENAMEE}}}} qeydan miyan de cı geyre]</span>, ema destur çıniyo ke na pele vırazê.",
        "missing-revision": "Rewizyonê name dê pela da #$1 \"{{FULLPAGENAME}}\" dı çıniyo.\n\nNo normal de tarix dê pelanê besterneyan dı ena xırabin asena.\nDetayê besternayışi [{{fullurl:{{#Special:Log}}/delete|page={{FULLPAGENAMEE}}}} tiya dı] aseno.",
        "next-page": "Pela peyên",
        "prevn-title": "$1o verên  {{PLURAL:$1|netice|neticeyan}}",
        "nextn-title": "$1o ke yeno {{PLURAL:$1|netice|neticey}}",
-       "shown-title": "bimocne $1î  {{PLURAL:$1|netice|neticeyan}} ser her pel",
+       "shown-title": "Herg per sero $1 {{PLURAL:$1|netici|netica}} bıasne",
        "viewprevnext": "($1 {{int:pipe-separator}} $2) ($3) bıvênên",
        "searchmenu-exists": "''Ena 'Wikipediya de ser \"[[:$1]]\" yew pel esto'''",
        "searchmenu-new": "<strong>Na wiki de pela \"[[:$1]]\" vıraze!</strong> {{PLURAL:$2|0=|Sewbina pela ke şıma geyrayê cı aye bıvênê.|Yew zi neticanê cıgeyrayışê xo bıvênê.}}",
        "rightslogtext": "Ena listeyê loganê ke heqqa karbaranî mucneno.",
        "action-read": "ena pela wanayış",
        "action-edit": "ena pela bıvurnê",
-       "action-createpage": "na pele vıraze",
+       "action-createpage": "na perer bıvıraz",
        "action-createtalk": "pelanê werênayışi bıvıraze",
        "action-createaccount": "hesabê nê karberi bıvıraze",
        "action-autocreateaccount": "nê hesabê karberiyê teberi otomatik vıraze",
        "enhancedrc-history": "tarix",
        "recentchanges": "Vurriyayışê peyêni",
        "recentchanges-legend": "Tercihê vurnayışanê peyênan",
-       "recentchanges-summary": "Ena pele de wiki sero vurriyayışanê peyênan teqib ke.",
+       "recentchanges-summary": "Wiki sero vurriyayışê peyêni asenê.",
        "recentchanges-noresult": "Goreyê kriteranê kıfşkerdeyan ra qet yew vurnayış nêvêniya.",
        "recentchanges-feed-description": "Ena feed dı vurnayişanê tewr peniyan teqip bık.",
        "recentchanges-label-newpage": "Enê vurnayışi ra yew pela newiye vıraziye",
-       "recentchanges-label-minor": "No yew vurnayışo werdiyo",
+       "recentchanges-label-minor": "Vurriyayışo werdiyo",
        "recentchanges-label-bot": "Eno vurnayış terefê yew boti ra vıraziyo",
        "recentchanges-label-unpatrolled": "Eno vurnayış hewna dewriya nêbiyo",
        "recentchanges-label-plusminus": "Ebadê pele de bazê bayti de vayeyê cı",
        "rcshowhidecategorization": "kategorizasyonê pele $1",
        "rcshowhidecategorization-show": "Bıasne",
        "rcshowhidecategorization-hide": "Bınımne",
-       "rclinks": "Peyniya $2 rocan de $1 vurriyayışan bımocne <br />$3",
+       "rclinks": "Peyniya $2 rocan de $1 vurriyayışê <br />$3 asenê",
        "diff": "ferq",
        "hist": "verên",
        "hide": "Bınımne",
        "removedwatchtext": "Ena pela \"[[:$1]]\" biya wedariya [[Special:Watchlist|listeyê seyr-kerdışi şıma]].",
        "removedwatchtext-short": "Pera $1`i listeya seyran de şıma ra wedari yê",
        "watch": "Seyr ke",
-       "watchthispage": "Ena pele seyr ke",
+       "watchthispage": "Seyr kı",
        "unwatch": "Teqib meke",
        "unwatchthispage": "temaşa kerdışê peli vındarn.",
        "notanarticle": "mebhesê peli niyo",
        "pagesize": "(bitî)",
        "restriction-edit": "Bıvurne",
        "restriction-move": "Bıkırış",
-       "restriction-create": "Vıraze",
+       "restriction-create": "Bıvıraz",
        "restriction-upload": "Bar ke",
        "restriction-level-sysop": "tam pawiyayo",
        "restriction-level-autoconfirmed": "nêm pawiyayo",
        "undeleterevision-missing": "revizyonê nemeqbul u vindbiyayeyi.\nRevizyoni ya hewn a biyê ya arşiw ra veciyayê ya zi cıresayişê şımayi şaş o.",
        "undelete-nodiff": "revizyonê verıni nidiya",
        "undeletebtn": "Timar bike",
-       "undeletelink": "bıvêne/peyser biya",
+       "undeletelink": "bıewni/peyser biya",
        "undeleteviewlink": "bıvin",
        "undeleteinvert": "Weçinayışi dimlaşt ke",
        "undeletecomment": "Sebeb:",
        "unblocked-id": "Blokê $1î wedariyayo",
        "blocklist": "Karberê kılitbiyayey",
        "ipblocklist": "Karberê kılitbiyayey",
-       "ipblocklist-legend": "Yew karberê kılitbiyayey bıvêne",
+       "ipblocklist-legend": "Karberê kılit biyayey bıvin",
        "blocklist-userblocks": "Kılitkerdışê hesaban bınımne",
        "blocklist-tempblocks": "Kılitkerdışan mıweqet bınımne",
        "blocklist-addressblocks": "Tenya kılitkerdışanê IPy bınımne",
        "articleexists": "Ena nameyê pela database ma dı esta ya zi tı raşt nınuşt. .\nYewna name bınus.",
        "cantmove-titleprotected": "şıma nêşkeni yew peli bıhewelnê tiya çunke pawıyeno",
        "movetalk": "Pela werênayışiê elaqedare bere",
-       "move-subpages": "Pelanê bınênan bıkırışe (heta pela $1)",
-       "move-talk-subpages": "Pelanê werênayışiyê bınênan bıkırışe (heta pela $1)",
+       "move-subpages": "Peranê bınênan bıkırış (hetana $1)",
+       "move-talk-subpages": "Bın peranê peranê vatena bıkırış (hetana $1)",
        "movepage-page-exists": "maddeya $1i ca ra esta u newe ra otomatikmen nênusyena.",
        "movepage-page-moved": "pelê $1i kırışiya pelê $2i.",
        "movepage-page-unmoved": "pelê $1i nêkırışiyeno sernameyê $2i.",
        "fileduplicatesearch-result-1": "Dosyayê ''$1î'' de hem-kopya çini yo.",
        "fileduplicatesearch-result-n": "Dosyayê ''$1î'' de {{PLURAL:$2|1 hem-kopya|$2 hem-kopyayî'}} esto.",
        "fileduplicatesearch-noresults": "Ebe namey \"$1\" ra dosya nêdiyayê.",
-       "specialpages": "Pelê xısusiyi",
+       "specialpages": "Page bağsey",
        "specialpages-note-top": "Kıtabek",
        "specialpages-note": "* Pelê xasê normali.\n* <span class=\"mw-specialpagerestricted\">Pelê xasê nımıtey.</span>",
        "specialpages-group-maintenance": "Raporê pawıtışi",
        "feedback-bugcheck": "Harika! Sadece [xırabina ke $1 ] çınyayışê cı kontrol keno.",
        "feedback-bugnew": "Mı qontrol ke. Xetaya newi xeber ke",
        "feedback-bugornote": "Jew mersela teferruato teknik esta şıma reca malumatê şıma hazıro se [ $1  jew xırab rapor] bıvinê.Zewbi zi, formê cerê xo rê şenê karfiyê. Vatışê xo pela da \"[ $3  $2 ]\", namey karber dê xoya piya u wasteriya karfiye.",
-       "feedback-cancel": "Bıtexelne",
+       "feedback-cancel": "İbtal kı",
        "feedback-close": "Biya star",
        "feedback-error1": "Xeta: API ra neticey ne vıcyay",
        "feedback-error2": "Xeta: Timar kerdış nebı",
index d2548aa..f76e5b8 100644 (file)
        "undeletehistorynoadmin": "Esta páxina foi borrada.\nO motivo do borrado consta no resumo que aparece a continuación, xunto cos detalles dos usuarios que editaron esta páxina antes da súa eliminación.\nO texto destas revisións eliminadas só está á disposición dos administradores.",
        "undelete-revision": "Revisión eliminada de \"$1\" (o $4 ás $5) feita por $3:",
        "undeleterevision-missing": "Revisión non válida ou inexistente. Pode que a ligazón conteña un erro ou que a revisión se restaurase ou eliminase do arquivo.",
+       "undeleterevision-duplicate-revid": "{{PLURAL:$1|Unha revisión non pode ser restaurada|$1 revisións non poden ser restauradas}} porque {{PLURAL:$1|o seu|os seus}}  <code>rev_id</code> xa {{PLURAL:$1|está|están}} en uso.",
        "undelete-nodiff": "Non se atopou ningunha revisión anterior.",
        "undeletebtn": "Restaurar",
        "undeletelink": "ver/restaurar",
        "undeletedrevisions": "{{PLURAL:$1|Restaurouse $1 revisión|Restauráronse $1 revisións}}",
        "undeletedrevisions-files": "Restauráronse $1 {{PLURAL:$1|revisión|revisións}} e $2 {{PLURAL:$2|ficheiro|ficheiros}}",
        "undeletedfiles": "{{PLURAL:$1|Restaurouse $1 ficheiro|Restauráronse $1 ficheiros}}",
-       "cannotundelete": "Houbo un erro durante a restauración:\n$1",
+       "cannotundelete": "Algunhas ou todas as restauracións fallaronː\n$1",
        "undeletedpage": "'''A páxina \"$1\" foi restaurada'''\n\nComprobe o [[Special:Log/delete|rexistro de borrados]] para ver as entradas recentes no rexistro de páxinas eliminadas e restauradas.",
        "undelete-header": "Consulte [[Special:Log/delete|no rexistro de borrados]] as páxinas borradas recentemente.",
        "undelete-search-title": "Procurar páxinas borradas",
        "sp-contributions-newbies-sub": "Contribucións dos usuarios novos",
        "sp-contributions-newbies-title": "Contribucións dos usuarios novos",
        "sp-contributions-blocklog": "rexistro de bloqueos",
-       "sp-contributions-suppresslog": "contribucións borradas do usuario",
-       "sp-contributions-deleted": "contribucións borradas do usuario",
+       "sp-contributions-suppresslog": "contribucións {{GENDER:$1|do usuario|da usuaria}} suprimidas",
+       "sp-contributions-deleted": "contribucións {{GENDER:$1|do usuario|da usuaria}} borradas",
        "sp-contributions-uploads": "cargas",
        "sp-contributions-logs": "rexistros",
        "sp-contributions-talk": "conversa",
index a99ca8f..7f73d57 100644 (file)
        "datedefault": "ברירת המחדל",
        "prefs-labs": "אפשרויות מעבדה",
        "prefs-user-pages": "דפי משתמש",
-       "prefs-personal": "פרטי המשתמש",
+       "prefs-personal": "פרטי ה{{GENDER:|משתמש|משתמשת}}",
        "prefs-rc": "שינויים אחרונים",
        "prefs-watchlist": "רשימת המעקב",
        "prefs-editwatchlist": "עריכת רשימת המעקב",
index 528968e..b807488 100644 (file)
        "undeletehistorynoadmin": "Questa pagina è stata cancellata.\nIl motivo della cancellazione è mostrato qui sotto, assieme ai dettagli dell'utente che ha modificato questa pagina prima della cancellazione.\nIl testo contenuto nelle versioni cancellate è disponibile solo agli amministratori.",
        "undelete-revision": "Versione cancellata della pagina $1, inserita il $4 alle $5 da $3:",
        "undeleterevision-missing": "Versione errata o mancante. Il collegamento è errato oppure la versione è stata già ripristinata o eliminata dall'archivio.",
+       "undeleterevision-duplicate-revid": "{{PLURAL:$1|Una versione non può essere ripristinata|$1 versioni non possono essere ripristinate}}, poiché {{PLURAL:$1|il suo|i loro}} <code>rev_id</code> {{PLURAL:$1|è già utilizzato|sono già utilizzati}}.",
        "undelete-nodiff": "Non è stata trovata nessuna versione precedente.",
        "undeletebtn": "Ripristina",
        "undeletelink": "visualizza/ripristina",
index c634c56..cc3b52e 100644 (file)
        "sp-contributions-username": "IP-мекенжайы немесе қатысушы аты:",
        "sp-contributions-toponly": "Өңдемелердің тек соңғы нұсқаларын көрсету",
        "sp-contributions-newonly": "Бет бастау өңдемелерін ғана көрсету",
+       "sp-contributions-hideminor": "Шағын өңдемелерді жасыру",
        "sp-contributions-submit": "Іздеу",
        "whatlinkshere": "Мұнда сілтейтін беттер",
        "whatlinkshere-title": "$1 дегенге сілтейтін беттер",
index ac2fb40..8552d01 100644 (file)
        "acct_creation_throttle_hit": "당신의 IP 주소를 이용한 방문자가 이전에 이미 {{PLURAL:$1|계정 $1개}}를 만들어, 계정 만들기 한도를 초과하였습니다.\n따라서 지금은 이 IP 주소로는 더 이상 계정을 만들 수 없습니다.",
        "emailauthenticated": "이메일 주소가 $2 $3에 인증되었습니다.",
        "emailnotauthenticated": "이메일 주소를 인증하지 않았습니다.\n이메일 확인 절차를 거치지 않으면 다음 이메일 기능을 사용할 수 없습니다.",
-       "noemailprefs": "이 기능을 사용하기 위해서는 사용자 환경 설정에서 이메일 주소를 설정해야 합니다.",
+       "noemailprefs": "이 기능을 사용하려면 사용자 환경 설정에서 이메일 주소를 지정하세요.",
        "emailconfirmlink": "이메일 주소 확인",
        "invalidemailaddress": "이메일 주소의 형식이 잘못되어 인식할 수 없습니다.\n정상적인 형식의 이메일을 입력하거나 칸을 비워 주세요.",
        "cannotchangeemail": "이 위키에서는 계정의 이메일 주소를 바꿀 수 없습니다.",
        "rev-showdeleted": "보이기",
        "revisiondelete": "판 삭제/되살리기",
        "revdelete-nooldid-title": "대상 판이 잘못되었습니다.",
-       "revdelete-nooldid-text": "이 기능을 수행할 특정 판을 제시하지 않았거나 해당 판이 없습니다. 또는 현재 판을 숨기려 하고 있을 수도 있습니다.",
+       "revdelete-nooldid-text": "이 기능을 수행할 대상 판을 지정하지 않았거나 해당 판이 존재하지 않습니다. 아니면 현재 판을 숨기려 하고 있을 수도 있습니다.",
        "revdelete-no-file": "해당 파일이 존재하지 않습니다.",
        "revdelete-show-file-confirm": "정말 \"<nowiki>$1</nowiki>\" 파일의 삭제된 $2 $3 버전을 보시겠습니까?",
        "revdelete-show-file-submit": "예",
        "tags-edit-success": "바뀜이 적용되었습니다.",
        "tags-edit-failure": "수정 사항이 적용될 수 없습니다: $1",
        "tags-edit-nooldid-title": "대상 판이 잘못되었습니다",
-       "tags-edit-nooldid-text": "이 기능을 수행할 특정 판을 제시하지 않았거나 해당 판이 없습니다.",
+       "tags-edit-nooldid-text": "이 기능을 수행할 대상 판을 지정하지 않았거나 해당 판이 존재하지 않습니다.",
        "tags-edit-none-selected": "추가하거나 제거할 최소 하나 이상의 태그를 선택하세요.",
        "comparepages": "문서 비교",
        "compare-page1": "첫 번째 문서",
index ebb6a50..d8bf6e3 100644 (file)
        "passwordreset-emailelement": "सदस्यनाव: \n$1\n\nअस्थायी परवलीचा शब्द: \n$2",
        "passwordreset-emailsentemail": "जर हा विपत्रपत्ता आपल्या खात्याशी संलग्न असेल तर, परवलीच्या शब्दाच्या पुनर्स्थापनेबाबत एक विपत्र पाठवण्यात येईल.",
        "passwordreset-emailsentusername": "जर या सदस्यनावाशी संलग्न विपत्रपत्ता असेल तर, परवलीचा शब्द पुनर्स्थापनाबाबत विपत्र पाठविल्या जाईल.",
-       "passwordreset-emailsent-capture": "'परवलीचा शब्द' पुनर्स्थापनेबाबत एक विपत्र पाठवण्यात आले आहे जे खाली दर्शविण्यात आले आहे.",
-       "passwordreset-emailerror-capture": "'परवलीचा शब्द' पुनर्स्थापनेबाबत एक विपत्र निर्माण करण्यात आले, जे खाली दर्शविण्यात आले आहे.परंतु,{{GENDER:$2|सदस्य}}ला पाठविणे असफल झाले: $1",
        "changeemail": "विपत्रपत्ता बदला किंवा हटवा",
        "changeemail-header": "आपला विपत्रपत्ता बदलण्यास हे आवेदन पूर्ण करा.जर आपणास आपल्या खात्याशी संलग्न कोणताही विपत्रपत्ता हटवायचा असेल तर,आवेदन सादर करण्यापूर्वी, नविन विपत्रपत्त्यासाठी असलेली जागा कोरी ठेवा.",
-       "changeemail-passwordrequired": "हे बदल नक्की करण्यासाठी आपणास आपला परवलीचा शब्द टाकावा लागेल.",
        "changeemail-no-info": "हे पान थेट बघण्यासठी तुम्हाला सनोंद-प्रवेशित असावे लागेल.",
        "changeemail-oldemail": "सध्याचा ईमेल पत्ता :",
        "changeemail-newemail": "नवा ईमेल पत्ता:",
        "undo-nochange": "असे दिसते कि हे संपादन पूर्ववत केल्या गेले आहे.",
        "undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|चर्चा]])यांची आवृत्ती $1 परतवली.",
        "undo-summary-username-hidden": "अज्ञात सदस्याची $1 आवृत्ती परतवा",
-       "cantcreateaccounttitle": "खाते उघडू शकत नाही",
        "cantcreateaccount-text": "('''$1''')या आंतरजाल अंकपत्त्याकडूनच्या खाते निर्मितीस [[User:$3|$3]]ने अटकाव केला आहे.\n\n$3ने ''$2'' कारण दिले आहे.",
        "cantcreateaccount-range-text": "<strong>$1</strong>आवाक्यातील आंतरजाल अंकपत्ते,ज्यात आपल्या (<strong>$4</strong>) या अंकपत्त्याचा समावेश आहे, [[User:$3|$3]] ने त्यांच्या खाते निर्मितीस प्रतिबंध केला आहे.\n\n$3 ने <em>$2</em>कारण दिले आहे.",
        "viewpagelogs": "या पानाच्या नोंदी पहा",
        "undeletedrevisions": "{{PLURAL:$1|1 आवर्तन|$1 आवर्तने}} पुनर्स्थापित",
        "undeletedrevisions-files": "{{PLURAL:$1|1 आवर्तन|$1 आवर्तने}}आणि {{PLURAL:$2|1 संचिका|$2 संचिका}} पुनर्स्थापित",
        "undeletedfiles": "{{PLURAL:$1|1 संचिका|$1 संचिका}} पुनर्स्थापित",
-       "cannotundelete": "उलटवणे फसले:$1",
+       "cannotundelete": "à¤\95ाहà¥\80 à¤\95िà¤\82वा à¤¸à¤°à¥\8dवà¤\9a à¤\89लà¤\9fवणà¥\87 à¤«à¤¸à¤²à¥\87:$1",
        "undeletedpage": "<strong>$1ला पुनर्स्थापित केले</strong>\n\nअलिकडिल वगळलेल्या आणि पुनर्स्थापितांच्या नोंदीकरिता [[Special:Log/delete|वगळल्याच्या नोंदी]] पहा .",
        "undelete-header": "अलीकडील वगळलेल्या पानांकरिता [[Special:Log/delete|वगळलेल्या नोंदी]] पहा.",
        "undelete-search-title": "वगळलेली पाने शोधा",
        "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": "चर्चा",
index 85894af..8108c7a 100644 (file)
        "grant-group-high-volume": "Realizar actividades em grande quantidade",
        "grant-group-customization": "Personalização e preferências",
        "grant-group-administration": "Executar acções administrativas",
+       "grant-group-private-information": "Aceder aos seus dados privados",
        "grant-group-other": "Actividade diversa",
        "grant-blockusers": "Bloquear e desbloquear utilizadores",
        "grant-createaccount": "Criar contas",
index d1f7ea4..7ef457b 100644 (file)
        "grant-group-high-volume": "Izvajanje visokoobsežnih dejavnosti",
        "grant-group-customization": "Prilagoditve in nastavitve",
        "grant-group-administration": "Izvajanje administrativnih dejanj",
+       "grant-group-private-information": "Dostop do zasebnih podatkov o vas",
        "grant-group-other": "Druga dejavnost",
        "grant-blockusers": "Blokiranje in odblokiranje uporabnikov",
        "grant-createaccount": "Ustvarjanje računov",
        "grant-highvolume": "Visokoobsežno urejanje",
        "grant-oversight": "Skrivanje uporabnikov in zatiranje redakcij",
        "grant-patrol": "Nadzor sprememb strani",
+       "grant-privateinfo": "Dostop do zasebnih podatkov",
        "grant-protect": "Zaščita in odstranitev zaščite strani",
        "grant-rollback": "Razveljavitev sprememb strani",
        "grant-sendemail": "Pošiljanje e-pošte drugim uporabnikom",
index d156e70..f81a37c 100644 (file)
        "watchthis": "Спостерігати за цією сторінкою",
        "savearticle": "Зберегти сторінку",
        "savechanges": "Зберегти зміни",
-       "publishpage": "Ð\9eпÑ\83блÑ\96кÑ\83вати сторінку",
-       "publishchanges": "Ð\9eпÑ\83блÑ\96кÑ\83вати зміни",
+       "publishpage": "Ð\97беÑ\80егти сторінку",
+       "publishchanges": "Ð\97беÑ\80егти зміни",
        "preview": "Попередній перегляд",
        "showpreview": "Попередній перегляд",
        "showdiff": "Показати зміни",
index b00ef27..7503b40 100644 (file)
        "shortpages": "چھوٹے صفحات",
        "longpages": "طویل ترین صفحات",
        "deadendpages": "مردہ صفحات",
-       "protectedpages": "محفوظ شدہ صفحات",
+       "protectedpages": "محفوظ کردہ صفحات",
        "protectedpages-noredirect": "رجوع مکررات چھپائیں",
        "protectedpages-timestamp": "وقت کی مہر",
        "protectedpages-page": "صفحہ",
index 7b604e7..71b389f 100644 (file)
        "minoredit": "Bu kichik tahrir",
        "watchthis": "Sahifani kuzatish",
        "savearticle": "Saqla",
+       "publishpage": "Sahifani chop et",
+       "publishchanges": "Oʻzgarishlarni chop et",
        "preview": "Ko‘rib chiqish",
        "showpreview": "Koʻrib chiqish",
        "showdiff": "Kiritilgan o‘zgarishlar",
        "undo-success": "Tahrirni bekor qilish imkoniyati bor. Iltimos, solishtirish oynasini koʻrib chiqib, aynan shu oʻzgarishlarni bekor qilmoqchiligingizga ishonch hosil qiling va undan keyin «Saqla» tugmasini bosing.",
        "undo-failure": "Keyingi tahrirlar bilan chalkashib ketgani sababli, ushbu tahrirni alohida oʻzini bekor qilishni iloji yoʻq.",
        "undo-summary": "[[Special:Contributions/$2|$2]] ([[User talk:$2|mun.]]) tomonidan qilingan $1-sonli tahrir qaytarildi",
-       "cantcreateaccounttitle": "Ro‘yxatdan o‘tib bo‘lmadi",
        "cantcreateaccount-text": "[[User:$3|$3]] ushbu IP manzil (<strong>$1</strong>) orqali ro‘yxatdan o‘tishni bloklab qo‘ygan.\n\n$3 <em>$2</em>ni sabab qilib ko‘rsatdi",
        "cantcreateaccount-range-text": "[[User:$3|$3]] <strong>$1</strong> sohaga tegishli IP manzillar, shu jumladan sizning IP manzilingiz (<strong>$4</strong>), orqali ro‘yxatdan o‘tishni bloklab qo‘ygan.\n\n$3 <em>$2</em>ni sabab qilib ko‘rsatdi",
        "viewpagelogs": "Ushbu sahifaga doir qaydlarni koʻrsat",
index 4e0e2a5..3956e0d 100644 (file)
        "revdelete-uname-unhid": "公开用户名",
        "revdelete-restricted": "应用对管理员的限制",
        "revdelete-unrestricted": "删除对管理员的限制",
-       "logentry-block-block": "$1{{GENDER:$2|封禁了}}{{GENDER:$4|$3}},期限$5 $6",
+       "logentry-block-block": "$1{{GENDER:$2|封禁了}}{{GENDER:$4|$3}},期限$5 $6",
        "logentry-block-unblock": "$1{{GENDER:$2|解封了}}{{GENDER:$4|$3}}",
        "logentry-block-reblock": "$1将{{GENDER:$4|$3}}的封禁设置{{GENDER:$2|更改为}}持续时间$5 $6",
        "logentry-suppress-block": "$1{{GENDER:$2|封禁了}}{{GENDER:$4|$3}},持续时间$5 $6",
        "log-action-filter-upload-upload": "新上传",
        "log-action-filter-upload-overwrite": "重新上传",
        "authmanager-authn-not-in-progress": "身份验证尚未进行,或会话数据丢失。请从头重新开始。",
-       "authmanager-authn-no-primary": "提供的证书不能被验证。",
+       "authmanager-authn-no-primary": "提供的凭据不能通过验证。",
        "authmanager-authn-no-local-user": "提供的证书没有与该wiki上的任何用户相关联。",
        "authmanager-authn-no-local-user-link": "提供的证书有效,但没有与该wiki上的任何用户相关联。请通过不同方式登录,或创建一个新用户,然后您将拥有一个把您之前的证书链接到对应账户的选项。",
        "authmanager-authn-autocreate-failed": "所有账户的自动创建失败:$1",
index 6931259..2da45ca 100644 (file)
 require_once __DIR__ . '/cleanupTable.inc';
 
 /**
- * Maintenance script to clean up broken page links when somebody turns on $wgCapitalLinks.
+ * Maintenance script to clean up broken page links when somebody turns
+ * on or off $wgCapitalLinks.
  *
  * @ingroup Maintenance
  */
 class CapsCleanup extends TableCleanup {
 
        private $user;
+       private $namespace;
 
        public function __construct() {
                parent::__construct();
@@ -47,25 +49,66 @@ class CapsCleanup extends TableCleanup {
        }
 
        public function execute() {
-               global $wgCapitalLinks;
-
-               if ( $wgCapitalLinks ) {
-                       $this->error( "\$wgCapitalLinks is on -- no need for caps links cleanup.", true );
-               }
-
                $this->user = User::newSystemUser( 'Conversion script', [ 'steal' => true ] );
 
                $this->namespace = intval( $this->getOption( 'namespace', 0 ) );
+
+               if ( MWNamespace::isCapitalized( $this->namespace ) ) {
+                       $this->output( "Will be moving pages to first letter capitalized titles" );
+                       $callback = 'processRowToUppercase';
+               } else {
+                       $this->output( "Will be moving pages to first letter lowercase titles" );
+                       $callback = 'processRowToLowercase';
+               }
+
                $this->dryrun = $this->hasOption( 'dry-run' );
 
                $this->runTable( [
                        'table' => 'page',
                        'conds' => [ 'page_namespace' => $this->namespace ],
                        'index' => 'page_id',
-                       'callback' => 'processRow' ] );
+                       'callback' => $callback ] );
        }
 
-       protected function processRow( $row ) {
+       protected function processRowToUppercase( $row ) {
+               global $wgContLang;
+
+               $current = Title::makeTitle( $row->page_namespace, $row->page_title );
+               $display = $current->getPrefixedText();
+               $lower = $row->page_title;
+               $upper = $wgContLang->ucfirst( $row->page_title );
+               if ( $upper == $lower ) {
+                       $this->output( "\"$display\" already uppercase.\n" );
+
+                       return $this->progress( 0 );
+               }
+
+               $target = Title::makeTitle( $row->page_namespace, $upper );
+               if ( $target->exists() ) {
+                       // Prefix "CapsCleanup" to bypass the conflict
+                       $target = Title::newFromText( __CLASS__ . '/' . $display );
+               }
+               $ok = $this->movePage(
+                       $current,
+                       $target,
+                       'Converting page title to first-letter uppercase',
+                       false
+               );
+               if ( $ok ) {
+                       $this->progress( 1 );
+                       if ( $row->page_namespace == $this->namespace ) {
+                               $talk = $target->getTalkPage();
+                               $row->page_namespace = $talk->getNamespace();
+                               if ( $talk->exists() ) {
+                                       return $this->processRowToUppercase( $row );
+                               }
+                       }
+               }
+
+               return $this->progress( 0 );
+       }
+
+       protected function processRowToLowercase( $row ) {
                global $wgContLang;
 
                $current = Title::makeTitle( $row->page_namespace, $row->page_title );
@@ -79,35 +122,51 @@ class CapsCleanup extends TableCleanup {
                }
 
                $target = Title::makeTitle( $row->page_namespace, $lower );
-               $targetDisplay = $target->getPrefixedText();
                if ( $target->exists() ) {
+                       $targetDisplay = $target->getPrefixedText();
                        $this->output( "\"$display\" skipped; \"$targetDisplay\" already exists\n" );
 
                        return $this->progress( 0 );
                }
 
-               if ( $this->dryrun ) {
-                       $this->output( "\"$display\" -> \"$targetDisplay\": DRY RUN, NOT MOVED\n" );
-                       $ok = true;
-               } else {
-                       $mp = new MovePage( $current, $target );
-                       $status = $mp->move( $this->user, 'Converting page titles to lowercase', true );
-                       $ok = $status->isOK() ? 'OK' : $status->getWikiText( false, false, 'en' );
-                       $this->output( "\"$display\" -> \"$targetDisplay\": $ok\n" );
-               }
+               $ok = $this->movePage( $current, $target, 'Converting page titles to lowercase', true );
                if ( $ok === true ) {
                        $this->progress( 1 );
                        if ( $row->page_namespace == $this->namespace ) {
                                $talk = $target->getTalkPage();
                                $row->page_namespace = $talk->getNamespace();
                                if ( $talk->exists() ) {
-                                       return $this->processRow( $row );
+                                       return $this->processRowToLowercase( $row );
                                }
                        }
                }
 
                return $this->progress( 0 );
        }
+
+       /**
+        * @param Title $current
+        * @param Title $target
+        * @param string $reason
+        * @param bool $createRedirect
+        * @return bool Success
+        */
+       private function movePage( Title $current, Title $target, $reason, $createRedirect ) {
+               $display = $current->getPrefixedText();
+               $targetDisplay = $target->getPrefixedText();
+
+               if ( $this->dryrun ) {
+                       $this->output( "\"$display\" -> \"$targetDisplay\": DRY RUN, NOT MOVED\n" );
+                       $ok = 'OK';
+               } else {
+                       $mp = new MovePage( $current, $target );
+                       $status = $mp->move( $this->user, $reason, $createRedirect );
+                       $ok = $status->isOK() ? 'OK' : $status->getWikiText( false, false, 'en' );
+                       $this->output( "\"$display\" -> \"$targetDisplay\": $ok\n" );
+               }
+
+               return $ok === 'OK';
+       }
 }
 
 $maintClass = "CapsCleanup";
index 99b9029..788d304 100644 (file)
@@ -3087,7 +3087,7 @@ class AuthManagerTest extends \MediaWikiTestCase {
                $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
                $expected = [
                        $rememberReq,
-                       $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
                        $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
                        $makeReq( "required2", AuthenticationRequest::PRIMARY_REQUIRED ),
                        $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
@@ -3107,10 +3107,10 @@ class AuthManagerTest extends \MediaWikiTestCase {
                $actual = $this->manager->getAuthenticationRequests( AuthManager::ACTION_LOGIN );
                $expected = [
                        $rememberReq,
-                       $makeReq( "primary-shared", AuthenticationRequest::REQUIRED ),
-                       $makeReq( "required", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "primary-shared", AuthenticationRequest::PRIMARY_REQUIRED ),
+                       $makeReq( "required", AuthenticationRequest::PRIMARY_REQUIRED ),
                        $makeReq( "optional", AuthenticationRequest::OPTIONAL ),
-                       $makeReq( "foo", AuthenticationRequest::REQUIRED ),
+                       $makeReq( "foo", AuthenticationRequest::PRIMARY_REQUIRED ),
                        $makeReq( "bar", AuthenticationRequest::REQUIRED ),
                        $makeReq( "baz", AuthenticationRequest::REQUIRED ),
                ];
index 6446416..86ce53f 100644 (file)
@@ -86,6 +86,25 @@ class ResourcesTest extends MediaWikiTestCase {
                }
        }
 
+       /**
+        * Verify that all specified messages actually exist.
+        */
+       public function testMissingMessages() {
+               $data = self::getAllModules();
+               $validDeps = array_keys( $data['modules'] );
+               $lang = Language::factory( 'en' );
+
+               /** @var ResourceLoaderModule $module */
+               foreach ( $data['modules'] as $moduleName => $module ) {
+                       foreach ( $module->getMessages() as $msgKey ) {
+                               $this->assertTrue(
+                                       wfMessage( $msgKey )->useDatabase( false )->inLanguage( $lang )->exists(),
+                                       "Message '$msgKey' required by '$moduleName' must exist"
+                               );
+                       }
+               }
+       }
+
        /**
         * Verify that all dependencies of all modules are always satisfiable with the 'targets' defined
         * for the involved modules.