&$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
$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;
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 );
*/
/**
- * 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
*/
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();
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 ]
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
$this->dieUsage( 'This interface is not supported for bots', 'botsnotsupported' );
}
+ $cache = ObjectCache::getLocalClusterInstance();
$page = $this->getTitleOrPageId( $params );
$title = $page->getTitle();
$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'] );
// 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
+ ]
+ );
}
/**
$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,
$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 );
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
*
* - 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() )
+ );
}
/**
*
* 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
],
'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',
$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
$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 );
*/
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;
}
$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;
}
}
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'];
}
/**
- * 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;
+ }
}
/**
* @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 );
/**
* 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' ];
}
}
"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.",
"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}}",
* @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;
/**
* 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' );
}
/**
foreach ( $callersByDB as $db => $callers ) {
$msg .= "$db: " . implode( '; ', $callers ) . "\n";
}
- $this->logger->info( $msg );
+ $this->trxLogger->info( $msg );
}
}
'servers' => $this->makeServerArray( $template, $loads, $groupLoads ),
'loadMonitor' => $this->loadMonitorClass,
'readOnlyReason' => $readOnlyReason,
- 'trxProfiler' => $this->trxProfiler
+ 'trxProfiler' => $this->trxProfiler,
+ 'srvCache' => $this->srvCache,
+ 'wanCache' => $this->wanCache
] );
}
] ];
}
- return new LoadBalancer( [
- 'servers' => $servers,
- 'loadMonitor' => $this->loadMonitorClass,
- 'readOnlyReason' => $this->readOnlyReason,
- 'trxProfiler' => $this->trxProfiler
- ] );
+ return $this->newLoadBalancer( $servers );
}
/**
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] );
}
/**
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,
$this->lb = new LoadBalancerSingle( [
'readOnlyReason' => $this->readOnlyReason,
- 'trxProfiler' => $this->trxProfiler
+ 'trxProfiler' => $this->trxProfiler,
+ 'srvCache' => $this->srvCache,
+ 'wanCache' => $this->wanCache
] + $conf );
}
'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'] ) ) {
* - 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 ) {
}
}
- // 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 {
[ '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' ],
];
}
[ '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' ],
];
}
[ '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' ],
'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 )' ],
];
}
[ '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' ],
];
}
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 );
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' ) {
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 );
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'] ];
}
}
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 ];
}
/**
protected $mProfiler;
/**
- * @var LinkRenderer
+ * @var \MediaWiki\Linker\LinkRenderer
*/
protected $mLinkRenderer;
}
/**
- * 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 ) {
protected $mContext;
/**
- * @var LinkRenderer|null
+ * @var \MediaWiki\Linker\LinkRenderer|null
*/
private $linkRenderer;
/**
* @since 1.28
- * @return LinkRenderer
+ * @return \MediaWiki\Linker\LinkRenderer
*/
protected function getLinkRenderer() {
if ( $this->linkRenderer ) {
/**
* @since 1.28
- * @param LinkRenderer $linkRenderer
+ * @param \MediaWiki\Linker\LinkRenderer $linkRenderer
*/
public function setLinkRenderer( LinkRenderer $linkRenderer ) {
$this->linkRenderer = $linkRenderer;
* @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";
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' );
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
* 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
* @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;
*/
public function stashFileGetKey() {
wfDeprecated( __METHOD__, '1.28' );
- return $this->stashFile()->getFileKey();
+ return $this->doStashFile()->getFileKey();
}
/**
*/
public function stashSession() {
wfDeprecated( __METHOD__, '1.28' );
- return $this->stashFile()->getFileKey();
+ return $this->doStashFile()->getFileKey();
}
/**
}
/**
- * 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();
// 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." );
* @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;
}
"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.",
"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.}}",
--- /dev/null
+-- @since 1.28
+CREATE INDEX /*i*/rc_name_type_patrolled_timestamp ON /*_*/recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
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' );
}
$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 ) {
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 );
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 (
--- /dev/null
+define mw_prefix='{$wgDBprefix}';
+
+CREATE INDEX &mw_prefix.recentchanges_i08 ON &mw_prefix.recentchanges (rc_namespace, rc_type, rc_patrolled, rc_timestamp);
+
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,
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;
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
$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";
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 (
$( 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 ) );