Merge "Fix comment about how to use AutoloadGenerator"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Thu, 11 Aug 2016 07:09:03 +0000 (07:09 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 11 Aug 2016 07:09:03 +0000 (07:09 +0000)
37 files changed:
docs/hooks.txt
includes/DefaultSettings.php
includes/FeedUtils.php
includes/actions/PurgeAction.php
includes/api/ApiStashEdit.php
includes/api/ApiUpload.php
includes/api/i18n/en.json
includes/api/i18n/qqq.json
includes/db/loadbalancer/LBFactory.php
includes/db/loadbalancer/LBFactoryMulti.php
includes/db/loadbalancer/LBFactorySimple.php
includes/db/loadbalancer/LBFactorySingle.php
includes/db/loadbalancer/LoadBalancer.php
includes/installer/MssqlUpdater.php
includes/installer/MysqlUpdater.php
includes/installer/OracleUpdater.php
includes/installer/PostgresUpdater.php
includes/installer/SqliteUpdater.php
includes/media/DjVuImage.php
includes/parser/Parser.php
includes/specialpage/SpecialPage.php
includes/specials/SpecialUpload.php
includes/upload/UploadBase.php
includes/upload/UploadFromChunks.php
includes/upload/UploadFromStash.php
languages/i18n/en.json
languages/i18n/qqq.json
maintenance/archives/patch-add-rc_name_type_patrolled_timestamp_index.sql [new file with mode: 0644]
maintenance/initEditCount.php
maintenance/mssql/tables.sql
maintenance/oracle/archives/patch-add-rc_name_type_patrolled_timestamp_index.sql [new file with mode: 0644]
maintenance/oracle/tables.sql
maintenance/postgres/tables.sql
maintenance/purgeChangedFiles.php
maintenance/storage/fixBug20757.php
maintenance/tables.sql
resources/src/mediawiki.action/mediawiki.action.edit.stash.js

index 8fa3793..5cf8ffe 100644 (file)
@@ -3373,6 +3373,18 @@ added to the descriptor
 &$radio: Boolean, if source type should be shown as radio button
 $selectedSourceType: The selected source type
 
+'UploadStashFile': Before a file is stashed (uploaded to stash).
+Note that code which has not been updated for MediaWiki 1.28 may not call this
+hook. If your extension absolutely, positively must prevent some files from
+being uploaded, use UploadVerifyFile or UploadVerifyUpload.
+$upload: (object) An instance of UploadBase, with all info about the upload
+$user: (object) An instance of User, the user uploading this file
+$props: (array) File properties, as returned by FSFile::getPropsFromPath()
+&$error: output: If the file stashing should be prevented, set this to the reason
+  in the form of array( messagename, param1, param2, ... ) or a MessageSpecifier
+  instance (you might want to use ApiMessage to provide machine-readable details
+  for the API).
+
 'UploadVerification': DEPRECATED! Use UploadVerifyFile instead.
 Additional chances to reject an uploaded file.
 $saveName: (string) destination file name
index 8363b32..2ac31bf 100644 (file)
@@ -5051,7 +5051,7 @@ $wgGroupPermissions['user']['upload'] = true;
 $wgGroupPermissions['user']['reupload'] = true;
 $wgGroupPermissions['user']['reupload-shared'] = true;
 $wgGroupPermissions['user']['minoredit'] = true;
-$wgGroupPermissions['user']['purge'] = true; // can use ?action=purge without clicking "ok"
+$wgGroupPermissions['user']['purge'] = true;
 $wgGroupPermissions['user']['sendemail'] = true;
 $wgGroupPermissions['user']['applychangetags'] = true;
 $wgGroupPermissions['user']['changetags'] = true;
index d7dbd22..071a3db 100644 (file)
@@ -39,6 +39,7 @@ class FeedUtils {
                global $wgRequest, $wgUser;
 
                $purge = $wgRequest->getVal( 'action' ) === 'purge';
+               // Allow users with 'purge' right to clear feed caches
                if ( $purge && $wgUser->isAllowed( 'purge' ) ) {
                        $cache = ObjectCache::getMainWANInstance();
                        $cache->delete( $timekey, 1 );
index 576533d..b2002ff 100644 (file)
  */
 
 /**
- * User-requested page cache purging.
- *
- * For users with 'purge', this will directly trigger the cache purging and
- * for users without that right, it will show a confirmation form.
+ * User-requested page cache purging
  *
  * @ingroup Actions
  */
@@ -48,10 +45,6 @@ class PurgeAction extends FormAction {
                return $this->page->doPurge();
        }
 
-       /**
-        * purge is slightly weird because it can be either formed or formless depending
-        * on user permissions
-        */
        public function show() {
                $this->setHeaders();
 
@@ -65,11 +58,7 @@ class PurgeAction extends FormAction {
                        return;
                }
 
-               if ( $user->isAllowed( 'purge' ) ) {
-                       // This will update the database immediately, even on HTTP GET.
-                       // Lots of uses may exist for this feature, so just ignore warnings.
-                       Profiler::instance()->getTransactionProfiler()->resetExpectations();
-
+               if ( $this->getRequest()->wasPosted() ) {
                        $this->redirectParams = wfArrayToCgi( array_diff_key(
                                $this->getRequest()->getQueryValues(),
                                [ 'title' => null, 'action' => null ]
index 446a98c..297c0fb 100644 (file)
@@ -39,6 +39,7 @@ class ApiStashEdit extends ApiBase {
        const ERROR_PARSE = 'error_parse';
        const ERROR_CACHE = 'error_cache';
        const ERROR_UNCACHEABLE = 'uncacheable';
+       const ERROR_BUSY = 'busy';
 
        const PRESUME_FRESH_TTL_SEC = 30;
        const MAX_CACHE_TTL = 300; // 5 minutes
@@ -51,6 +52,7 @@ class ApiStashEdit extends ApiBase {
                        $this->dieUsage( 'This interface is not supported for bots', 'botsnotsupported' );
                }
 
+               $cache = ObjectCache::getLocalClusterInstance();
                $page = $this->getTitleOrPageId( $params );
                $title = $page->getTitle();
 
@@ -60,8 +62,23 @@ class ApiStashEdit extends ApiBase {
                        $this->dieUsage( 'Unsupported content model/format', 'badmodelformat' );
                }
 
-               // Trim and fix newlines so the key SHA1's match (see RequestContext::getText())
-               $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
+               if ( strlen( $params['stashedtexthash'] ) ) {
+                       // Load from cache since the client indicates the text is the same as last stash
+                       $textHash = $params['stashedtexthash'];
+                       $textKey = $cache->makeKey( 'stashedit', 'text', $textHash );
+                       $text = $cache->get( $textKey );
+                       if ( !is_string( $text ) ) {
+                               $this->dieUsage( 'No stashed text found with the given hash', 'missingtext' );
+                       }
+               } elseif ( $params['text'] !== null ) {
+                       // Trim and fix newlines so the key SHA1's match (see WebRequest::getText())
+                       $text = rtrim( str_replace( "\r\n", "\n", $params['text'] ) );
+                       $textHash = sha1( $text );
+               } else {
+                       $this->dieUsage(
+                               'The text or stashedtexthash parameter must be given', 'missingtextparam' );
+               }
+
                $textContent = ContentHandler::makeContent(
                        $text, $title, $params['contentmodel'], $params['contentformat'] );
 
@@ -113,24 +130,24 @@ class ApiStashEdit extends ApiBase {
                // The user will abort the AJAX request by pressing "save", so ignore that
                ignore_user_abort( true );
 
-               // Use the master DB for fast blocking locks
-               $dbw = wfGetDB( DB_MASTER );
-
-               // Get a key based on the source text, format, and user preferences
-               $key = self::getStashKey( $title, $content, $user );
-               // De-duplicate requests on the same key
                if ( $user->pingLimiter( 'stashedit' ) ) {
                        $status = 'ratelimited';
-               } elseif ( $dbw->lock( $key, __METHOD__, 1 ) ) {
-                       $status = self::parseAndStash( $page, $content, $user, $params['summary'] );
-                       $dbw->unlock( $key, __METHOD__ );
                } else {
-                       $status = 'busy';
+                       $status = self::parseAndStash( $page, $content, $user, $params['summary'] );
+                       $textKey = $cache->makeKey( 'stashedit', 'text', $textHash );
+                       $cache->set( $textKey, $text, self::MAX_CACHE_TTL );
                }
 
                $this->getStats()->increment( "editstash.cache_stores.$status" );
 
-               $this->getResult()->addValue( null, $this->getModuleName(), [ 'status' => $status ] );
+               $this->getResult()->addValue(
+                       null,
+                       $this->getModuleName(),
+                       [
+                               'status' => $status,
+                               'texthash' => $textHash
+                       ]
+               );
        }
 
        /**
@@ -145,17 +162,41 @@ class ApiStashEdit extends ApiBase {
                $cache = ObjectCache::getLocalClusterInstance();
                $logger = LoggerFactory::getInstance( 'StashEdit' );
 
-               $format = $content->getDefaultFormat();
-               $editInfo = $page->prepareContentForEdit( $content, null, $user, $format, false );
                $title = $page->getTitle();
+               $key = self::getStashKey( $title, self::getContentHash( $content ), $user );
 
-               if ( $editInfo && $editInfo->output ) {
-                       $key = self::getStashKey( $title, $content, $user );
+               // Use the master DB for fast blocking locks
+               $dbw = wfGetDB( DB_MASTER );
+               if ( !$dbw->lock( $key, __METHOD__, 1 ) ) {
+                       // De-duplicate requests on the same key
+                       return self::ERROR_BUSY;
+               }
+               $unlocker = new ScopedCallback( function () use ( $dbw, $key ) {
+                       $dbw->unlock( $key, __METHOD__ );
+               } );
+
+               $cutoffTime = time() - self::PRESUME_FRESH_TTL_SEC;
+
+               // Reuse any freshly build matching edit stash cache
+               $editInfo = $cache->get( $key );
+               if ( $editInfo && wfTimestamp( TS_UNIX, $editInfo->timestamp ) >= $cutoffTime ) {
+                       $alreadyCached = true;
+               } else {
+                       $format = $content->getDefaultFormat();
+                       $editInfo = $page->prepareContentForEdit( $content, null, $user, $format, false );
+                       $alreadyCached = false;
+               }
 
+               if ( $editInfo && $editInfo->output ) {
                        // Let extensions add ParserOutput metadata or warm other caches
                        Hooks::run( 'ParserOutputStashForEdit',
                                [ $page, $content, $editInfo->output, $summary, $user ] );
 
+                       if ( $alreadyCached ) {
+                               $logger->debug( "Already cached parser output for key '$key' ('$title')." );
+                               return self::ERROR_NONE;
+                       }
+
                        list( $stashInfo, $ttl, $code ) = self::buildStashValue(
                                $editInfo->pstContent,
                                $editInfo->output,
@@ -207,7 +248,7 @@ class ApiStashEdit extends ApiBase {
                $logger = LoggerFactory::getInstance( 'StashEdit' );
                $stats = RequestContext::getMain()->getStats();
 
-               $key = self::getStashKey( $title, $content, $user );
+               $key = self::getStashKey( $title, self::getContentHash( $content ), $user );
                $editInfo = $cache->get( $key );
                if ( !is_object( $editInfo ) ) {
                        $start = microtime( true );
@@ -282,6 +323,20 @@ class ApiStashEdit extends ApiBase {
                return wfTimestampOrNull( TS_MW, $time );
        }
 
+       /**
+        * Get hash of the content, factoring in model/format
+        *
+        * @param Content $content
+        * @return string
+        */
+       private static function getContentHash( Content $content ) {
+               return sha1( implode( "\n", [
+                       $content->getModel(),
+                       $content->getDefaultFormat(),
+                       $content->serialize( $content->getDefaultFormat() )
+               ] ) );
+       }
+
        /**
         * Get the temporary prepared edit stash key for a user
         *
@@ -290,22 +345,19 @@ class ApiStashEdit extends ApiBase {
         *   - b) The parser output was made from the PST using cannonical matching options
         *
         * @param Title $title
-        * @param Content $content
+        * @param string $contentHash Result of getContentHash()
         * @param User $user User to get parser options from
         * @return string
         */
-       private static function getStashKey( Title $title, Content $content, User $user ) {
-               $hash = sha1( implode( ':', [
+       private static function getStashKey( Title $title, $contentHash, User $user ) {
+               return ObjectCache::getLocalClusterInstance()->makeKey(
+                       'prepared-edit',
+                       md5( $title->getPrefixedDBkey() ),
                        // Account for the edit model/text
-                       $content->getModel(),
-                       $content->getDefaultFormat(),
-                       sha1( $content->serialize( $content->getDefaultFormat() ) ),
+                       $contentHash,
                        // Account for user name related variables like signatures
-                       $user->getId(),
-                       md5( $user->getName() )
-               ] ) );
-
-               return wfMemcKey( 'prepared-edit', md5( $title->getPrefixedDBkey() ), $hash );
+                       md5( $user->getId() . "\n" . $user->getName() )
+               );
        }
 
        /**
@@ -313,7 +365,7 @@ class ApiStashEdit extends ApiBase {
         *
         * This makes a simple version of WikiPage::prepareContentForEdit() as stash info
         *
-        * @param Content $pstContent
+        * @param Content $pstContent Pre-Save transformed content
         * @param ParserOutput $parserOutput
         * @param string $timestamp TS_MW
         * @param User $user
@@ -355,7 +407,11 @@ class ApiStashEdit extends ApiBase {
                        ],
                        'text' => [
                                ApiBase::PARAM_TYPE => 'text',
-                               ApiBase::PARAM_REQUIRED => true
+                               ApiBase::PARAM_DFLT => null
+                       ],
+                       'stashedtexthash' => [
+                               ApiBase::PARAM_TYPE => 'string',
+                               ApiBase::PARAM_DFLT => null
                        ],
                        'summary' => [
                                ApiBase::PARAM_TYPE => 'string',
index 95659e5..89d9af8 100644 (file)
@@ -64,7 +64,8 @@ class ApiUpload extends ApiBase {
                                $this->dieUsage( 'No upload module set', 'nomodule' );
                        }
                } catch ( UploadStashException $e ) { // XXX: don't spam exception log
-                       $this->handleStashException( $e );
+                       list( $msg, $code ) = $this->handleStashException( get_class( $e ), $e->getMessage() );
+                       $this->dieUsage( $msg, $code );
                }
 
                // First check permission to upload
@@ -112,7 +113,8 @@ class ApiUpload extends ApiBase {
                                $result['imageinfo'] = $this->mUpload->getImageInfo( $this->getResult() );
                        }
                } catch ( UploadStashException $e ) { // XXX: don't spam exception log
-                       $this->handleStashException( $e );
+                       list( $msg, $code ) = $this->handleStashException( get_class( $e ), $e->getMessage() );
+                       $this->dieUsage( $msg, $code );
                }
 
                $this->getResult()->addValue( null, $this->getModuleName(), $result );
@@ -156,20 +158,13 @@ class ApiUpload extends ApiBase {
         */
        private function getStashResult( $warnings ) {
                $result = [];
+               $result['result'] = 'Success';
+               if ( $warnings && count( $warnings ) > 0 ) {
+                       $result['warnings'] = $warnings;
+               }
                // Some uploads can request they be stashed, so as not to publish them immediately.
                // In this case, a failure to stash ought to be fatal
-               try {
-                       $result['result'] = 'Success';
-                       $result['filekey'] = $this->performStash();
-                       $result['sessionkey'] = $result['filekey']; // backwards compatibility
-                       if ( $warnings && count( $warnings ) > 0 ) {
-                               $result['warnings'] = $warnings;
-                       }
-               } catch ( UploadStashException $e ) {
-                       $this->handleStashException( $e );
-               } catch ( Exception $e ) {
-                       $this->dieUsage( $e->getMessage(), 'stashfailed' );
-               }
+               $this->performStash( 'critical', $result );
 
                return $result;
        }
@@ -185,12 +180,7 @@ class ApiUpload extends ApiBase {
                $result['warnings'] = $warnings;
                // in case the warnings can be fixed with some further user action, let's stash this upload
                // and return a key they can use to restart it
-               try {
-                       $result['filekey'] = $this->performStash();
-                       $result['sessionkey'] = $result['filekey']; // backwards compatibility
-               } catch ( Exception $e ) {
-                       $result['warnings']['stashfailed'] = $e->getMessage();
-               }
+               $this->performStash( 'optional', $result );
 
                return $result;
        }
@@ -228,14 +218,7 @@ class ApiUpload extends ApiBase {
                }
 
                if ( $this->mParams['offset'] == 0 ) {
-                       try {
-                               $filekey = $this->performStash();
-                       } catch ( UploadStashException $e ) {
-                               $this->handleStashException( $e );
-                       } catch ( Exception $e ) {
-                               // FIXME: Error handling here is wrong/different from rest of this
-                               $this->dieUsage( $e->getMessage(), 'stashfailed' );
-                       }
+                       $filekey = $this->performStash( 'critical' );
                } else {
                        $filekey = $this->mParams['filekey'];
 
@@ -320,27 +303,57 @@ class ApiUpload extends ApiBase {
        }
 
        /**
-        * Stash the file and return the file key
-        * Also re-raises exceptions with slightly more informative message strings (useful for API)
-        * @throws MWException
-        * @return string File key
+        * Stash the file and add the file key, or error information if it fails, to the data.
+        *
+        * @param string $failureMode What to do on failure to stash:
+        *   - When 'critical', use dieStatus() to produce an error response and throw an exception.
+        *     Use this when stashing the file was the primary purpose of the API request.
+        *   - When 'optional', only add a 'stashfailed' key to the data and return null.
+        *     Use this when some error happened for a non-stash upload and we're stashing the file
+        *     only to save the client the trouble of re-uploading it.
+        * @param array &$data API result to which to add the information
+        * @return string|null File key
         */
-       private function performStash() {
+       private function performStash( $failureMode, &$data = null ) {
                try {
-                       $stashFile = $this->mUpload->stashFile( $this->getUser() );
+                       $status = $this->mUpload->tryStashFile( $this->getUser() );
 
-                       if ( !$stashFile ) {
-                               throw new MWException( 'Invalid stashed file' );
+                       if ( $status->isGood() && !$status->getValue() ) {
+                               // Not actually a 'good' status...
+                               $status->fatal( new ApiRawMessage( 'Invalid stashed file', 'stashfailed' ) );
                        }
-                       $fileKey = $stashFile->getFileKey();
                } catch ( Exception $e ) {
-                       $message = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
-                       wfDebug( __METHOD__ . ' ' . $message . "\n" );
-                       $className = get_class( $e );
-                       throw new $className( $message );
+                       $debugMessage = 'Stashing temporary file failed: ' . get_class( $e ) . ' ' . $e->getMessage();
+                       wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" );
+                       $status = Status::newFatal( new ApiRawMessage( $e->getMessage(), 'stashfailed' ) );
+               }
+
+               if ( $status->isGood() ) {
+                       $stashFile = $status->getValue();
+                       $data['filekey'] = $stashFile->getFileKey();
+                       // Backwards compatibility
+                       $data['sessionkey'] = $data['filekey'];
+                       return $data['filekey'];
+               }
+
+               if ( $status->getMessage()->getKey() === 'uploadstash-exception' ) {
+                       // The exceptions thrown by upload stash code and pretty silly and UploadBase returns poor
+                       // Statuses for it. Just extract the exception details and parse them ourselves.
+                       list( $exceptionType, $message ) = $status->getMessage()->getParams();
+                       $debugMessage = 'Stashing temporary file failed: ' . $exceptionType . ' ' . $message;
+                       wfDebug( __METHOD__ . ' ' . $debugMessage . "\n" );
+                       list( $msg, $code ) = $this->handleStashException( $exceptionType, $message );
+                       $status = Status::newFatal( new ApiRawMessage( $msg, $code ) );
                }
 
-               return $fileKey;
+               // Bad status
+               if ( $failureMode !== 'optional' ) {
+                       $this->dieStatus( $status );
+               } else {
+                       list( $code, $msg ) = $this->getErrorFromStatus( $status );
+                       $data['stashfailed'] = $msg;
+                       return null;
+               }
        }
 
        /**
@@ -354,12 +367,7 @@ class ApiUpload extends ApiBase {
         * @throws UsageException
         */
        private function dieRecoverableError( $error, $parameter, $data = [], $code = 'unknownerror' ) {
-               try {
-                       $data['filekey'] = $this->performStash();
-                       $data['sessionkey'] = $data['filekey'];
-               } catch ( Exception $e ) {
-                       $data['stashfailed'] = $e->getMessage();
-               }
+               $this->performStash( 'optional', $data );
                $data['invalidparameter'] = $parameter;
 
                $parsed = $this->parseMsg( $error );
@@ -653,49 +661,41 @@ class ApiUpload extends ApiBase {
 
        /**
         * Handles a stash exception, giving a useful error to the user.
-        * @param Exception $e The exception we encountered.
+        * @param string $exceptionType Class name of the exception we encountered.
+        * @param string $message Message of the exception we encountered.
+        * @return array Array of message and code, suitable for passing to dieUsage()
         */
-       protected function handleStashException( $e ) {
-               $exceptionType = get_class( $e );
-
+       protected function handleStashException( $exceptionType, $message ) {
                switch ( $exceptionType ) {
                        case 'UploadStashFileNotFoundException':
-                               $this->dieUsage(
-                                       'Could not find the file in the stash: ' . $e->getMessage(),
+                               return [
+                                       'Could not find the file in the stash: ' . $message,
                                        'stashedfilenotfound'
-                               );
-                               break;
+                               ];
                        case 'UploadStashBadPathException':
-                               $this->dieUsage(
-                                       'File key of improper format or otherwise invalid: ' . $e->getMessage(),
+                               return [
+                                       'File key of improper format or otherwise invalid: ' . $message,
                                        'stashpathinvalid'
-                               );
-                               break;
+                               ];
                        case 'UploadStashFileException':
-                               $this->dieUsage(
-                                       'Could not store upload in the stash: ' . $e->getMessage(),
+                               return [
+                                       'Could not store upload in the stash: ' . $message,
                                        'stashfilestorage'
-                               );
-                               break;
+                               ];
                        case 'UploadStashZeroLengthFileException':
-                               $this->dieUsage(
+                               return [
                                        'File is of zero length, and could not be stored in the stash: ' .
-                                               $e->getMessage(),
+                                               $message,
                                        'stashzerolength'
-                               );
-                               break;
+                               ];
                        case 'UploadStashNotLoggedInException':
-                               $this->dieUsage( 'Not logged in: ' . $e->getMessage(), 'stashnotloggedin' );
-                               break;
+                               return [ 'Not logged in: ' . $message, 'stashnotloggedin' ];
                        case 'UploadStashWrongOwnerException':
-                               $this->dieUsage( 'Wrong owner: ' . $e->getMessage(), 'stashwrongowner' );
-                               break;
+                               return [ 'Wrong owner: ' . $message, 'stashwrongowner' ];
                        case 'UploadStashNoSuchKeyException':
-                               $this->dieUsage( 'No such filekey: ' . $e->getMessage(), 'stashnosuchfilekey' );
-                               break;
+                               return [ 'No such filekey: ' . $message, 'stashnosuchfilekey' ];
                        default:
-                               $this->dieUsage( $exceptionType . ': ' . $e->getMessage(), 'stasherror' );
-                               break;
+                               return [ $exceptionType . ': ' . $message, 'stasherror' ];
                }
        }
 
index 2f46e71..1815836 100644 (file)
        "apihelp-stashedit-param-section": "Section number. <kbd>0</kbd> for the top section, <kbd>new</kbd> for a new section.",
        "apihelp-stashedit-param-sectiontitle": "The title for a new section.",
        "apihelp-stashedit-param-text": "Page content.",
+       "apihelp-stashedit-param-stashedtexthash": "Page content hash from a prior stash to use instead.",
        "apihelp-stashedit-param-contentmodel": "Content model of the new content.",
        "apihelp-stashedit-param-contentformat": "Content serialization format used for the input text.",
        "apihelp-stashedit-param-baserevid": "Revision ID of the base revision.",
index cbd9fb8..650acb9 100644 (file)
        "apihelp-stashedit-param-section": "{{doc-apihelp-param|stashedit|section}}",
        "apihelp-stashedit-param-sectiontitle": "{{doc-apihelp-param|stashedit|sectiontitle}}",
        "apihelp-stashedit-param-text": "{{doc-apihelp-param|stashedit|text}}",
+       "apihelp-stashedit-param-stashedtexthash": "{{doc-apihelp-param|stashedit|stashedtexthash}}",
        "apihelp-stashedit-param-contentmodel": "{{doc-apihelp-param|stashedit|contentmodel}}",
        "apihelp-stashedit-param-contentformat": "{{doc-apihelp-param|stashedit|contentformat}}",
        "apihelp-stashedit-param-baserevid": "{{doc-apihelp-param|stashedit|baserevid}}",
index 053f9f8..4078a39 100644 (file)
@@ -31,15 +31,16 @@ use MediaWiki\Logger\LoggerFactory;
  * @ingroup Database
  */
 abstract class LBFactory implements DestructibleService {
-
        /** @var ChronologyProtector */
        protected $chronProt;
-
        /** @var TransactionProfiler */
        protected $trxProfiler;
-
        /** @var LoggerInterface */
-       protected $logger;
+       protected $trxLogger;
+       /** @var BagOStuff */
+       protected $srvCache;
+       /** @var WANObjectCache */
+       protected $wanCache;
 
        /** @var string|bool Reason all LBs are read-only or false if not */
        protected $readOnlyReason = false;
@@ -49,15 +50,28 @@ abstract class LBFactory implements DestructibleService {
        /**
         * Construct a factory based on a configuration array (typically from $wgLBFactoryConf)
         * @param array $conf
+        * @TODO: inject objects via dependency framework
         */
        public function __construct( array $conf ) {
                if ( isset( $conf['readOnlyReason'] ) && is_string( $conf['readOnlyReason'] ) ) {
                        $this->readOnlyReason = $conf['readOnlyReason'];
                }
-
                $this->chronProt = $this->newChronologyProtector();
                $this->trxProfiler = Profiler::instance()->getTransactionProfiler();
-               $this->logger = LoggerFactory::getInstance( 'DBTransaction' );
+               // Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804)
+               $cache = ObjectCache::getLocalServerInstance();
+               if ( $cache->getQoS( $cache::ATTR_EMULATION ) > $cache::QOS_EMULATION_SQL ) {
+                       $this->srvCache = $cache;
+               } else {
+                       $this->srvCache = new EmptyBagOStuff();
+               }
+               $wCache = ObjectCache::getMainWANInstance();
+               if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) {
+                       $this->wanCache = $wCache;
+               } else {
+                       $this->wanCache = WANObjectCache::newEmpty();
+               }
+               $this->trxLogger = LoggerFactory::getInstance( 'DBTransaction' );
        }
 
        /**
@@ -261,7 +275,7 @@ abstract class LBFactory implements DestructibleService {
                        foreach ( $callersByDB as $db => $callers ) {
                                $msg .= "$db: " . implode( '; ', $callers ) . "\n";
                        }
-                       $this->logger->info( $msg );
+                       $this->trxLogger->info( $msg );
                }
        }
 
index 32b1a0f..0f3ca93 100644 (file)
@@ -317,7 +317,9 @@ class LBFactoryMulti extends LBFactory {
                        'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
                        'loadMonitor' => $this->loadMonitorClass,
                        'readOnlyReason' => $readOnlyReason,
-                       'trxProfiler' => $this->trxProfiler
+                       'trxProfiler' => $this->trxProfiler,
+                       'srvCache' => $this->srvCache,
+                       'wanCache' => $this->wanCache
                ] );
        }
 
index 1b0a1f3..14baf2e 100644 (file)
@@ -84,12 +84,7 @@ class LBFactorySimple extends LBFactory {
                        ] ];
                }
 
-               return new LoadBalancer( [
-                       'servers' => $servers,
-                       'loadMonitor' => $this->loadMonitorClass,
-                       'readOnlyReason' => $this->readOnlyReason,
-                       'trxProfiler' => $this->trxProfiler
-               ] );
+               return $this->newLoadBalancer( $servers );
        }
 
        /**
@@ -118,12 +113,7 @@ class LBFactorySimple extends LBFactory {
                        throw new MWException( __METHOD__ . ": Unknown cluster \"$cluster\"" );
                }
 
-               return new LoadBalancer( [
-                       'servers' => $wgExternalServers[$cluster],
-                       'loadMonitor' => $this->loadMonitorClass,
-                       'readOnlyReason' => $this->readOnlyReason,
-                       'trxProfiler' => $this->trxProfiler
-               ] );
+               return $this->newLoadBalancer( $wgExternalServers[$cluster] );
        }
 
        /**
@@ -141,6 +131,17 @@ class LBFactorySimple extends LBFactory {
                return $this->extLBs[$cluster];
        }
 
+       private function newLoadBalancer( array $servers ) {
+               return new LoadBalancer( [
+                       'servers' => $servers,
+                       'loadMonitor' => $this->loadMonitorClass,
+                       'readOnlyReason' => $this->readOnlyReason,
+                       'trxProfiler' => $this->trxProfiler,
+                       'srvCache' => $this->srvCache,
+                       'wanCache' => $this->wanCache
+               ] );
+       }
+
        /**
         * Execute a function for each tracked load balancer
         * The callback is called with the load balancer as the first parameter,
index 79ca3a7..14c1c28 100644 (file)
@@ -37,7 +37,9 @@ class LBFactorySingle extends LBFactory {
 
                $this->lb = new LoadBalancerSingle( [
                        'readOnlyReason' => $this->readOnlyReason,
-                       'trxProfiler' => $this->trxProfiler
+                       'trxProfiler' => $this->trxProfiler,
+                       'srvCache' => $this->srvCache,
+                       'wanCache' => $this->wanCache
                ] + $conf );
        }
 
@@ -106,7 +108,9 @@ class LoadBalancerSingle extends LoadBalancer {
                                        'load' => 1,
                                ]
                        ],
-                       'trxProfiler' => $this->trxProfiler
+                       'trxProfiler' => isset( $params['trxProfiler'] ) ? $params['trxProfiler'] : null,
+                       'srvCache' => isset( $params['srvCache'] ) ? $params['srvCache'] : null,
+                       'wanCache' => isset( $params['wanCache'] ) ? $params['wanCache'] : null
                ] );
 
                if ( isset( $params['readOnlyReason'] ) ) {
index 1269c3e..a7c486c 100644 (file)
@@ -91,6 +91,8 @@ class LoadBalancer {
         *  - servers : Required. Array of server info structures.
         *  - loadMonitor : Name of a class used to fetch server lag and load.
         *  - readOnlyReason : Reason the master DB is read-only if so [optional]
+        *  - srvCache : BagOStuff object [optional]
+        *  - wanCache : WANObjectCache object [optional]
         * @throws MWException
         */
        public function __construct( array $params ) {
@@ -138,21 +140,16 @@ class LoadBalancer {
                        }
                }
 
-               // Use APC/memcached style caching, but avoids loops with CACHE_DB (T141804)
-               // @TODO: inject these in via LBFactory at some point
-               $cache = ObjectCache::getLocalServerInstance();
-               if ( $cache->getQoS( $cache::ATTR_EMULATION ) > $cache::QOS_EMULATION_SQL ) {
-                       $this->srvCache = $cache;
+               if ( isset( $params['srvCache'] ) ) {
+                       $this->srvCache = $params['srvCache'];
                } else {
                        $this->srvCache = new EmptyBagOStuff();
                }
-               $wCache = ObjectCache::getMainWANInstance();
-               if ( $wCache->getQoS( $wCache::ATTR_EMULATION ) > $wCache::QOS_EMULATION_SQL ) {
-                       $this->wanCache = $wCache;
+               if ( isset( $params['wanCache'] ) ) {
+                       $this->wanCache = $params['wanCache'];
                } else {
                        $this->wanCache = WANObjectCache::newEmpty();
                }
-
                if ( isset( $params['trxProfiler'] ) ) {
                        $this->trxProfiler = $params['trxProfiler'];
                } else {
index accc42f..770d3bf 100644 (file)
@@ -87,7 +87,11 @@ class MssqlUpdater extends DatabaseUpdater {
                        [ 'updateSchema', 'recentchanges', 'recentchanges-drop-fks',
                                'patch-recentchanges-drop-fks.sql' ],
                        [ 'updateSchema', 'logging', 'logging-drop-fks', 'patch-logging-drop-fks.sql' ],
-                       [ 'updateSchema', 'archive', 'archive-drop-fks', 'patch-archive-drop-fks.sql' ]
+                       [ 'updateSchema', 'archive', 'archive-drop-fks', 'patch-archive-drop-fks.sql' ],
+
+                       // 1.28
+                       [ 'addIndex', 'recentchanges', 'rc_name_type_patrolled_timestamp',
+                               'patch-add-rc_name_type_patrolled_timestamp_index.sql' ],
                ];
        }
 
index d414d90..719b66a 100644 (file)
@@ -283,6 +283,10 @@ class MysqlUpdater extends DatabaseUpdater {
                        [ 'addIndex', 'categorylinks', 'cl_collation_ext',
                                'patch-add-cl_collation_ext_index.sql' ],
                        [ 'doCollationUpdate' ],
+
+                       // 1.28
+                       [ 'addIndex', 'recentchanges', 'rc_name_type_patrolled_timestamp',
+                               'patch-add-rc_name_type_patrolled_timestamp_index.sql' ],
                ];
        }
 
index 334256b..8075aac 100644 (file)
@@ -113,6 +113,10 @@ class OracleUpdater extends DatabaseUpdater {
                        [ 'dropTable', 'msg_resource' ],
                        [ 'addField', 'watchlist', 'wl_id', 'patch-watchlist-wl_id.sql' ],
 
+                       // 1.28
+                       [ 'addIndex', 'recentchanges', 'rc_name_type_patrolled_timestamp',
+                               'patch-add-rc_name_type_patrolled_timestamp_index.sql' ],
+
                        // KEEP THIS AT THE BOTTOM!!
                        [ 'doRebuildDuplicateFunction' ],
 
index a3b50ac..be94d91 100644 (file)
@@ -433,6 +433,10 @@ class PostgresUpdater extends DatabaseUpdater {
                                'addPgField', 'watchlist', 'wl_id',
                                "INTEGER NOT NULL PRIMARY KEY DEFAULT nextval('watchlist_wl_id_seq')"
                        ],
+
+                       // 1.28
+                       [ 'addPgIndex', 'recentchanges', 'rc_name_type_patrolled_timestamp',
+                               '( rc_namespace, rc_type, rc_patrolled, rc_timestamp )' ],
                ];
        }
 
index 86dccd7..1c6e6eb 100644 (file)
@@ -152,6 +152,10 @@ class SqliteUpdater extends DatabaseUpdater {
                        [ 'addIndex', 'categorylinks', 'cl_collation_ext',
                                'patch-add-cl_collation_ext_index.sql' ],
                        [ 'doCollationUpdate' ],
+
+                       // 1.28
+                       [ 'addIndex', 'recentchanges', 'rc_name_type_patrolled_timestamp',
+                               'patch-add-rc_name_type_patrolled_timestamp_index.sql' ],
                ];
        }
 
index 16740d8..ed361eb 100644 (file)
@@ -84,11 +84,9 @@ class DjVuImage {
        function dump() {
                $file = fopen( $this->mFilename, 'rb' );
                $header = fread( $file, 12 );
-               // @todo FIXME: Would be good to replace this extract() call with
-               // something that explicitly initializes local variables.
-               extract( unpack( 'a4magic/a4chunk/NchunkLength', $header ) );
-               /** @var string $chunk
-                * @var string $chunkLength */
+               $arr = unpack( 'a4magic/a4chunk/NchunkLength', $header );
+               $chunk = $arr['chunk'];
+               $chunkLength = $arr['chunkLength'];
                echo "$chunk $chunkLength\n";
                $this->dumpForm( $file, $chunkLength, 1 );
                fclose( $file );
@@ -103,11 +101,9 @@ class DjVuImage {
                        if ( $chunkHeader == '' ) {
                                break;
                        }
-                       // @todo FIXME: Would be good to replace this extract() call with
-                       // something that explicitly initializes local variables.
-                       extract( unpack( 'a4chunk/NchunkLength', $chunkHeader ) );
-                       /** @var string $chunk
-                        * @var string $chunkLength */
+                       $arr = unpack( 'a4chunk/NchunkLength', $chunkHeader );
+                       $chunk = $arr['chunk'];
+                       $chunkLength = $arr['chunkLength'];
                        echo str_repeat( ' ', $indent * 4 ) . "$chunk $chunkLength\n";
 
                        if ( $chunk == 'FORM' ) {
@@ -138,24 +134,19 @@ class DjVuImage {
                if ( strlen( $header ) < 16 ) {
                        wfDebug( __METHOD__ . ": too short file header\n" );
                } else {
-                       // @todo FIXME: Would be good to replace this extract() call with
-                       // something that explicitly initializes local variables.
-                       extract( unpack( 'a4magic/a4form/NformLength/a4subtype', $header ) );
-
-                       /** @var string $magic
-                        * @var string $subtype
-                        * @var string $formLength
-                        * @var string $formType */
-                       if ( $magic != 'AT&T' ) {
+                       $arr = unpack( 'a4magic/a4form/NformLength/a4subtype', $header );
+
+                       $subtype = $arr['subtype'];
+                       if ( $arr['magic'] != 'AT&T' ) {
                                wfDebug( __METHOD__ . ": not a DjVu file\n" );
                        } elseif ( $subtype == 'DJVU' ) {
                                // Single-page document
                                $info = $this->getPageInfo( $file );
                        } elseif ( $subtype == 'DJVM' ) {
                                // Multi-page document
-                               $info = $this->getMultiPageInfo( $file, $formLength );
+                               $info = $this->getMultiPageInfo( $file, $arr['formLength'] );
                        } else {
-                               wfDebug( __METHOD__ . ": unrecognized DJVU file type '$formType'\n" );
+                               wfDebug( __METHOD__ . ": unrecognized DJVU file type '{$arr['subtype']}'\n" );
                        }
                }
                fclose( $file );
@@ -168,13 +159,9 @@ class DjVuImage {
                if ( strlen( $header ) < 8 ) {
                        return [ false, 0 ];
                } else {
-                       // @todo FIXME: Would be good to replace this extract() call with
-                       // something that explicitly initializes local variables.
-                       extract( unpack( 'a4chunk/Nlength', $header ) );
+                       $arr = unpack( 'a4chunk/Nlength', $header );
 
-                       /** @var string $chunk
-                        * @var string $length */
-                       return [ $chunk, $length ];
+                       return [ $arr['chunk'], $arr['length'] ];
                }
        }
 
@@ -236,31 +223,22 @@ class DjVuImage {
                        return false;
                }
 
-               // @todo FIXME: Would be good to replace this extract() call with
-               // something that explicitly initializes local variables.
-               extract( unpack(
+               $arr = unpack(
                        'nwidth/' .
                        'nheight/' .
                        'Cminor/' .
                        'Cmajor/' .
                        'vresolution/' .
-                       'Cgamma', $data ) );
+                       'Cgamma', $data );
 
                # Newer files have rotation info in byte 10, but we don't use it yet.
 
-               /** @var string $width
-                * @var string $height
-                * @var string $major
-                * @var string $minor
-                * @var string $resolution
-                * @var string $length
-                * @var string $gamma */
                return [
-                       'width' => $width,
-                       'height' => $height,
-                       'version' => "$major.$minor",
-                       'resolution' => $resolution,
-                       'gamma' => $gamma / 10.0 ];
+                       'width' => $arr['width'],
+                       'height' => $arr['height'],
+                       'version' => "{$arr['major']}.{$arr['minor']}",
+                       'resolution' => $arr['resolution'],
+                       'gamma' => $arr['gamma'] / 10.0 ];
        }
 
        /**
index f9eea48..206ad00 100644 (file)
@@ -251,7 +251,7 @@ class Parser {
        protected $mProfiler;
 
        /**
-        * @var LinkRenderer
+        * @var \MediaWiki\Linker\LinkRenderer
         */
        protected $mLinkRenderer;
 
@@ -882,10 +882,10 @@ class Parser {
        }
 
        /**
-        * Get a LinkRenderer instance to make links with
+        * Get a \MediaWiki\Linker\LinkRenderer instance to make links with
         *
         * @since 1.28
-        * @return LinkRenderer
+        * @return \MediaWiki\Linker\LinkRenderer
         */
        public function getLinkRenderer() {
                if ( !$this->mLinkRenderer ) {
index 35ecc6e..99b9f33 100644 (file)
@@ -61,7 +61,7 @@ class SpecialPage {
        protected $mContext;
 
        /**
-        * @var LinkRenderer|null
+        * @var \MediaWiki\Linker\LinkRenderer|null
         */
        private $linkRenderer;
 
@@ -834,7 +834,7 @@ class SpecialPage {
 
        /**
         * @since 1.28
-        * @return LinkRenderer
+        * @return \MediaWiki\Linker\LinkRenderer
         */
        protected function getLinkRenderer() {
                if ( $this->linkRenderer ) {
@@ -846,7 +846,7 @@ class SpecialPage {
 
        /**
         * @since 1.28
-        * @param LinkRenderer $linkRenderer
+        * @param \MediaWiki\Linker\LinkRenderer $linkRenderer
         */
        public function setLinkRenderer( LinkRenderer $linkRenderer ) {
                $this->linkRenderer = $linkRenderer;
index bfb52e8..6ca1100 100644 (file)
@@ -339,7 +339,13 @@ class SpecialUpload extends SpecialPage {
         * @param string $message HTML message to be passed to mainUploadForm
         */
        protected function showRecoverableUploadError( $message ) {
-               $sessionKey = $this->mUpload->stashFile()->getFileKey();
+               $stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
+               if ( $stashStatus->isGood() ) {
+                       $sessionKey = $stashStatus->getValue()->getFileKey();
+               } else {
+                       $sessionKey = null;
+                       // TODO Add a warning message about the failure to stash here?
+               }
                $message = '<h2>' . $this->msg( 'uploaderror' )->escaped() . "</h2>\n" .
                        '<div class="error">' . $message . "</div>\n";
 
@@ -368,7 +374,13 @@ class SpecialUpload extends SpecialPage {
                        return false;
                }
 
-               $sessionKey = $this->mUpload->stashFile()->getFileKey();
+               $stashStatus = $this->mUpload->tryStashFile( $this->getUser() );
+               if ( $stashStatus->isGood() ) {
+                       $sessionKey = $stashStatus->getValue()->getFileKey();
+               } else {
+                       $sessionKey = null;
+                       // TODO Add a warning message about the failure to stash here?
+               }
 
                // Add styles for the warning, reused from the live preview
                $this->getOutput()->addModuleStyles( 'mediawiki.special.upload.styles' );
index b620132..03c864a 100644 (file)
@@ -944,6 +944,33 @@ abstract class UploadBase {
                return $this->mLocalFile;
        }
 
+       /**
+        * Like stashFile(), but respects extensions' wishes to prevent the stashing.
+        *
+        * Upload stash exceptions are also caught and converted to an error status.
+        *
+        * @since 1.28
+        * @param User $user
+        * @return Status If successful, value is an UploadStashFile instance
+        */
+       public function tryStashFile( User $user ) {
+               $props = $this->mFileProps;
+               $error = null;
+               Hooks::run( 'UploadStashFile', [ $this, $user, $props, &$error ] );
+               if ( $error ) {
+                       if ( !is_array( $error ) ) {
+                               $error = [ $error ];
+                       }
+                       return call_user_func_array( 'Status::newFatal', $error );
+               }
+               try {
+                       $file = $this->doStashFile( $user );
+                       return Status::newGood( $file );
+               } catch ( UploadStashException $e ) {
+                       return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
+               }
+       }
+
        /**
         * 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
@@ -956,6 +983,7 @@ abstract class UploadBase {
         * which can be passed through a form or API request to find this stashed
         * file again.
         *
+        * @deprecated since 1.28 Use tryStashFile() instead
         * @param User $user
         * @return UploadStashFile Stashed file
         * @throws UploadStashBadPathException
@@ -963,6 +991,16 @@ abstract class UploadBase {
         * @throws UploadStashNotLoggedInException
         */
        public function stashFile( User $user = null ) {
+               return $this->doStashFile( $user );
+       }
+
+       /**
+        * Implementation for stashFile() and tryStashFile().
+        *
+        * @param User $user
+        * @return UploadStashFile Stashed file
+        */
+       protected function doStashFile( User $user = null ) {
                $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $user );
                $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
                $this->mLocalFile = $file;
@@ -979,7 +1017,7 @@ abstract class UploadBase {
         */
        public function stashFileGetKey() {
                wfDeprecated( __METHOD__, '1.28' );
-               return $this->stashFile()->getFileKey();
+               return $this->doStashFile()->getFileKey();
        }
 
        /**
@@ -990,7 +1028,7 @@ abstract class UploadBase {
         */
        public function stashSession() {
                wfDeprecated( __METHOD__, '1.28' );
-               return $this->stashFile()->getFileKey();
+               return $this->doStashFile()->getFileKey();
        }
 
        /**
index 0323b68..cc1d698 100644 (file)
@@ -65,19 +65,19 @@ class UploadFromChunks extends UploadFromFile {
        }
 
        /**
-        * Calls the parent stashFile and updates the uploadsession table to handle "chunks"
+        * Calls the parent doStashFile and updates the uploadsession table to handle "chunks"
         *
         * @param User|null $user
         * @return UploadStashFile Stashed file
         */
-       public function stashFile( User $user = null ) {
+       protected function doStashFile( User $user = null ) {
                // Stash file is the called on creating a new chunk session:
                $this->mChunkIndex = 0;
                $this->mOffset = 0;
 
                $this->verifyChunk();
                // Create a local stash target
-               $this->mLocalFile = parent::stashFile( $user );
+               $this->mLocalFile = parent::doStashFile( $user );
                // Update the initial file offset (based on file size)
                $this->mOffset = $this->mLocalFile->getSize();
                $this->mFileKey = $this->mLocalFile->getFileKey();
@@ -162,7 +162,7 @@ class UploadFromChunks extends UploadFromFile {
                // Update the mTempPath and mLocalFile
                // (for FileUpload or normal Stash to take over)
                $tStart = microtime( true );
-               $this->mLocalFile = parent::stashFile( $this->user );
+               $this->mLocalFile = parent::doStashFile( $this->user );
                $tAmount = microtime( true ) - $tStart;
                $this->mLocalFile->setLocalReference( $tmpFile ); // reuse (e.g. for getImageInfo())
                wfDebugLog( 'fileconcatenate', "Stashed combined file ($i chunks) in $tAmount seconds." );
index 908c8aa..50bcbc4 100644 (file)
@@ -153,10 +153,10 @@ class UploadFromStash extends UploadBase {
         * @param User $user
         * @return UploadStashFile
         */
-       public function stashFile( User $user = null ) {
+       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::stashFile( $user );
+               $this->mLocalFile = parent::doStashFile( $user );
 
                return $this->mLocalFile;
        }
index 839e081..b55cb76 100644 (file)
        "uploadstash-errclear": "Clearing the files failed.",
        "uploadstash-refresh": "Refresh the list of files",
        "uploadstash-thumbnail": "view thumbnail",
+       "uploadstash-exception": "Could not store upload in the stash ($1): \"$2\".",
        "invalid-chunk-offset": "Invalid chunk offset",
        "img-auth-accessdenied": "Access denied",
        "img-auth-nopathinfo": "Missing PATH_INFO.\nYour server is not set up to pass this information.\nIt may be CGI-based and cannot support img_auth.\nSee https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Image_Authorization.",
index 77d863d..953ebbb 100644 (file)
        "uploadstash-errclear": "Used as error message in [[Special:UploadStash]].",
        "uploadstash-refresh": "Used as link text in [[Special:UploadStash]].",
        "uploadstash-thumbnail": "Used as link text in [[Special:UploadStash]].",
+       "uploadstash-exception": "Error message shown when an action related to the upload stash fails unexpectedly.\n\nParameters:\n* $1 - exception name, e.g. 'UploadStashFileNotFoundException'\n* $2 - exceptions details (always in English), e.g. 'cannot find path, or not a plain file'",
        "invalid-chunk-offset": "Error that can happen if chunks get uploaded out of order.\nAs a result of this error, clients can continue from an offset provided or restart the upload.\nUsed on [[Special:UploadWizard]].",
        "img-auth-accessdenied": "[[mw:Manual:Image Authorization|Manual:Image Authorization]]: Access Denied\n{{Identical|Access denied}}",
        "img-auth-nopathinfo": "[[mw:Manual:Image Authorization|Manual:Image Authorization]]: Missing PATH_INFO - see english description\n{{Doc-important|This is plain text. Do not use any wiki syntax.}}",
diff --git a/maintenance/archives/patch-add-rc_name_type_patrolled_timestamp_index.sql b/maintenance/archives/patch-add-rc_name_type_patrolled_timestamp_index.sql
new file mode 100644 (file)
index 0000000..aa54e75
--- /dev/null
@@ -0,0 +1,2 @@
+-- @since 1.28
+CREATE INDEX /*i*/rc_name_type_patrolled_timestamp ON /*_*/recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
index c219b9b..50a4018 100644 (file)
@@ -32,8 +32,7 @@ class InitEditCount extends Maintenance {
                avoids locking tables or lagging slaves with large updates;
                calculates counts on a slave if possible.
 
-Background mode will be automatically used if the server is MySQL 4.0
-(which does not support subqueries) or if multiple servers are listed
+Background mode will be automatically used if multiple servers are listed
 in the load balancer, usually indicating a replication environment.' );
                $this->addDescription( 'Batch-recalculate user_editcount fields from the revision table' );
        }
@@ -46,13 +45,12 @@ in the load balancer, usually indicating a replication environment.' );
                $dbver = $dbw->getServerVersion();
 
                // Autodetect mode...
-               $backgroundMode = wfGetLB()->getServerCount() > 1 ||
-                       ( $dbw instanceof DatabaseMysql );
-
                if ( $this->hasOption( 'background' ) ) {
                        $backgroundMode = true;
                } elseif ( $this->hasOption( 'quick' ) ) {
                        $backgroundMode = false;
+               } else {
+                       $backgroundMode = wfGetLB()->getServerCount() > 1;
                }
 
                if ( $backgroundMode ) {
@@ -96,7 +94,6 @@ in the load balancer, usually indicating a replication environment.' );
                                wfWaitForSlaves();
                        }
                } else {
-                       // Subselect should work on modern MySQLs etc
                        $this->output( "Using single-query mode...\n" );
                        $sql = "UPDATE $user SET user_editcount=(SELECT COUNT(*) FROM $revision WHERE rev_user=user_id)";
                        $dbw->query( $sql );
index 48b2250..ea087a6 100644 (file)
@@ -861,6 +861,7 @@ CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,
 CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip);
 CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text);
 CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp);
+CREATE INDEX /*i*/rc_name_type_patrolled_timestamp ON /*_*/recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
 
 
 CREATE TABLE /*_*/watchlist (
diff --git a/maintenance/oracle/archives/patch-add-rc_name_type_patrolled_timestamp_index.sql b/maintenance/oracle/archives/patch-add-rc_name_type_patrolled_timestamp_index.sql
new file mode 100644 (file)
index 0000000..cd0d396
--- /dev/null
@@ -0,0 +1,4 @@
+define mw_prefix='{$wgDBprefix}';
+
+CREATE INDEX &mw_prefix.recentchanges_i08 ON &mw_prefix.recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
+
index 27c3030..d9369c9 100644 (file)
@@ -434,6 +434,7 @@ CREATE INDEX &mw_prefix.recentchanges_i04 ON &mw_prefix.recentchanges (rc_new,rc
 CREATE INDEX &mw_prefix.recentchanges_i05 ON &mw_prefix.recentchanges (rc_ip);
 CREATE INDEX &mw_prefix.recentchanges_i06 ON &mw_prefix.recentchanges (rc_namespace, rc_user_text);
 CREATE INDEX &mw_prefix.recentchanges_i07 ON &mw_prefix.recentchanges (rc_user_text, rc_timestamp);
+CREATE INDEX &mw_prefix.recentchanges_i08 ON &mw_prefix.recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
 
 CREATE TABLE &mw_prefix.watchlist (
   wl_id                     NUMBER     NOT NULL,
index 61cb198..95c87c0 100644 (file)
@@ -446,6 +446,7 @@ CREATE INDEX rc_namespace_title ON recentchanges (rc_namespace, rc_title);
 CREATE INDEX rc_cur_id          ON recentchanges (rc_cur_id);
 CREATE INDEX new_name_timestamp ON recentchanges (rc_new, rc_namespace, rc_timestamp);
 CREATE INDEX rc_ip              ON recentchanges (rc_ip);
+CREATE INDEX rc_name_type_patrolled_timestamp ON recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
 
 
 CREATE SEQUENCE watchlist_wl_id_seq;
index 5b035e1..70a26cb 100644 (file)
@@ -164,8 +164,12 @@ class PurgeChangedFiles extends Maintenance {
                                        continue;
                                }
 
-                               // Purge current version and any versions in oldimage table
+                               // Purge current version and its thumbnails
                                $file->purgeCache();
+                               // Purge the old versions and their thumbnails
+                               foreach ( $file->getHistory() as $oldFile ) {
+                                       $oldFile->purgeCache();
+                               }
 
                                if ( $logType === 'delete' ) {
                                        // If there is an orphaned storage file... delete it
index 0ea52ca..94335cf 100644 (file)
@@ -57,10 +57,8 @@ class FixBug20757 extends Maintenance {
 
                $totalRevs = $dbr->selectField( 'text', 'MAX(old_id)', false, __METHOD__ );
 
-               if ( $dbr->getType() == 'mysql' ) {
-                       // In MySQL 4.1+, the binary field old_text has a non-working LOWER() function
-                       $lowerLeft = 'LOWER(CONVERT(LEFT(old_text,22) USING latin1))';
-               }
+               // In MySQL 4.1+, the binary field old_text has a non-working LOWER() function
+               $lowerLeft = 'LOWER(CONVERT(LEFT(old_text,22) USING latin1))';
 
                while ( true ) {
                        print "ID: $startId / $totalRevs\r";
index 9c9bdfb..40506bf 100644 (file)
@@ -1136,6 +1136,7 @@ CREATE INDEX /*i*/new_name_timestamp ON /*_*/recentchanges (rc_new,rc_namespace,
 CREATE INDEX /*i*/rc_ip ON /*_*/recentchanges (rc_ip);
 CREATE INDEX /*i*/rc_ns_usertext ON /*_*/recentchanges (rc_namespace, rc_user_text);
 CREATE INDEX /*i*/rc_user_text ON /*_*/recentchanges (rc_user_text, rc_timestamp);
+CREATE INDEX /*i*/rc_name_type_patrolled_timestamp ON /*_*/recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
 
 
 CREATE TABLE /*_*/watchlist (
index 07c7d76..6c63957 100644 (file)
@@ -9,7 +9,11 @@
        $( function () {
                var idleTimeout = 3000,
                        api = new mw.Api(),
-                       pending = null,
+                       timer,
+                       pending,
+                       lastText,
+                       lastSummary,
+                       lastTextHash,
                        $form = $( '#editform' ),
                        $text = $form.find( '#wpTextbox1' ),
                        $summary = $form.find( '#wpSummary' ),
                        model = $form.find( '[name=model]' ).val(),
                        format = $form.find( '[name=format]' ).val(),
                        revId = $form.find( '[name=parentRevId]' ).val(),
-                       lastText = $text.textSelection( 'getContents' ),
-                       timer = null;
+                       lastPriority = 0,
+                       PRIORITY_LOW = 1,
+                       PRIORITY_HIGH = 2;
 
                // Send a request to stash the edit to the API.
                // If a request is in progress, abort it since its payload is stale and the API
                // may limit concurrent stash parses.
                function stashEdit() {
-                       if ( pending ) {
-                               pending.abort();
-                       }
-
                        api.getToken( 'csrf' ).then( function ( token ) {
-                               lastText = $text.textSelection( 'getContents' );
+                               var req, params,
+                                       textChanged = isTextChanged(),
+                                       priority = textChanged ? PRIORITY_HIGH : PRIORITY_LOW;
+
+                               if ( pending ) {
+                                       if ( lastPriority > priority ) {
+                                               // Stash request for summary change should wait on pending text change stash
+                                               pending.then( checkStash );
+                                               return;
+                                       }
+                                       pending.abort();
+                               }
 
-                               pending = api.post( {
+                               // Update the "last" tracking variables
+                               lastSummary = $summary.textSelection( 'getContents' );
+                               lastPriority = priority;
+                               if ( textChanged ) {
+                                       lastText = $text.textSelection( 'getContents' );
+                                       // Reset hash
+                                       lastTextHash = null;
+                               }
+
+                               params = {
                                        action: 'stashedit',
                                        token: token,
                                        title: mw.config.get( 'wgPageName' ),
                                        section: section,
                                        sectiontitle: '',
-                                       text: lastText,
-                                       summary: $summary.textSelection( 'getContents' ),
+                                       summary: lastSummary,
                                        contentmodel: model,
                                        contentformat: format,
                                        baserevid: revId
+                               };
+                               if ( lastTextHash ) {
+                                       params.stashedtexthash = lastTextHash;
+                               } else {
+                                       params.text = lastText;
+                               }
+
+                               req = api.post( params );
+                               pending = req;
+                               req.then( function ( data ) {
+                                       if ( req === pending ) {
+                                               pending = null;
+                                       }
+                                       if ( data.stashedit && data.stashedit.texthash ) {
+                                               lastTextHash = data.stashedit.texthash;
+                                       } else {
+                                               // Request failed or text hash expired;
+                                               // include the text in a future stash request.
+                                               lastTextHash = null;
+                                       }
                                } );
                        } );
                }
 
-               // Check if edit body text changed since the last stashEdit() call or if no edit
-               // stash calls have yet been made
-               function isChanged() {
-                       var newText = $text.textSelection( 'getContents' );
-                       return newText !== lastText;
+               // Whether the body text content changed since the last stashEdit()
+               function isTextChanged() {
+                       return lastText !== $text.textSelection( 'getContents' );
+               }
+
+               // Whether the edit summary has changed since the last stashEdit()
+               function isSummaryChanged() {
+                       return lastSummary !== $summary.textSelection( 'getContents' );
                }
 
-               function onEditorIdle() {
-                       if ( !isChanged() ) {
+               // Check whether text or summary have changed and call stashEdit()
+               function checkStash() {
+                       if ( !isTextChanged() && !isSummaryChanged() ) {
                                return;
                        }
 
                        stashEdit();
                }
 
-               function onTextKeyUp( e ) {
+               function onKeyUp( e ) {
                        // Ignore keystrokes that don't modify text, like cursor movements.
                        // See <http://www.javascripter.net/faq/keycodes.htm> and
                        // <http://www.quirksmode.org/js/keys.html>. We don't have to be exhaustive,
                        }
 
                        clearTimeout( timer );
-                       timer = setTimeout( onEditorIdle, idleTimeout );
+                       timer = setTimeout( checkStash, idleTimeout );
+               }
+
+               function onSummaryFocus() {
+                       // Summary typing is usually near the end of the workflow and involves less pausing.
+                       // Re-stash more frequently in hopes of capturing the final summary before submission.
+                       idleTimeout = 1000;
+                       // Stash now since the text is likely the final version. The re-stashes based on the
+                       // summary are targeted at caching edit checks that need the final summary.
+                       checkStash();
+               }
+
+               function onTextFocus() {
+                       // User returned to the text field... reset stash rate to default
+                       idleTimeout = 3000;
                }
 
                function onFormLoaded() {
                                // probably save the page soon
                                || $.inArray( $form.find( '#mw-edit-mode' ).val(), [ 'preview', 'diff' ] ) > -1
                        ) {
-                               stashEdit();
+                               checkStash();
                        }
                }
 
-               // We don't attempt to stash new section edits because in such cases
-               // the parser output varies on the edit summary (since it determines
-               // the new section's name).
+               // We don't attempt to stash new section edits because in such cases the parser output
+               // varies on the edit summary (since it determines the new section's name).
                if ( $form.find( 'input[name=wpSection]' ).val() === 'new' ) {
                        return;
                }
 
-               $text.on( { change: onEditorIdle, keyup: onTextKeyUp } );
-               $summary.on( { focus: onEditorIdle } );
+               $text.on( {
+                       change: checkStash,
+                       keyup: onKeyUp,
+                       focus: onTextFocus
+               } );
+               $summary.on( {
+                       focus: onSummaryFocus,
+                       focusout: checkStash,
+                       keyup: onKeyUp
+               } );
                onFormLoaded();
-
        } );
 }( mediaWiki, jQuery ) );