in JavaScript, use mw.log.deprecate() instead.
* The 'user.groups' module, deprecated in 1.28, was removed.
Use the 'user' module instead.
+* The ability to override User::$mRights has been removed.
+* Previously, when iterating ResultWrapper with foreach() or a similar
+ construct, the range of the index was 1..numRows. This has been fixed to be
+ 0..(numRows-1).
* …
=== Deprecations in 1.34 ===
template option 'searchaction' instead.
* LoadBalancer::haveIndex() and LoadBalancer::isNonZeroLoad() have
been deprecated.
+* User::getRights() and User::$mRights have been deprecated. Use
+ PermissionManager::getUserPermissions() instead.
=== Other changes in 1.34 ===
* …
* See DataUpdate::getCauseAction(). (default 'unknown')
* - causeAgent: name of the user who caused the update. See DataUpdate::getCauseAgent().
* (string, default 'unknown')
+ * - known-revision-output: a combined canonical ParserOutput for the revision, perhaps
+ * from some cache. The caller is responsible for ensuring that the ParserOutput indeed
+ * matched the $rev and $options. This mechanism is intended as a temporary stop-gap,
+ * for the time until caches have been changed to store RenderedRevision states instead
+ * of ParserOutput objects. (default: null) (since 1.33)
*/
public function prepareUpdate( RevisionRecord $revision, array $options = [] ) {
Assert::parameter(
if ( $this->renderedRevision ) {
$this->renderedRevision->updateRevision( $revision );
} else {
-
// NOTE: we want a canonical rendering, so don't pass $this->user or ParserOptions
// NOTE: the revision is either new or current, so we can bypass audience checks.
$this->renderedRevision = $this->revisionRenderer->getRenderedRevision(
$this->revision,
null,
null,
- [ 'use-master' => $this->useMaster(), 'audience' => RevisionRecord::RAW ]
+ [
+ 'use-master' => $this->useMaster(),
+ 'audience' => RevisionRecord::RAW,
+ 'known-revision-output' => $options['known-revision-output'] ?? null
+ ]
);
// XXX: Since we presumably are dealing with the current revision,
->getPermissionErrors( $action, $user, $this, $rigor, $ignoreErrors );
}
- /**
- * Add the resulting error code to the errors array
- *
- * @param array $errors List of current errors
- * @param array|string|MessageSpecifier|false $result Result of errors
- *
- * @return array List of errors
- */
- private function resultToError( $errors, $result ) {
- if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
- // A single array representing an error
- $errors[] = $result;
- } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
- // A nested array representing multiple errors
- $errors = array_merge( $errors, $result );
- } elseif ( $result !== '' && is_string( $result ) ) {
- // A string representing a message-id
- $errors[] = [ $result ];
- } elseif ( $result instanceof MessageSpecifier ) {
- // A message specifier representing an error
- $errors[] = [ $result ];
- } elseif ( $result === false ) {
- // a generic "We don't want them to do that"
- $errors[] = [ 'badaccess-group0' ];
- }
- return $errors;
- }
-
/**
* Get a filtered list of all restriction types supported by this wiki.
* @param bool $exists True to get all restriction types that apply to
$this->mHasSubpages = false;
$subpages = $this->getSubpages( 1 );
if ( $subpages instanceof TitleArray ) {
- $this->mHasSubpages = (bool)$subpages->count();
+ $this->mHasSubpages = (bool)$subpages->current();
}
}
*/
function formatRow( $row ) {
if ( $this->lastRow ) {
- $latest = ( $this->counter == 1 && $this->mIsFirst );
$firstInList = $this->counter == 1;
$this->counter++;
? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
: false;
- $s = $this->historyLine(
- $this->lastRow, $row, $notifTimestamp, $latest, $firstInList );
+ $s = $this->historyLine( $this->lastRow, $row, $notifTimestamp, false, $firstInList );
} else {
$s = '';
}
$s .= Html::hidden( 'type', 'revision' ) . "\n";
// Button container stored in $this->buttons for re-use in getEndBody()
- $this->buttons = Html::openElement( 'div', [ 'class' => 'mw-history-compareselectedversions' ] );
- $className = 'historysubmit mw-history-compareselectedversions-button';
- $attrs = [ 'class' => $className ]
- + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
- $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
- $attrs
- ) . "\n";
-
- $user = $this->getUser();
- $actionButtons = '';
- if ( $user->isAllowed( 'deleterevision' ) ) {
- $actionButtons .= $this->getRevisionButton( 'revisiondelete', 'showhideselectedversions' );
- }
- if ( $this->showTagEditUI ) {
- $actionButtons .= $this->getRevisionButton( 'editchangetags', 'history-edit-tags' );
- }
- if ( $actionButtons ) {
- $this->buttons .= Xml::tags( 'div', [ 'class' =>
- 'mw-history-revisionactions' ], $actionButtons );
- }
+ $this->buttons = '';
+ if ( $this->getNumRows() > 0 ) {
+ $this->buttons .= Html::openElement(
+ 'div', [ 'class' => 'mw-history-compareselectedversions' ] );
+ $className = 'historysubmit mw-history-compareselectedversions-button';
+ $attrs = [ 'class' => $className ]
+ + Linker::tooltipAndAccesskeyAttribs( 'compareselectedversions' );
+ $this->buttons .= $this->submitButton( $this->msg( 'compareselectedversions' )->text(),
+ $attrs
+ ) . "\n";
+
+ $user = $this->getUser();
+ $actionButtons = '';
+ if ( $user->isAllowed( 'deleterevision' ) ) {
+ $actionButtons .= $this->getRevisionButton(
+ 'revisiondelete', 'showhideselectedversions' );
+ }
+ if ( $this->showTagEditUI ) {
+ $actionButtons .= $this->getRevisionButton(
+ 'editchangetags', 'history-edit-tags' );
+ }
+ if ( $actionButtons ) {
+ $this->buttons .= Xml::tags( 'div', [ 'class' =>
+ 'mw-history-revisionactions' ], $actionButtons );
+ }
- if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
- $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
- }
+ if ( $user->isAllowed( 'deleterevision' ) || $this->showTagEditUI ) {
+ $this->buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
+ }
- $this->buttons .= '</div>';
+ $this->buttons .= '</div>';
- $s .= $this->buttons;
+ $s .= $this->buttons;
+ }
$s .= '<ul id="pagehistory">' . "\n";
return $s;
protected function getEndBody() {
if ( $this->lastRow ) {
- $latest = $this->counter == 1 && $this->mIsFirst;
$firstInList = $this->counter == 1;
if ( $this->mIsBackwards ) {
# Next row is unknown, but for UI reasons, probably exists if an offset has been specified
? $this->getTitle()->getNotificationTimestamp( $this->getUser() )
: false;
- $s = $this->historyLine(
- $this->lastRow, $next, $notifTimestamp, $latest, $firstInList );
+ $s = $this->historyLine( $this->lastRow, $next, $notifTimestamp, false, $firstInList );
} else {
$s = '';
}
* @param mixed $next The database row corresponding to the next line
* (chronologically previous)
* @param bool|string $notificationtimestamp
- * @param bool $latest Whether this row corresponds to the page's latest revision.
+ * @param bool $dummy Unused.
* @param bool $firstInList Whether this row corresponds to the first
* displayed on this history page.
* @return string HTML output for the row
*/
function historyLine( $row, $next, $notificationtimestamp = false,
- $latest = false, $firstInList = false ) {
+ $dummy = false, $firstInList = false ) {
$rev = new Revision( $row, 0, $this->getTitle() );
if ( is_object( $next ) ) {
$prevRev = null;
}
- $curlink = $this->curLink( $rev, $latest );
+ $latest = $rev->getId() === $this->getWikiPage()->getLatest();
+ $curlink = $this->curLink( $rev );
$lastlink = $this->lastLink( $rev, $next );
$curLastlinks = Html::rawElement( 'span', [], $curlink ) .
Html::rawElement( 'span', [], $lastlink );
* Create a diff-to-current link for this revision for this page
*
* @param Revision $rev
- * @param bool $latest This is the latest revision of the page?
* @return string
*/
- function curLink( $rev, $latest ) {
+ function curLink( $rev ) {
$cur = $this->historyPage->message['cur'];
- if ( $latest || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
+ $latest = $this->getWikiPage()->getLatest();
+ if ( $latest === $rev->getId() || !$rev->userCan( Revision::DELETED_TEXT, $this->getUser() ) ) {
return $cur;
} else {
return MediaWikiServices::getInstance()->getLinkRenderer()->makeKnownLink(
new HtmlArmor( $cur ),
[],
[
- 'diff' => $this->getWikiPage()->getLatest(),
+ 'diff' => $latest,
'oldid' => $rev->getId()
]
);
private function isCacheable( LinkTarget $title ) {
$ns = $title->getNamespace();
- if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY ] ) ) {
+ if ( in_array( $ns, [ NS_TEMPLATE, NS_FILE, NS_CATEGORY, NS_MEDIAWIKI ] ) ) {
return true;
}
// Focus on transcluded pages more than the main content
use CLDRPluralRuleParser\Evaluator;
use CLDRPluralRuleParser\Error as CLDRPluralRuleError;
+use MediaWiki\Logger\LoggerFactory;
use MediaWiki\MediaWikiServices;
/**
*/
private $store;
+ /**
+ * @var \Psr\Log\LoggerInterface
+ */
+ private $logger;
+
/**
* A 2-d associative array, code/key, where presence indicates that the item
* is loaded. Value arbitrary.
global $wgCacheDirectory;
$this->conf = $conf;
+ $this->logger = LoggerFactory::getInstance( 'localisation' );
$directory = !empty( $conf['storeDirectory'] ) ? $conf['storeDirectory'] : $wgCacheDirectory;
$storeArg = [];
);
}
}
-
- wfDebugLog( 'caches', static::class . ": using store $storeClass" );
+ $this->logger->debug( static::class . ": using store $storeClass" );
$this->store = new $storeClass( $storeArg );
foreach ( [ 'manualRecache', 'forceRecache' ] as $var ) {
*/
public function isExpired( $code ) {
if ( $this->forceRecache && !isset( $this->recachedLangs[$code] ) ) {
- wfDebug( __METHOD__ . "($code): forced reload\n" );
+ $this->logger->debug( __METHOD__ . "($code): forced reload\n" );
return true;
}
$preload = $this->store->get( $code, 'preload' );
// Different keys may expire separately for some stores
if ( $deps === null || $keys === null || $preload === null ) {
- wfDebug( __METHOD__ . "($code): cache missing, need to make one\n" );
+ $this->logger->debug( __METHOD__ . "($code): cache missing, need to make one\n" );
return true;
}
// anymore (e.g. uninstalled extensions)
// When this happens, always expire the cache
if ( !$dep instanceof CacheDependency || $dep->isExpired() ) {
- wfDebug( __METHOD__ . "($code): cache for $code expired due to " .
+ $this->logger->debug( __METHOD__ . "($code): cache for $code expired due to " .
get_class( $dep ) . "\n" );
return true;
try {
$compiledRules = Evaluator::compile( $rules );
} catch ( CLDRPluralRuleError $e ) {
- wfDebugLog( 'l10n', $e->getMessage() );
+ $this->logger->debug( $e->getMessage() );
return [];
}
# Load the primary localisation from the source file
$data = $this->readSourceFilesAndRegisterDeps( $code, $deps );
if ( $data === false ) {
- wfDebug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
+ $this->logger->debug( __METHOD__ . ": no localisation file for $code, using fallback to en\n" );
$coreData['fallback'] = 'en';
} else {
- wfDebug( __METHOD__ . ": got localisation for $code from source\n" );
+ $this->logger->debug( __METHOD__ . ": got localisation for $code from source\n" );
# Merge primary localisation
foreach ( $data as $key => $value ) {
* @param array $params Additional parameters include:
* - keywordTableMap : Map of reserved table names to alternative table names to use
*/
- function __construct( array $params ) {
+ public function __construct( array $params ) {
$this->keywordTableMap = $params['keywordTableMap'] ?? [];
$params['tablePrefix'] = strtoupper( $params['tablePrefix'] );
parent::__construct( $params );
"and database)\n" );
}
+ if ( $schema !== null ) {
+ // We use the *database* aspect of $domain for schema, not the domain schema
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": cannot use schema '$schema'; " .
+ "the database component '$dbName' is actually interpreted as the Oracle schema."
+ );
+ }
+
$this->close();
$this->user = $user;
$this->password = $password;
protected function doSelectDomain( DatabaseDomain $domain ) {
if ( $domain->getSchema() !== null ) {
// We use the *database* aspect of $domain for schema, not the domain schema
- throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": domain '{$domain->getId()}' has a schema component; " .
+ "the database component is actually interpreted as the Oracle schema."
+ );
}
$database = $domain->getDatabase();
*
* @file
*/
+
+use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
+use MediaWiki\Logger\LoggerFactory;
+use Psr\Log\LoggerInterface;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\MediaWikiServices;
use Wikimedia\Rdbms\LBFactory;
protected static function handleUpdateQueue( array &$queue, $mode, $stage ) {
$services = MediaWikiServices::getInstance();
$stats = $services->getStatsdDataFactory();
- $lbFactory = $services->getDBLoadBalancerFactory();
- $method = RequestContext::getMain()->getRequest()->getMethod();
-
- /** @var ErrorPageError $reportableError */
- $reportableError = null;
+ $lbf = $services->getDBLoadBalancerFactory();
+ $logger = LoggerFactory::getInstance( 'DeferredUpdates' );
+ $httpMethod = $services->getMainConfig()->get( 'CommandLineMode' )
+ ? 'cli'
+ : strtolower( RequestContext::getMain()->getRequest()->getMethod() );
+
+ /** @var ErrorPageError $guiEx */
+ $guiEx = null;
/** @var DeferrableUpdate[] $updates Snapshot of queue */
$updates = $queue;
while ( $updates ) {
$queue = []; // clear the queue
- // Order will be DataUpdate followed by generic DeferrableUpdate tasks
- $updatesByType = [ 'data' => [], 'generic' => [] ];
- foreach ( $updates as $du ) {
- if ( $du instanceof DataUpdate ) {
- $updatesByType['data'][] = $du;
+ // Segregate the queue into one for DataUpdate and one for everything else
+ $dataUpdateQueue = [];
+ $genericUpdateQueue = [];
+ foreach ( $updates as $update ) {
+ if ( $update instanceof DataUpdate ) {
+ $dataUpdateQueue[] = $update;
} else {
- $updatesByType['generic'][] = $du;
+ $genericUpdateQueue[] = $update;
}
-
- $name = ( $du instanceof DeferrableCallback )
- ? get_class( $du ) . '-' . $du->getOrigin()
- : get_class( $du );
- $stats->increment( 'deferred_updates.' . $method . '.' . $name );
}
-
- // Execute all remaining tasks...
- foreach ( $updatesByType as $updatesForType ) {
- foreach ( $updatesForType as $update ) {
+ // Execute all DataUpdate queue followed by the DeferrableUpdate queue...
+ foreach ( [ $dataUpdateQueue, $genericUpdateQueue ] as $updateQueue ) {
+ foreach ( $updateQueue as $du ) {
+ // Enqueue the task into the job queue system instead if applicable
+ if ( $mode === 'enqueue' && $du instanceof EnqueueableDataUpdate ) {
+ self::jobify( $du, $lbf, $logger, $stats, $httpMethod );
+ continue;
+ }
+ // Otherwise, execute the task and any subtasks that it spawns
self::$executeContext = [ 'stage' => $stage, 'subqueue' => [] ];
try {
- /** @var DeferrableUpdate $update */
- $guiError = self::handleUpdate( $update, $lbFactory, $mode, $stage );
- $reportableError = $reportableError ?: $guiError;
+ $e = self::run( $du, $lbf, $logger, $stats, $httpMethod );
+ $guiEx = $guiEx ?: ( $e instanceof ErrorPageError ? $e : null );
// Do the subqueue updates for $update until there are none
while ( self::$executeContext['subqueue'] ) {
- $subUpdate = reset( self::$executeContext['subqueue'] );
+ $duChild = reset( self::$executeContext['subqueue'] );
$firstKey = key( self::$executeContext['subqueue'] );
unset( self::$executeContext['subqueue'][$firstKey] );
- $guiError = self::handleUpdate( $subUpdate, $lbFactory, $mode, $stage );
- $reportableError = $reportableError ?: $guiError;
+ $e = self::run( $duChild, $lbf, $logger, $stats, $httpMethod );
+ $guiEx = $guiEx ?: ( $e instanceof ErrorPageError ? $e : null );
}
} finally {
// Make sure we always clean up the context.
$updates = $queue; // new snapshot of queue (check for new entries)
}
- if ( $reportableError ) {
- throw $reportableError; // throw the first of any GUI errors
+ // Throw the first of any GUI errors as long as the context is HTTP pre-send. However,
+ // callers should check permissions *before* enqueueing updates. If the main transaction
+ // round actions succeed but some deferred updates fail due to permissions errors then
+ // there is a risk that some secondary data was not properly updated.
+ if ( $guiEx && $stage === self::PRESEND && !headers_sent() ) {
+ throw $guiEx;
}
}
/**
- * Run or enqueue an update
+ * Run a task and catch/log any exceptions
*
* @param DeferrableUpdate $update
* @param LBFactory $lbFactory
- * @param string $mode
- * @param int $stage
- * @return ErrorPageError|null
+ * @param LoggerInterface $logger
+ * @param StatsdDataFactoryInterface $stats
+ * @param string $httpMethod
+ * @return Exception|Throwable|null
*/
- private static function handleUpdate(
- DeferrableUpdate $update, LBFactory $lbFactory, $mode, $stage
+ private static function run(
+ DeferrableUpdate $update,
+ LBFactory $lbFactory,
+ LoggerInterface $logger,
+ StatsdDataFactoryInterface $stats,
+ $httpMethod
) {
- $guiError = null;
+ $name = get_class( $update );
+ $suffix = ( $update instanceof DeferrableCallback ) ? "_{$update->getOrigin()}" : '';
+ $stats->increment( "deferred_updates.$httpMethod.{$name}{$suffix}" );
+
+ $e = null;
try {
- if ( $mode === 'enqueue' && $update instanceof EnqueueableDataUpdate ) {
- // Run only the job enqueue logic to complete the update later
- $spec = $update->getAsJobSpecification();
- $domain = $spec['domain'] ?? $spec['wiki'];
- JobQueueGroup::singleton( $domain )->push( $spec['job'] );
- } else {
- self::attemptUpdate( $update, $lbFactory );
- }
+ self::attemptUpdate( $update, $lbFactory );
} catch ( Exception $e ) {
- // Reporting GUI exceptions does not work post-send
- if ( $e instanceof ErrorPageError && $stage === self::PRESEND ) {
- $guiError = $e;
- }
- $lbFactory->rollbackMasterChanges( __METHOD__ );
+ } catch ( Throwable $e ) {
+ }
+ if ( $e ) {
+ $logger->error(
+ "Deferred update {type} failed: {message}",
+ [
+ 'type' => $name . $suffix,
+ 'message' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]
+ );
+ $lbFactory->rollbackMasterChanges( __METHOD__ );
// VW-style hack to work around T190178, so we can make sure
// PageMetaDataUpdater doesn't throw exceptions.
if ( defined( 'MW_PHPUNIT_TEST' ) ) {
}
}
- return $guiError;
+ return $e;
+ }
+
+ /**
+ * Push a task into the job queue system and catch/log any exceptions
+ *
+ * @param EnqueueableDataUpdate $update
+ * @param LBFactory $lbFactory
+ * @param LoggerInterface $logger
+ * @param StatsdDataFactoryInterface $stats
+ * @param string $httpMethod
+ */
+ private static function jobify(
+ EnqueueableDataUpdate $update,
+ LBFactory $lbFactory,
+ LoggerInterface $logger,
+ StatsdDataFactoryInterface $stats,
+ $httpMethod
+ ) {
+ $stats->increment( "deferred_updates.$httpMethod." . get_class( $update ) );
+
+ $e = null;
+ try {
+ $spec = $update->getAsJobSpecification();
+ JobQueueGroup::singleton( $spec['domain'] ?? $spec['wiki'] )->push( $spec['job'] );
+ } catch ( Exception $e ) {
+ } catch ( Throwable $e ) {
+ }
+
+ if ( $e ) {
+ $logger->error(
+ "Job insertion of deferred update {type} failed: {message}",
+ [
+ 'type' => get_class( $update ),
+ 'message' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString()
+ ]
+ );
+ $lbFactory->rollbackMasterChanges( __METHOD__ );
+ }
}
/**
$update instanceof TransactionRoundAwareUpdate &&
$update->getTransactionRoundRequirement() == $update::TRX_ROUND_ABSENT
) {
- $update->doUpdate();
+ $fnameTrxOwner = null;
} else {
- // Run the bulk of the update now
$fnameTrxOwner = get_class( $update ) . '::doUpdate';
+ }
+
+ if ( $fnameTrxOwner !== null ) {
$lbFactory->beginMasterChanges( $fnameTrxOwner );
- $update->doUpdate();
+ }
+
+ $update->doUpdate();
+
+ if ( $fnameTrxOwner !== null ) {
$lbFactory->commitMasterChanges( $fnameTrxOwner );
}
}
}
/**
- * Acquire a lock for performing link table updates for a page on a DB
+ * Acquire a session-level lock for performing link table updates for a page on a DB
*
* @param IDatabase $dbw
* @param int $pageId
* @since 1.27
*/
public static function acquirePageLock( IDatabase $dbw, $pageId, $why = 'atomicity' ) {
- $key = "LinksUpdate:$why:pageid:$pageId";
+ $key = "{$dbw->getDomainID()}:LinksUpdate:$why:pageid:$pageId"; // per-wiki
$scopedLock = $dbw->getScopedLockAndFlush( $key, __METHOD__, 15 );
if ( !$scopedLock ) {
$logger = LoggerFactory::getInstance( 'SecondaryDataUpdate' );
'type' => 'sqlite',
'dbname' => \"{\$wgDBname}_jobqueue\",
'tablePrefix' => '',
+ 'variables' => [ 'synchronous' => 'NORMAL' ],
'dbDirectory' => \$wgSQLiteDataDir,
'trxMode' => 'IMMEDIATE',
'flags' => 0
"config-copyright": "== Аўтарскае права і ўмовы ==\n\n$1\n\nThis program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.\n\nThis program is distributed in the hope that it will be useful, but '''without any warranty'''; without even the implied warranty of '''merchantability''' or '''fitness for a particular purpose'''.\nSee the GNU General Public License for more details.\n\nYou should have received <doclink href=Copying>a copy of the GNU General Public License</doclink> along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. or [https://www.gnu.org/copyleft/gpl.html read it online].",
"config-sidebar": "* [https://www.mediawiki.org Хатняя старонка MediaWiki]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Help:Contents Даведка для ўдзельнікаў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:Contents Даведка для адміністратараў]\n* [https://www.mediawiki.org/wiki/Special:MyLanguage/Manual:FAQ Адказы на частыя пытаньні]",
"config-sidebar-readme": "Прачытай мяне",
+ "config-sidebar-relnotes": "Заўвагі да выпуску",
"config-env-good": "Асяродзьдзе было праверанае.\nВы можаце ўсталёўваць MediaWiki.",
"config-env-bad": "Асяродзьдзе было праверанае.\nУсталяваньне MediaWiki немагчымае.",
"config-env-php": "Усталяваны PHP $1.",
"config-page-existingwiki": "Eksisterende wiki",
"config-help-restart": "Vil du rydde alle gemte data, du har indtastet og genstarte installationen?",
"config-restart": "Ja, genstart den",
+ "config-sidebar-upgrade": "Opgraderer",
"config-env-php": "PHP $1 er installeret.",
"config-env-hhvm": "HHVM $1 er installeret.",
"config-apc": "[https://www.php.net/apc APC] er installeret",
"Tosky",
"Selven",
"Sarah Bernabei",
- "ArTrix"
+ "ArTrix",
+ "Annibale covini gerolamo"
]
},
"config-desc": "Programma di installazione per MediaWiki",
"config-sidebar": "* [https://www.mediawiki.org Pagina principale MediaWiki]\n* [https://www.mediawiki.org/Special:MyLanguage/Help:Contents Guida ai contenuti per utenti]\n* [https://www.mediawiki.org/Special:MyLanguage/Manual:Contents Guida ai contenuti per admin]\n* [https://www.mediawiki.org/Special:MyLanguage/Manual:FAQ FAQ]",
"config-sidebar-readme": "Leggimi",
"config-sidebar-relnotes": "Note di versione",
+ "config-sidebar-license": "copiando",
"config-sidebar-upgrade": "Aggiornamento",
"config-env-good": "L'ambiente è stato controllato.\nÈ possibile installare MediaWiki.",
"config-env-bad": "L'ambiente è stato controllato.\nNon è possibile installare MediaWiki.",
*
* @file
*/
+
+use MediaWiki\Logger\LoggerFactory;
use Psr\Log\LoggerInterface;
/**
"Non-daemonized mode is no longer supported. Please install the " .
"mediawiki/services/jobrunner service and update \$wgJobTypeConf as needed." );
}
- $this->logger = \MediaWiki\Logger\LoggerFactory::getInstance( 'redis' );
+ $this->logger = LoggerFactory::getInstance( 'redis' );
}
protected function supportedOrders() {
try {
return $conn->lSize( $this->getQueueKey( 'l-unclaimed' ) );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
return array_sum( $conn->exec() );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
try {
return $conn->zSize( $this->getQueueKey( 'z-delayed' ) );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
try {
return $conn->zSize( $this->getQueueKey( 'z-abandoned' ) );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
throw new RedisException( $err );
}
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
$job = $this->getJobFromFields( $item ); // may be false
} while ( !$job ); // job may be false if invalid
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $job;
$this->incrStats( 'acks', $this->type );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return true;
// Update the timestamp of the last root job started at the location...
return $conn->set( $key, $params['rootJobTimestamp'], self::ROOTJOB_TTL ); // 2 weeks
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
// Get the last time this root job was enqueued
$timestamp = $conn->get( $this->getRootJobCacheKey( $params['rootJobSignature'] ) );
} catch ( RedisException $e ) {
- $timestamp = false;
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
// Check if a new root job was started at the location after this one's...
return $ok;
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
try {
$uids = $conn->lRange( $this->getQueueKey( 'l-unclaimed' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
try {
$uids = $conn->zRange( $this->getQueueKey( 'z-delayed' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
try {
$uids = $conn->zRange( $this->getQueueKey( 'z-claimed' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
try {
$uids = $conn->zRange( $this->getQueueKey( 'z-abandoned' ), 0, -1 );
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $this->getJobIterator( $conn, $uids );
}
}
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $sizes;
* This function should not be called outside JobQueueRedis
*
* @param string $uid
- * @param RedisConnRef $conn
+ * @param RedisConnRef|Redis $conn
* @return RunnableJob|bool Returns false if the job does not exist
* @throws JobQueueError
* @throws UnexpectedValueException
*/
- public function getJobFromUidInternal( $uid, RedisConnRef $conn ) {
+ public function getJobFromUidInternal( $uid, $conn ) {
try {
$data = $conn->hGet( $this->getQueueKey( 'h-data' ), $uid );
if ( $data === false ) {
return $job;
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
}
$queues[] = $this->decodeQueueName( $queue );
}
} catch ( RedisException $e ) {
- $this->throwRedisException( $conn, $e );
+ throw $this->handleErrorAndMakeException( $conn, $e );
}
return $queues;
/**
* Get a connection to the server that handles all sub-queues for this queue
*
- * @return RedisConnRef
+ * @return RedisConnRef|Redis
* @throws JobQueueConnectionError
*/
protected function getConnection() {
/**
* @param RedisConnRef $conn
* @param RedisException $e
- * @throws JobQueueError
+ * @return JobQueueError
*/
- protected function throwRedisException( RedisConnRef $conn, $e ) {
+ protected function handleErrorAndMakeException( RedisConnRef $conn, $e ) {
$this->redisPool->handleError( $conn, $e );
- throw new JobQueueError( "Redis server error: {$e->getMessage()}\n" );
+ return new JobQueueError( "Redis server error: {$e->getMessage()}\n" );
}
/**
}
// Use a named lock so that jobs for this page see each others' changes
- $lockKey = "CategoryMembershipUpdates:{$page->getId()}";
+ $lockKey = "{$dbw->getDomainID()}:CategoryMembershipChange:{$page->getId()}"; // per-wiki
$scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 3 );
if ( !$scopedLock ) {
$this->setLastError( "Could not acquire lock '$lockKey'" );
}
// Use a named lock so that jobs for this user see each others' changes
- $lockKey = "ClearUserWatchlistJob:$userId";
+ $lockKey = "{{$dbw->getDomainID()}}:ClearUserWatchlist:$userId"; // per-wiki
$scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 10 );
if ( !$scopedLock ) {
$this->setLastError( "Could not acquire lock '$lockKey'" );
*/
use MediaWiki\MediaWikiServices;
use MediaWiki\Revision\RevisionRecord;
+use MediaWiki\Revision\RevisionRenderer;
+use Liuggio\StatsdClient\Factory\StatsdDataFactoryInterface;
/**
* Job to update link tables for pages
* @ingroup JobQueue
*/
class RefreshLinksJob extends Job {
- /** @var float Cache parser output when it takes this long to render */
- const PARSE_THRESHOLD_SEC = 1.0;
/** @var int Lag safety margin when comparing root job times to last-refresh times */
- const CLOCK_FUDGE = 10;
+ const NORMAL_MAX_LAG = 10;
/** @var int How many seconds to wait for replica DBs to catch up */
const LAG_WAIT_TIMEOUT = 15;
!( isset( $params['pages'] ) && count( $params['pages'] ) != 1 )
);
$this->params += [ 'causeAction' => 'unknown', 'causeAgent' => 'unknown' ];
- // This will control transaction rounds in order to run DataUpdates
+ // Tell JobRunner to not automatically wrap run() in a transaction round.
+ // Each runForTitle() call will manage its own rounds in order to run DataUpdates
+ // and to avoid contention as well.
$this->executionFlags |= self::JOB_NO_EXPLICIT_TRX_ROUND;
}
}
function run() {
- global $wgUpdateRowsPerJob;
-
$ok = true;
+
// Job to update all (or a range of) backlink pages for a page
if ( !empty( $this->params['recursive'] ) ) {
+ $services = MediaWikiServices::getInstance();
// When the base job branches, wait for the replica DBs to catch up to the master.
// From then on, we know that any template changes at the time the base job was
// enqueued will be reflected in backlink page parses when the leaf jobs run.
if ( !isset( $this->params['range'] ) ) {
- $lbFactory = MediaWikiServices::getInstance()->getDBLoadBalancerFactory();
+ $lbFactory = $services->getDBLoadBalancerFactory();
if ( !$lbFactory->waitForReplication( [
- 'domain' => $lbFactory->getLocalDomainID(),
- 'timeout' => self::LAG_WAIT_TIMEOUT
+ 'domain' => $lbFactory->getLocalDomainID(),
+ 'timeout' => self::LAG_WAIT_TIMEOUT
] ) ) { // only try so hard
- $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
+ $stats = $services->getStatsdDataFactory();
$stats->increment( 'refreshlinks.lag_wait_failed' );
}
}
// jobs and possibly a recursive RefreshLinks job for the rest of the backlinks
$jobs = BacklinkJobUtils::partitionBacklinkJob(
$this,
- $wgUpdateRowsPerJob,
+ $services->getMainConfig()->get( 'UpdateRowsPerJob' ),
1, // job-per-title
[ 'params' => $extraParams ]
);
foreach ( $this->params['pages'] as list( $ns, $dbKey ) ) {
$title = Title::makeTitleSafe( $ns, $dbKey );
if ( $title ) {
- $this->runForTitle( $title );
+ $ok = $this->runForTitle( $title ) && $ok;
} else {
$ok = false;
$this->setLastError( "Invalid title ($ns,$dbKey)." );
}
// Job to update link tables for a given title
} else {
- $this->runForTitle( $this->title );
+ $ok = $this->runForTitle( $this->title );
}
return $ok;
protected function runForTitle( Title $title ) {
$services = MediaWikiServices::getInstance();
$stats = $services->getStatsdDataFactory();
- $lbFactory = $services->getDBLoadBalancerFactory();
- $revisionStore = $services->getRevisionStore();
$renderer = $services->getRevisionRenderer();
+ $parserCache = $services->getParserCache();
+ $lbFactory = $services->getDBLoadBalancerFactory();
$ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
- $lbFactory->beginMasterChanges( __METHOD__ );
-
+ // Load the page from the master DB
$page = WikiPage::factory( $title );
$page->loadPageData( WikiPage::READ_LATEST );
- // Serialize links updates by page ID so they see each others' changes
+ // Serialize link update job by page ID so they see each others' changes.
+ // The page ID and latest revision ID will be queried again after the lock
+ // is acquired to bail if they are changed from that of loadPageData() above.
$dbw = $lbFactory->getMainLB()->getConnection( DB_MASTER );
- /** @noinspection PhpUnusedLocalVariableInspection */
$scopedLock = LinksUpdate::acquirePageLock( $dbw, $page->getId(), 'job' );
if ( $scopedLock === null ) {
- $lbFactory->commitMasterChanges( __METHOD__ );
- // Another job is already updating the page, likely for an older revision (T170596).
+ // Another job is already updating the page, likely for a prior revision (T170596)
$this->setLastError( 'LinksUpdate already running for this page, try again later.' );
+ $stats->increment( 'refreshlinks.lock_failure' );
+
+ return false;
+ }
+
+ if ( $this->isAlreadyRefreshed( $page ) ) {
+ $stats->increment( 'refreshlinks.update_skipped' );
+
+ return true;
+ }
+
+ // Parse during a fresh transaction round for better read consistency
+ $lbFactory->beginMasterChanges( __METHOD__ );
+ $output = $this->getParserOutput( $renderer, $parserCache, $page, $stats );
+ $options = $this->getDataUpdateOptions();
+ $lbFactory->commitMasterChanges( __METHOD__ );
+
+ if ( !$output ) {
+ return false; // raced out?
+ }
+
+ // Tell DerivedPageDataUpdater to use this parser output
+ $options['known-revision-output'] = $output;
+ // Execute corresponding DataUpdates immediately
+ $page->doSecondaryDataUpdates( $options );
+ InfoAction::invalidateCache( $title );
+
+ // Commit any writes here in case this method is called in a loop.
+ // In that case, the scoped lock will fail to be acquired.
+ $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
+
+ return true;
+ }
+
+ /**
+ * @param WikiPage $page
+ * @return bool Whether something updated the backlinks with data newer than this job
+ */
+ private function isAlreadyRefreshed( WikiPage $page ) {
+ // Get the timestamp of the change that triggered this job
+ $rootTimestamp = $this->params['rootJobTimestamp'] ?? null;
+ if ( $rootTimestamp === null ) {
return false;
}
- // Get the latest ID *after* acquirePageLock() flushed the transaction.
+
+ if ( !empty( $this->params['isOpportunistic'] ) ) {
+ // Neither clock skew nor DB snapshot/replica DB lag matter much for
+ // such updates; focus on reusing the (often recently updated) cache
+ $lagAwareTimestamp = $rootTimestamp;
+ } else {
+ // For transclusion updates, the template changes must be reflected
+ $lagAwareTimestamp = wfTimestamp(
+ TS_MW,
+ wfTimestamp( TS_UNIX, $rootTimestamp ) + self::NORMAL_MAX_LAG
+ );
+ }
+
+ return ( $page->getLinksTimestamp() > $lagAwareTimestamp );
+ }
+
+ /**
+ * Get the parser output if the page is unchanged from what was loaded in $page
+ *
+ * @param RevisionRenderer $renderer
+ * @param ParserCache $parserCache
+ * @param WikiPage $page Page already loaded with READ_LATEST
+ * @param StatsdDataFactoryInterface $stats
+ * @return ParserOutput|null Combined output for all slots; might only contain metadata
+ */
+ private function getParserOutput(
+ RevisionRenderer $renderer,
+ ParserCache $parserCache,
+ WikiPage $page,
+ StatsdDataFactoryInterface $stats
+ ) {
+ $revision = $this->getCurrentRevisionIfUnchanged( $page, $stats );
+ if ( !$revision ) {
+ return null; // race condition?
+ }
+
+ $cachedOutput = $this->getParserOutputFromCache( $parserCache, $page, $revision, $stats );
+ if ( $cachedOutput ) {
+ return $cachedOutput;
+ }
+
+ $renderedRevision = $renderer->getRenderedRevision(
+ $revision,
+ $page->makeParserOptions( 'canonical' ),
+ null,
+ [ 'audience' => $revision::RAW ]
+ );
+
+ $parseTimestamp = wfTimestampNow(); // timestamp that parsing started
+ $output = $renderedRevision->getRevisionParserOutput( [ 'generate-html' => false ] );
+ $output->setCacheTime( $parseTimestamp ); // notify LinksUpdate::doUpdate()
+
+ return $output;
+ }
+
+ /**
+ * Get the current revision record if it is unchanged from what was loaded in $page
+ *
+ * @param WikiPage $page Page already loaded with READ_LATEST
+ * @param StatsdDataFactoryInterface $stats
+ * @return RevisionRecord|null The same instance that $page->getRevisionRecord() uses
+ */
+ private function getCurrentRevisionIfUnchanged(
+ WikiPage $page,
+ StatsdDataFactoryInterface $stats
+ ) {
+ $title = $page->getTitle();
+ // Get the latest ID since acquirePageLock() in runForTitle() flushed the transaction.
// This is used to detect edits/moves after loadPageData() but before the scope lock.
- // The works around the chicken/egg problem of determining the scope lock key.
+ // The works around the chicken/egg problem of determining the scope lock key name.
$latest = $title->getLatestRevID( Title::GAID_FOR_UPDATE );
- if ( !empty( $this->params['triggeringRevisionId'] ) ) {
- // Fetch the specified revision; lockAndGetLatest() below detects if the page
- // was edited since and aborts in order to avoid corrupting the link tables
- $revision = $revisionStore->getRevisionById(
- (int)$this->params['triggeringRevisionId'],
- Revision::READ_LATEST
- );
- } else {
- // Fetch current revision; READ_LATEST reduces lockAndGetLatest() check failures
- $revision = $revisionStore->getRevisionByTitle( $title, 0, Revision::READ_LATEST );
+ $triggeringRevisionId = $this->params['triggeringRevisionId'] ?? null;
+ if ( $triggeringRevisionId && $triggeringRevisionId !== $latest ) {
+ // This job is obsolete and one for the latest revision will handle updates
+ $stats->increment( 'refreshlinks.rev_not_current' );
+ $this->setLastError( "Revision $triggeringRevisionId is not current" );
+
+ return null;
}
+ // Load the current revision. Note that $page should have loaded with READ_LATEST.
+ // This instance will be reused in WikiPage::doSecondaryDataUpdates() later on.
+ $revision = $page->getRevisionRecord();
if ( !$revision ) {
- $lbFactory->commitMasterChanges( __METHOD__ );
$stats->increment( 'refreshlinks.rev_not_found' );
$this->setLastError( "Revision not found for {$title->getPrefixedDBkey()}" );
- return false; // just deleted?
- } elseif ( $revision->getId() != $latest || $revision->getPageId() !== $page->getId() ) {
- $lbFactory->commitMasterChanges( __METHOD__ );
+
+ return null; // just deleted?
+ } elseif ( $revision->getId() !== $latest || $revision->getPageId() !== $page->getId() ) {
// Do not clobber over newer updates with older ones. If all jobs where FIFO and
// serialized, it would be OK to update links based on older revisions since it
// would eventually get to the latest. Since that is not the case (by design),
// only update the link tables to a state matching the current revision's output.
$stats->increment( 'refreshlinks.rev_not_current' );
$this->setLastError( "Revision {$revision->getId()} is not current" );
- return false;
+
+ return null;
}
- $parserOutput = false;
- $parserOptions = $page->makeParserOptions( 'canonical' );
+ return $revision;
+ }
+
+ /**
+ * Get the parser output from cache if it reflects the change that triggered this job
+ *
+ * @param ParserCache $parserCache
+ * @param WikiPage $page
+ * @param RevisionRecord $currentRevision
+ * @param StatsdDataFactoryInterface $stats
+ * @return ParserOutput|null
+ */
+ private function getParserOutputFromCache(
+ ParserCache $parserCache,
+ WikiPage $page,
+ RevisionRecord $currentRevision,
+ StatsdDataFactoryInterface $stats
+ ) {
+ $cachedOutput = null;
// If page_touched changed after this root job, then it is likely that
// any views of the pages already resulted in re-parses which are now in
// cache. The cache can be reused to avoid expensive parsing in some cases.
- if ( isset( $this->params['rootJobTimestamp'] ) ) {
+ $rootTimestamp = $this->params['rootJobTimestamp'] ?? null;
+ if ( $rootTimestamp !== null ) {
$opportunistic = !empty( $this->params['isOpportunistic'] );
-
- $skewedTimestamp = $this->params['rootJobTimestamp'];
if ( $opportunistic ) {
- // Neither clock skew nor DB snapshot/replica DB lag matter much for such
- // updates; focus on reusing the (often recently updated) cache
+ // Neither clock skew nor DB snapshot/replica DB lag matter much for
+ // such updates; focus on reusing the (often recently updated) cache
+ $lagAwareTimestamp = $rootTimestamp;
} else {
// For transclusion updates, the template changes must be reflected
- $skewedTimestamp = wfTimestamp( TS_MW,
- wfTimestamp( TS_UNIX, $skewedTimestamp ) + self::CLOCK_FUDGE
+ $lagAwareTimestamp = wfTimestamp(
+ TS_MW,
+ wfTimestamp( TS_UNIX, $rootTimestamp ) + self::NORMAL_MAX_LAG
);
}
- if ( $page->getLinksTimestamp() > $skewedTimestamp ) {
- $lbFactory->commitMasterChanges( __METHOD__ );
- // Something already updated the backlinks since this job was made
- $stats->increment( 'refreshlinks.update_skipped' );
- return true;
- }
-
- if ( $page->getTouched() >= $this->params['rootJobTimestamp'] || $opportunistic ) {
- // Cache is suspected to be up-to-date. As long as the cache rev ID matches
- // and it reflects the job's triggering change, then it is usable.
- $parserOutput = $services->getParserCache()->getDirty( $page, $parserOptions );
- if ( !$parserOutput
- || $parserOutput->getCacheRevisionId() != $revision->getId()
- || $parserOutput->getCacheTime() < $skewedTimestamp
+ if ( $page->getTouched() >= $rootTimestamp || $opportunistic ) {
+ // Cache is suspected to be up-to-date so it's worth the I/O of checking.
+ // As long as the cache rev ID matches the current rev ID and it reflects
+ // the job's triggering change, then it is usable.
+ $parserOptions = $page->makeParserOptions( 'canonical' );
+ $output = $parserCache->getDirty( $page, $parserOptions );
+ if (
+ $output &&
+ $output->getCacheRevisionId() == $currentRevision->getId() &&
+ $output->getCacheTime() >= $lagAwareTimestamp
) {
- $parserOutput = false; // too stale
+ $cachedOutput = $output;
}
}
}
- // Fetch the current revision and parse it if necessary...
- if ( $parserOutput ) {
+ if ( $cachedOutput ) {
$stats->increment( 'refreshlinks.parser_cached' );
} else {
- $start = microtime( true );
-
- $checkCache = $page->shouldCheckParserCache( $parserOptions, $revision->getId() );
-
- // Revision ID must be passed to the parser output to get revision variables correct
- $renderedRevision = $renderer->getRenderedRevision(
- $revision,
- $parserOptions,
- null,
- [
- // use master, for consistency with the getRevisionByTitle call above.
- 'use-master' => true,
- // bypass audience checks, since we know that this is the current revision.
- 'audience' => RevisionRecord::RAW
- ]
- );
- $parserOutput = $renderedRevision->getRevisionParserOutput(
- // HTML is only needed if the output is to be placed in the parser cache
- [ 'generate-html' => $checkCache ]
- );
-
- // If it took a long time to render, then save this back to the cache to avoid
- // wasted CPU by other apaches or job runners. We don't want to always save to
- // cache as this can cause high cache I/O and LRU churn when a template changes.
- $elapsed = microtime( true ) - $start;
-
- $parseThreshold = $this->params['parseThreshold'] ?? self::PARSE_THRESHOLD_SEC;
-
- if ( $checkCache && $elapsed >= $parseThreshold && $parserOutput->isCacheable() ) {
- $ctime = wfTimestamp( TS_MW, (int)$start ); // cache time
- $services->getParserCache()->save(
- $parserOutput, $page, $parserOptions, $ctime, $revision->getId()
- );
- }
$stats->increment( 'refreshlinks.parser_uncached' );
}
+ return $cachedOutput;
+ }
+
+ /**
+ * @return array
+ */
+ private function getDataUpdateOptions() {
$options = [
'recursive' => !empty( $this->params['useRecursiveLinksUpdate'] ),
// Carry over cause so the update can do extra logging
}
}
- $lbFactory->commitMasterChanges( __METHOD__ );
-
- $page->doSecondaryDataUpdates( $options );
-
- InfoAction::invalidateCache( $title );
-
- // Commit any writes here in case this method is called in a loop.
- // In that case, the scoped lock will fail to be acquired.
- $lbFactory->commitAndWaitForReplication( __METHOD__, $ticket );
-
- return true;
+ return $options;
}
public function getDeduplicationInfo() {
/** @var float|null */
private $wallClockOverride;
+ /** @var float */
const RANK_TOP = 1.0;
/** @var int Array key that holds the entry's main timestamp (flat key use) */
*
* @param string $key
* @param mixed $value
- * @param float $rank Bottom fraction of the list where keys start off [Default: 1.0]
+ * @param float $rank Bottom fraction of the list where keys start off [default: 1.0]
* @return void
*/
public function set( $key, $value, $rank = self::RANK_TOP ) {
* Check if a key exists
*
* @param string $key
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return bool
+ * @since 1.32 Added $maxAge
*/
- public function has( $key, $maxAge = 0.0 ) {
+ public function has( $key, $maxAge = INF ) {
if ( !is_int( $key ) && !is_string( $key ) ) {
throw new UnexpectedValueException(
__METHOD__ . ': invalid key; must be string or integer.' );
* If the item is already set, it will be pushed to the top of the cache.
*
* @param string $key
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
- * @return mixed Returns null if the key was not found or is older than $maxAge
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
+ * @param mixed|null $default Value to return if no key is found [default: null]
+ * @return mixed Returns $default if the key was not found or is older than $maxAge
+ * @since 1.32 Added $maxAge
+ * @since 1.34 Added $default
*/
- public function get( $key, $maxAge = 0.0 ) {
+ public function get( $key, $maxAge = INF, $default = null ) {
if ( !$this->has( $key, $maxAge ) ) {
- return null;
+ return $default;
}
$this->ping( $key );
/**
* @param string|int $key
* @param string|int $field
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return bool
+ * @since 1.32 Added $maxAge
*/
- public function hasField( $key, $field, $maxAge = 0.0 ) {
+ public function hasField( $key, $field, $maxAge = INF ) {
$value = $this->get( $key );
if ( !is_int( $field ) && !is_string( $field ) ) {
/**
* @param string|int $key
* @param string|int $field
- * @param float $maxAge Ignore items older than this many seconds [optional] (since 1.32)
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return mixed Returns null if the key was not found or is older than $maxAge
+ * @since 1.32 Added $maxAge
*/
- public function getField( $key, $field, $maxAge = 0.0 ) {
+ public function getField( $key, $field, $maxAge = INF ) {
if ( !$this->hasField( $key, $field, $maxAge ) ) {
return null;
}
* @since 1.28
* @param string $key
* @param callable $callback Callback that will produce the value
- * @param float $rank Bottom fraction of the list where keys start off [Default: 1.0]
- * @param float $maxAge Ignore items older than this many seconds [Default: 0.0] (since 1.32)
+ * @param float $rank Bottom fraction of the list where keys start off [default: 1.0]
+ * @param float $maxAge Ignore items older than this many seconds [default: INF]
* @return mixed The cached value if found or the result of $callback otherwise
+ * @since 1.32 Added $maxAge
*/
public function getWithSetCallback(
- $key, callable $callback, $rank = self::RANK_TOP, $maxAge = 0.0
+ $key, callable $callback, $rank = self::RANK_TOP, $maxAge = INF
) {
if ( $this->has( $key, $maxAge ) ) {
$value = $this->get( $key );
* @param int $flags Bitfield of BagOStuff::WRITE_* constants
* @return bool Success
*/
- protected function mergeViaCas( $key, $callback, $exptime = 0, $attempts = 10, $flags = 0 ) {
+ final protected function mergeViaCas( $key, callable $callback, $exptime, $attempts, $flags ) {
do {
$casToken = null; // passed by reference
// Get the old value and CAS token from cache
/**
* Delete all objects expiring before a certain date.
* @param string|int $timestamp The reference date in MW or TS_UNIX format
- * @param callable|null $progressCallback Optional, a function which will be called
+ * @param callable|null $progress Optional, a function which will be called
* regularly during long-running operations with the percentage progress
* as the first parameter. [optional]
* @param int $limit Maximum number of keys to delete [default: INF]
*
- * @return bool Success, false if unimplemented
+ * @return bool Success; false if unimplemented
*/
public function deleteObjectsExpiringBefore(
$timestamp,
- callable $progressCallback = null,
+ callable $progress = null,
$limit = INF
) {
- // stub
return false;
}
* @return bool Success
* @since 1.24
*/
- final public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
+ public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
}
-
return $this->doSetMulti( $data, $exptime, $flags );
}
foreach ( $data as $key => $value ) {
$res = $this->doSet( $key, $value, $exptime, $flags ) && $res;
}
-
return $res;
}
* @return bool Success
* @since 1.33
*/
- final public function deleteMulti( array $keys, $flags = 0 ) {
+ public function deleteMulti( array $keys, $flags = 0 ) {
if ( ( $flags & self::WRITE_ALLOW_SEGMENTS ) === self::WRITE_ALLOW_SEGMENTS ) {
throw new InvalidArgumentException( __METHOD__ . ' got WRITE_ALLOW_SEGMENTS' );
}
-
return $this->doDeleteMulti( $keys, $flags );
}
foreach ( $keys as $key ) {
$res = $this->doDelete( $key, $flags ) && $res;
}
-
return $res;
}
* @param mixed $mainValue
* @return string|null|bool The combined string, false if missing, null on error
*/
- protected function resolveSegments( $key, $mainValue ) {
+ final protected function resolveSegments( $key, $mainValue ) {
if ( SerializedValueContainer::isUnified( $mainValue ) ) {
return $this->unserialize( $mainValue->{SerializedValueContainer::UNIFIED_DATA} );
}
* @param callable $workCallback
* @since 1.28
*/
- public function addBusyCallback( callable $workCallback ) {
+ final public function addBusyCallback( callable $workCallback ) {
$this->busyCallbacks[] = $workCallback;
}
*/
protected function debug( $text ) {
if ( $this->debugMode ) {
- $this->logger->debug( "{class} debug: $text", [
- 'class' => static::class,
- ] );
+ $this->logger->debug( "{class} debug: $text", [ 'class' => static::class ] );
}
}
* @param int $exptime
* @return bool
*/
- protected function expiryIsRelative( $exptime ) {
+ final protected function expiryIsRelative( $exptime ) {
return ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) );
}
* @param int $exptime Absolute TTL or 0 for indefinite
* @return int
*/
- protected function convertToExpiry( $exptime ) {
- $exptime = (int)$exptime; // sanity
-
+ final protected function convertToExpiry( $exptime ) {
return $this->expiryIsRelative( $exptime )
? (int)$this->getCurrentTime() + $exptime
: $exptime;
* @param int $exptime
* @return int
*/
- protected function convertToRelative( $exptime ) {
- if ( $exptime >= ( 10 * self::TTL_YEAR ) ) {
- $exptime -= (int)$this->getCurrentTime();
- if ( $exptime <= 0 ) {
- $exptime = 1;
- }
- return $exptime;
- } else {
- return $exptime;
- }
+ final protected function convertToRelative( $exptime ) {
+ return $this->expiryIsRelative( $exptime )
+ ? (int)$exptime
+ : max( $exptime - (int)$this->getCurrentTime(), 1 );
}
/**
* @param mixed $value
* @return bool
*/
- protected function isInteger( $value ) {
+ final protected function isInteger( $value ) {
if ( is_int( $value ) ) {
return true;
} elseif ( !is_string( $value ) ) {
* @param BagOStuff[] $bags
* @return int[] Resulting flag map (class ATTR_* constant => class QOS_* constant)
*/
- protected function mergeFlagMaps( array $bags ) {
+ final protected function mergeFlagMaps( array $bags ) {
$map = [];
foreach ( $bags as $bag ) {
foreach ( $bag->attrMap as $attr => $rank ) {
public function deleteObjectsExpiringBefore(
$timestamp,
- callable $progressCallback = null,
+ callable $progress = null,
$limit = INF
) {
- $this->procCache->deleteObjectsExpiringBefore( $timestamp, $progressCallback, $limit );
+ $this->procCache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
- return $this->backend->deleteObjectsExpiringBefore(
- $timestamp,
- $progressCallback,
- $limit
- );
+ return $this->backend->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
}
// These just call the backend (tested elsewhere)
return false;
}
- protected function doSet( $key, $value, $exp = 0, $flags = 0 ) {
+ protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
return true;
}
return $result;
}
- public function doGetMulti( array $keys, $flags = 0 ) {
+ protected function doGetMulti( array $keys, $flags = 0 ) {
$this->debug( 'getMulti(' . implode( ', ', $keys ) . ')' );
foreach ( $keys as $key ) {
$this->validateKeyEncoding( $key );
return $this->checkResult( false, $result );
}
- public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+ protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
$this->debug( 'setMulti(' . implode( ', ', array_keys( $data ) ) . ')' );
foreach ( array_keys( $data ) as $key ) {
$this->validateKeyEncoding( $key );
return $this->checkResult( false, $result );
}
- public function doDeleteMulti( array $keys, $flags = 0 ) {
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
$this->debug( 'deleteMulti(' . implode( ', ', $keys ) . ')' );
foreach ( $keys as $key ) {
$this->validateKeyEncoding( $key );
);
}
- public function doGetMulti( array $keys, $flags = 0 ) {
+ protected function doGetMulti( array $keys, $flags = 0 ) {
foreach ( $keys as $key ) {
$this->validateKeyEncoding( $key );
}
public function deleteObjectsExpiringBefore(
$timestamp,
- callable $progressCallback = null,
+ callable $progress = null,
$limit = INF
) {
$ret = false;
foreach ( $this->caches as $cache ) {
- if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progressCallback, $limit ) ) {
+ if ( $cache->deleteObjectsExpiringBefore( $timestamp, $progress, $limit ) ) {
$ret = true;
}
}
return $res;
}
- public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+ public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
return $this->doWrite(
$this->cacheIndexes,
$this->usesAsyncWritesGivenFlags( $flags ),
);
}
- public function doDeleteMulti( array $data, $flags = 0 ) {
+ public function deleteMulti( array $data, $flags = 0 ) {
+ return $this->doWrite(
+ $this->cacheIndexes,
+ $this->usesAsyncWritesGivenFlags( $flags ),
+ __FUNCTION__,
+ func_get_args()
+ );
+ }
+
+ public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
return $this->doWrite(
$this->cacheIndexes,
$this->usesAsyncWritesGivenFlags( $flags ),
throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
}
+ protected function doSetMulti( array $keys, $exptime = 0, $flags = 0 ) {
+ throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ }
+
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
+ throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ }
+
protected function serialize( $value ) {
throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
}
- protected function unserialize( $value ) {
+ protected function unserialize( $blob ) {
throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
}
}
return $result;
}
- protected function doSet( $key, $value, $expiry = 0, $flags = 0 ) {
+ protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
list( $server, $conn ) = $this->getConnection( $key );
if ( !$conn ) {
return false;
}
- $expiry = $this->convertToRelative( $expiry );
+ $ttl = $this->convertToRelative( $exptime );
try {
- if ( $expiry ) {
- $result = $conn->setex( $key, $expiry, $this->serialize( $value ) );
+ if ( $ttl ) {
+ $result = $conn->setex( $key, $ttl, $this->serialize( $value ) );
} else {
// No expiry, that is very different from zero expiry in Redis
$result = $conn->set( $key, $this->serialize( $value ) );
return $result;
}
- public function doGetMulti( array $keys, $flags = 0 ) {
+ protected function doGetMulti( array $keys, $flags = 0 ) {
$batches = [];
$conns = [];
foreach ( $keys as $key ) {
return $result;
}
- public function doSetMulti( array $data, $expiry = 0, $flags = 0 ) {
+ protected function doSetMulti( array $data, $expiry = 0, $flags = 0 ) {
$batches = [];
$conns = [];
foreach ( $data as $key => $value ) {
return $result;
}
- public function doDeleteMulti( array $keys, $flags = 0 ) {
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
$batches = [];
$conns = [];
foreach ( $keys as $key ) {
public function deleteObjectsExpiringBefore(
$timestamp,
- callable $progressCallback = null,
+ callable $progress = null,
$limit = INF
) {
- return $this->writeStore->deleteObjectsExpiringBefore(
- $timestamp,
- $progressCallback,
- $limit
- );
+ return $this->writeStore->deleteObjectsExpiringBefore( $timestamp, $progress, $limit );
}
public function getMulti( array $keys, $flags = 0 ) {
: $this->readStore->getMulti( $keys, $flags );
}
- public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+ public function setMulti( array $data, $exptime = 0, $flags = 0 ) {
return $this->writeStore->setMulti( $data, $exptime, $flags );
}
- public function doDeleteMulti( array $keys, $flags = 0 ) {
+ public function deleteMulti( array $keys, $flags = 0 ) {
return $this->writeStore->deleteMulti( $keys, $flags );
}
+ public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+ return $this->writeStore->changeTTLMulti( $keys, $exptime, $flags );
+ }
+
public function incr( $key, $value = 1 ) {
return $this->writeStore->incr( $key, $value );
}
throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
}
+ protected function doSetMulti( array $keys, $exptime = 0, $flags = 0 ) {
+ throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ }
+
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
+ throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
+ }
+
protected function serialize( $value ) {
throw new LogicException( __METHOD__ . ': proxy class does not need this method.' );
}
return $success;
}
- protected function doSet( $key, $value, $expire = 0, $flags = 0 ) {
- $result = wincache_ucache_set( $key, $this->serialize( $value ), $expire );
+ protected function doSet( $key, $value, $exptime = 0, $flags = 0 ) {
+ $result = wincache_ucache_set( $key, $this->serialize( $value ), $exptime );
// false positive, wincache_ucache_set returns an empty array
// in some circumstances.
protected function doSelectDomain( DatabaseDomain $domain ) {
if ( $domain->getSchema() !== null ) {
- throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
+ );
}
$database = $domain->getDatabase();
* - sslCiphers : array list of allowable ciphers [default: null]
* @param array $params
*/
- function __construct( array $params ) {
+ public function __construct( array $params ) {
$this->lagDetectionMethod = $params['lagDetectionMethod'] ?? 'Seconds_Behind_Master';
$this->lagDetectionOptions = $params['lagDetectionOptions'] ?? [];
$this->useGTIDs = !empty( $params['useGTIDs' ] );
$this->close();
if ( $schema !== null ) {
- throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+ throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" );
}
$this->server = $server;
protected function doSelectDomain( DatabaseDomain $domain ) {
if ( $domain->getSchema() !== null ) {
- throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": domain '{$domain->getId()}' has a schema component"
+ );
}
$database = $domain->getDatabase();
);
}
+ $this->close();
+
$this->server = $server;
$this->user = $user;
$this->password = $password;
}
$this->connectString = $this->makeConnectionString( $connectVars );
- $this->close();
- $this->installErrorHandler();
+ $this->installErrorHandler();
try {
// Use new connections to let LoadBalancer/LBFactory handle reuse
$this->conn = pg_connect( $this->connectString, PGSQL_CONNECT_FORCE_NEW );
$this->restoreErrorHandler();
throw $ex;
}
-
$phpError = $this->restoreErrorHandler();
if ( !$this->conn ) {
// See https://www.postgresql.org/docs/8.3/sql-set.html
throw new DBUnexpectedError(
$this,
- __METHOD__ . ": a transaction is currently active."
+ __METHOD__ . ": a transaction is currently active"
);
}
use Exception;
use LockManager;
use FSLockManager;
-use InvalidArgumentException;
use RuntimeException;
use stdClass;
* @ingroup Database
*/
class DatabaseSqlite extends Database {
- /** @var bool Whether full text is enabled */
- private static $fulltextEnabled = null;
-
- /** @var string|null Directory */
+ /** @var string|null Directory for SQLite database files listed under their DB name */
protected $dbDir;
- /** @var string File name for SQLite database file */
+ /** @var string|null Explicit path for the SQLite database file */
protected $dbPath;
/** @var string Transaction mode */
protected $trxMode;
/** @var array List of shared database already attached to this connection */
private $alreadyAttached = [];
+ /** @var bool Whether full text is enabled */
+ private static $fulltextEnabled = null;
+
/**
* Additional params include:
* - dbDirectory : directory containing the DB and the lock file directory
- * [defaults to $wgSQLiteDataDir]
* - dbFilePath : use this to force the path of the DB file
* - trxMode : one of (deferred, immediate, exclusive)
* @param array $p
*/
- function __construct( array $p ) {
+ public function __construct( array $p ) {
if ( isset( $p['dbFilePath'] ) ) {
$this->dbPath = $p['dbFilePath'];
- $lockDomain = md5( $this->dbPath );
- // Use "X" for things like X.sqlite and ":memory:" for RAM-only DBs
- if ( !isset( $p['dbname'] ) || !strlen( $p['dbname'] ) ) {
- $p['dbname'] = preg_replace( '/\.sqlite\d?$/', '', basename( $this->dbPath ) );
+ if ( !strlen( $p['dbname'] ) ) {
+ $p['dbname'] = self::generateDatabaseName( $this->dbPath );
}
} elseif ( isset( $p['dbDirectory'] ) ) {
$this->dbDir = $p['dbDirectory'];
- $lockDomain = $p['dbname'];
- } else {
- throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
}
- $this->trxMode = isset( $p['trxMode'] ) ? strtoupper( $p['trxMode'] ) : null;
- if ( $this->trxMode &&
- !in_array( $this->trxMode, [ 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ] )
- ) {
- $this->trxMode = null;
- $this->queryLogger->warning( "Invalid SQLite transaction mode provided." );
- }
+ // Set a dummy user to make initConnection() trigger open()
+ parent::__construct( [ 'user' => '@' ] + $p );
- if ( $this->hasProcessMemoryPath() ) {
- $this->lockMgr = new NullLockManager( [ 'domain' => $lockDomain ] );
- } else {
+ $this->trxMode = strtoupper( $p['trxMode'] ?? '' );
+
+ $lockDirectory = $this->getLockFileDirectory();
+ if ( $lockDirectory !== null ) {
$this->lockMgr = new FSLockManager( [
- 'domain' => $lockDomain,
- 'lockDirectory' => is_string( $this->dbDir )
- ? "{$this->dbDir}/locks"
- : dirname( $this->dbPath ) . "/locks"
+ 'domain' => $this->getDomainID(),
+ 'lockDirectory' => $lockDirectory
] );
+ } else {
+ $this->lockMgr = new NullLockManager( [ 'domain' => $this->getDomainID() ] );
}
-
- parent::__construct( $p );
}
protected static function getAttributes() {
return $db;
}
- protected function doInitConnection() {
- if ( $this->dbPath !== null ) {
- // Standalone .sqlite file mode.
- $this->openFile(
- $this->dbPath,
- $this->connectionParams['dbname'],
- $this->connectionParams['tablePrefix']
- );
- } elseif ( $this->dbDir !== null ) {
- // Stock wiki mode using standard file names per DB
- if ( strlen( $this->connectionParams['dbname'] ) ) {
- $this->open(
- $this->connectionParams['host'],
- $this->connectionParams['user'],
- $this->connectionParams['password'],
- $this->connectionParams['dbname'],
- $this->connectionParams['schema'],
- $this->connectionParams['tablePrefix']
- );
- } else {
- // Caller will manually call open() later?
- $this->connLogger->debug( __METHOD__ . ': no database opened.' );
- }
- } else {
- throw new InvalidArgumentException( "Need 'dbDirectory' or 'dbFilePath' parameter." );
- }
- }
-
/**
* @return string
*/
- function getType() {
+ public function getType() {
return 'sqlite';
}
*
* @return bool
*/
- function implicitGroupby() {
+ public function implicitGroupby() {
return false;
}
protected function open( $server, $user, $pass, $dbName, $schema, $tablePrefix ) {
$this->close();
+ // Note that for SQLite, $server, $user, and $pass are ignored
+
if ( $schema !== null ) {
- throw new DBExpectedError( $this, __CLASS__ . ": domain schemas are not supported." );
+ throw new DBExpectedError( $this, __CLASS__ . ": cannot use schemas ('$schema')" );
}
- // Only $dbName is used, the other parameters are irrelevant for SQLite databases
- $this->openFile( self::generateFileName( $this->dbDir, $dbName ), $dbName, $tablePrefix );
- }
+ if ( $this->dbPath !== null ) {
+ $path = $this->dbPath;
+ } elseif ( $this->dbDir !== null ) {
+ $path = self::generateFileName( $this->dbDir, $dbName );
+ } else {
+ throw new DBExpectedError( $this, __CLASS__ . ": DB path or directory required" );
+ }
- /**
- * Opens a database file
- *
- * @param string $fileName
- * @param string $dbName
- * @param string $tablePrefix
- * @throws DBConnectionError
- */
- protected function openFile( $fileName, $dbName, $tablePrefix ) {
- if ( !$this->hasProcessMemoryPath() && !is_readable( $fileName ) ) {
+ if ( !in_array( $this->trxMode, [ '', 'DEFERRED', 'IMMEDIATE', 'EXCLUSIVE' ], true ) ) {
+ throw new DBExpectedError(
+ $this,
+ __CLASS__ . ": invalid transaction mode '{$this->trxMode}'"
+ );
+ }
+
+ if ( !self::isProcessMemoryPath( $path ) && !is_readable( $path ) ) {
$error = "SQLite database file not readable";
$this->connLogger->error(
"Error connecting to {db_server}: {error}",
throw new DBConnectionError( $this, $error );
}
- $this->dbPath = $fileName;
try {
- $this->conn = new PDO(
- "sqlite:$fileName",
+ $conn = new PDO(
+ "sqlite:$path",
'',
'',
[ PDO::ATTR_PERSISTENT => (bool)( $this->flags & self::DBO_PERSISTENT ) ]
);
- $error = 'unknown error';
+ // Set error codes only, don't raise exceptions
+ $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
} catch ( PDOException $e ) {
$error = $e->getMessage();
- }
-
- if ( !$this->conn ) {
$this->connLogger->error(
"Error connecting to {db_server}: {error}",
$this->getLogContext( [ 'method' => __METHOD__, 'error' => $error ] )
throw new DBConnectionError( $this, $error );
}
- try {
- // Set error codes only, don't raise exceptions
- $this->conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_SILENT );
-
- $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
+ $this->conn = $conn;
+ $this->currentDomain = new DatabaseDomain( $dbName, null, $tablePrefix );
+ try {
$flags = self::QUERY_IGNORE_DBO_TRX | self::QUERY_NO_RETRY;
// Enforce LIKE to be case sensitive, just like MySQL
$this->query( 'PRAGMA case_sensitive_like = 1', __METHOD__, $flags );
// Apply an optimizations or requirements regarding fsync() usage
$sync = $this->connectionVariables['synchronous'] ?? null;
if ( in_array( $sync, [ 'EXTRA', 'FULL', 'NORMAL', 'OFF' ], true ) ) {
- $this->query( "PRAGMA synchronous = $sync", __METHOD__ );
+ $this->query( "PRAGMA synchronous = $sync", __METHOD__, $flags );
}
} catch ( Exception $e ) {
// Connection was not fully initialized and is not safe for use
}
/**
- * @return string SQLite DB file path
+ * @return string|null SQLite DB file path
+ * @throws DBUnexpectedError
* @since 1.25
*/
public function getDbFilePath() {
- return $this->dbPath;
+ return $this->dbPath ?? self::generateFileName( $this->dbDir, $this->getDBname() );
+ }
+
+ /**
+ * @return string|null Lock file directory
+ */
+ public function getLockFileDirectory() {
+ if ( $this->dbPath !== null && !self::isProcessMemoryPath( $this->dbPath ) ) {
+ return dirname( $this->dbPath ) . '/locks';
+ } elseif ( $this->dbDir !== null && !self::isProcessMemoryPath( $this->dbDir ) ) {
+ return $this->dbDir . '/locks';
+ }
+
+ return null;
}
/**
/**
* Generates a database file name. Explicitly public for installer.
* @param string $dir Directory where database resides
- * @param string $dbName Database name
+ * @param string|bool $dbName Database name (or false from Database::factory, validated here)
* @return string
+ * @throws DBUnexpectedError
*/
public static function generateFileName( $dir, $dbName ) {
+ if ( $dir == '' ) {
+ throw new DBUnexpectedError( null, __CLASS__ . ": no DB directory specified" );
+ } elseif ( self::isProcessMemoryPath( $dir ) ) {
+ throw new DBUnexpectedError(
+ null,
+ __CLASS__ . ": cannot use process memory directory '$dir'"
+ );
+ } elseif ( !strlen( $dbName ) ) {
+ throw new DBUnexpectedError( null, __CLASS__ . ": no DB name specified" );
+ }
+
return "$dir/$dbName.sqlite";
}
+ /**
+ * @param string $path
+ * @return string
+ */
+ private static function generateDatabaseName( $path ) {
+ if ( preg_match( '/^(:memory:$|file::memory:)/', $path ) ) {
+ // E.g. "file::memory:?cache=shared" => ":memory":
+ return ':memory:';
+ } elseif ( preg_match( '/^file::([^?]+)\?mode=memory(&|$)/', $path, $m ) ) {
+ // E.g. "file:memdb1?mode=memory" => ":memdb1:"
+ return ":{$m[1]}:";
+ } else {
+ // E.g. "/home/.../some_db.sqlite3" => "some_db"
+ return preg_replace( '/\.sqlite\d?$/', '', basename( $path ) );
+ }
+ }
+
+ /**
+ * @param string $path
+ * @return bool
+ */
+ private static function isProcessMemoryPath( $path ) {
+ return preg_match( '/^(:memory:$|file:(:memory:|[^?]+\?mode=memory(&|$)))/', $path );
+ }
+
/**
* Check if the searchindext table is FTS enabled.
* @return bool False if not enabled.
* @param string $fname Calling function name
* @return IResultWrapper
*/
- function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
- if ( !$file ) {
- $file = self::generateFileName( $this->dbDir, $name );
- }
- $file = $this->addQuotes( $file );
+ public function attachDatabase( $name, $file = false, $fname = __METHOD__ ) {
+ $file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
+ $encFile = $this->addQuotes( $file );
- return $this->query( "ATTACH DATABASE $file AS $name", $fname );
+ return $this->query( "ATTACH DATABASE $encFile AS $name", $fname );
}
protected function isWriteQuery( $sql ) {
}
public function serverIsReadOnly() {
- return ( !$this->hasProcessMemoryPath() && !is_writable( $this->dbPath ) );
- }
+ $path = $this->getDbFilePath();
- /**
- * @return bool
- */
- private function hasProcessMemoryPath() {
- return ( strpos( $this->dbPath, ':memory:' ) === 0 );
+ return ( !self::isProcessMemoryPath( $path ) && !is_writable( $path ) );
}
/**
}
protected function doBegin( $fname = '' ) {
- if ( $this->trxMode ) {
+ if ( $this->trxMode != '' ) {
$this->query( "BEGIN {$this->trxMode}", $fname );
} else {
$this->query( 'BEGIN', $fname );
}
public function lock( $lockName, $method, $timeout = 5 ) {
- // Give better error message for permission problems than just returning false
+ $status = $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout );
if (
- !is_dir( "{$this->dbDir}/locks" ) &&
- ( !is_writable( $this->dbDir ) || !mkdir( "{$this->dbDir}/locks" ) )
+ $this->lockMgr instanceof FSLockManager &&
+ $status->hasMessage( 'lockmanager-fail-openlock' )
) {
- throw new DBError( $this, "Cannot create directory \"{$this->dbDir}/locks\"." );
+ throw new DBError( $this, "Cannot create directory \"{$this->getLockFileDirectory()}\"" );
}
- return $this->lockMgr->lock( [ $lockName ], LockManager::LOCK_EX, $timeout )->isOK();
+ return $status->isOK();
}
public function unlock( $lockName, $method ) {
return $values;
}
- public function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
+ protected function doSetMulti( array $data, $exptime = 0, $flags = 0 ) {
return $this->modifyMulti( $data, $exptime, $flags, self::$OP_SET );
}
return (bool)$db->affectedRows();
}
- public function doDeleteMulti( array $keys, $flags = 0 ) {
+ protected function doDeleteMulti( array $keys, $flags = 0 ) {
return $this->modifyMulti(
array_fill_keys( $keys, null ),
0,
return $ok;
}
- public function changeTTLMulti( array $keys, $exptime, $flags = 0 ) {
+ protected function doChangeTTLMulti( array $keys, $exptime, $flags = 0 ) {
return $this->modifyMulti(
array_fill_keys( $keys, null ),
$exptime,
public function deleteObjectsExpiringBefore(
$timestamp,
- callable $progressCallback = null,
+ callable $progress = null,
$limit = INF
) {
/** @noinspection PhpUnusedLocalVariableInspection */
$this->deleteServerObjectsExpiringBefore(
$db,
$timestamp,
- $progressCallback,
+ $progress,
$limit,
$numServersDone,
$keysDeletedCount
* - defer: one of the DeferredUpdates constants, or false to run immediately (default: false).
* Note that even when this is set to false, some updates might still get deferred (as
* some update might directly add child updates to DeferredUpdates).
+ * - known-revision-output: a combined canonical ParserOutput for the revision, perhaps
+ * from some cache. The caller is responsible for ensuring that the ParserOutput indeed
+ * matched the $rev and $options. This mechanism is intended as a temporary stop-gap,
+ * for the time until caches have been changed to store RenderedRevision states instead
+ * of ParserOutput objects. (default: null) (since 1.33)
* @since 1.32
*/
public function doSecondaryDataUpdates( array $options = [] ) {
if ( isset( $info['object'] ) ) {
return false;
}
- return (
+ return !isset( $info['factory'] ) && (
// The implied default for 'class' is ResourceLoaderFileModule
!isset( $info['class'] ) ||
// Explicit default
'image' => $this->getName(),
'variant' => $variant,
'format' => $format,
- 'lang' => $context->getLanguage(),
- 'skin' => $context->getSkin(),
- 'version' => $context->getVersion(),
];
+ if ( $this->varyOnLanguage() ) {
+ $query['lang'] = $context->getLanguage();
+ }
+ // The following parameters are at the end to keep the original order of the parameters.
+ $query['skin'] = $context->getSkin();
+ $query['version'] = $context->getVersion();
return wfAppendQuery( $script, $query );
}
return $png ?: false;
}
}
+
+ /**
+ * Check if the image depends on the language.
+ *
+ * @return bool
+ */
+ private function varyOnLanguage() {
+ return is_array( $this->descriptor ) && (
+ isset( $this->descriptor['ltr'] ) ||
+ isset( $this->descriptor['rtl'] ) ||
+ isset( $this->descriptor['lang'] ) );
+ }
}
/** @var bool */
protected $warn = true;
- /** @var SessionManager|null */
+ /** @var SessionManagerInterface|null */
protected $manager;
/** @var BagOStuff|null */
/** @var array Track original session fields for later modification check */
protected $sessionFieldCache = [];
- protected function __construct( SessionManager $manager ) {
+ protected function __construct( SessionManagerInterface $manager ) {
$this->setEnableFlags(
\RequestContext::getMain()->getConfig()->get( 'PHPSessionHandling' )
);
/**
* Install a session handler for the current web request
- * @param SessionManager $manager
+ * @param SessionManagerInterface $manager
*/
- public static function install( SessionManager $manager ) {
+ public static function install( SessionManagerInterface $manager ) {
if ( self::$instance ) {
$manager->setupPHPSessionHandler( self::$instance );
return;
/**
* Set the manager, store, and logger
* @private Use self::install().
- * @param SessionManager $manager
+ * @param SessionManagerInterface $manager
* @param BagOStuff $store
* @param LoggerInterface $logger
*/
public function setManager(
- SessionManager $manager, BagOStuff $store, LoggerInterface $logger
+ SessionManagerInterface $manager, BagOStuff $store, LoggerInterface $logger
) {
if ( $this->manager !== $manager ) {
// Close any existing session before we change stores
return $out;
}
+ public function execute( $par ) {
+ $this->addHelpLink( 'Help:Redirects' );
+ parent::execute( $par );
+ }
+
/**
* Cache page content model for performance
*
$this->setHeaders();
$this->outputHeader();
$this->getOutput()->addModuleStyles( 'mediawiki.special' );
+ $this->addHelpLink( 'Help:Diff' );
$form = HTMLForm::factory( 'ooui', [
'Page1' => [
$this->setHeaders();
$this->outputHeader();
$this->checkPermissions();
+ $this->addHelpLink( 'Help:User contributions' );
$out = $this->getOutput();
$out->setPageTitle( $this->msg( 'deletedcontributions-title' ) );
return ( "{$linkA} {$edit} {$arr} {$linkB} {$arr} {$linkC}" );
}
+ public function execute( $par ) {
+ $this->addHelpLink( 'Help:Redirects' );
+ parent::execute( $par );
+ }
+
/**
* Cache page content model and gender distinction for performance
*
$out = $this->getOutput();
$out->addModuleStyles( 'mediawiki.special' );
+ $this->addHelpLink( 'Help:User_rights_and_groups' );
$out->wrapWikiMsg( "<div class=\"mw-listgrouprights-key\">\n$1\n</div>", 'listgrouprights-key' );
}
}
+ public function execute( $par ) {
+ $this->addHelpLink( 'Help:Redirects' );
+ parent::execute( $par );
+ }
+
protected function getGroupName() {
return 'pages';
}
$this->setHeaders();
$this->outputHeader();
$this->getOutput()->addModuleStyles( 'mediawiki.special' );
+ $this->addHelpLink( 'Help:Protected_pages' );
$request = $this->getRequest();
$type = $request->getVal( $this->IdType );
function execute( $par ) {
$this->setHeaders();
$this->outputHeader();
+ $this->addHelpLink( 'Help:Protected_pages' );
$request = $this->getRequest();
$type = $request->getVal( $this->IdType );
function execute( $par ) {
$this->setHeaders();
$this->outputHeader();
+ $this->addHelpLink( 'Manual:Tags' );
$request = $this->getRequest();
switch ( $par ) {
class UncategorizedImagesPage extends ImageQueryPage {
function __construct( $name = 'Uncategorizedimages' ) {
parent::__construct( $name );
+ $this->addHelpLink( 'Help:Categories' );
}
function sortDescending() {
function __construct( $name = 'Uncategorizedpages' ) {
parent::__construct( $name );
+ $this->addHelpLink( 'Help:Categories' );
}
function sortDescending() {
$this->setHeaders();
$this->outputHeader();
+ $this->addHelpLink( 'Help:Deletion_and_undeletion' );
$this->loadRequest( $par );
$this->checkPermissions(); // Needs to be after mTargetObj is set
}
// Purge old, expired memberships from the DB
- JobQueueGroup::singleton()->push( new UserGroupExpiryJob() );
+ $hasExpiredRow = $dbw->selectField(
+ 'user_groups',
+ '1',
+ [ 'ug_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
+ __METHOD__
+ );
+ if ( $hasExpiredRow ) {
+ JobQueueGroup::singleton()->lazyPush( new UserGroupExpiryJob() );
+ }
// Check that the values make sense
if ( $this->group === null ) {
$ticket = $lbFactory->getEmptyTransactionTicket( __METHOD__ );
$dbw = $services->getDBLoadBalancer()->getConnection( DB_MASTER );
- $lockKey = $dbw->getDomainID() . ':usergroups-prune'; // specific to this wiki
+ $lockKey = "{$dbw->getDomainID()}:UserGroupMembership:purge"; // per-wiki
$scopedLock = $dbw->getScopedLockAndFlush( $lockKey, __METHOD__, 0 );
if ( !$scopedLock ) {
return false; // already running
}
$oldRev = $this->revisionLookup->getRevisionById( $oldid );
+ if ( !$oldRev ) {
+ // Oldid given but does not exist (probably deleted)
+ return false;
+ }
+
$nextRev = $this->revisionLookup->getNextRevision( $oldRev );
if ( !$nextRev ) {
// Oldid given and is the latest revision for this title; clear the timestamp.
"delete-legend": "Выдаліць",
"historywarning": "<strong>Папярэджаньне</strong>: старонка, якую Вы зьбіраецеся выдаліць, мае гісторыю з $1 {{PLURAL:$1|вэрсіі|вэрсіяў|вэрсіяў}}:",
"historyaction-submit": "Паказаць вэрсіі",
- "confirmdeletetext": "Ð\97аÑ\80аз Ð\92Ñ\8b вÑ\8bдалÑ\96Ñ\86е Ñ\81Ñ\82аÑ\80онкÑ\83 Ñ\80азам з Ñ\83Ñ\81Ñ\91й гÑ\96Ñ\81Ñ\82оÑ\80Ñ\8bÑ\8fй зÑ\8cменаÑ\9e.\nÐ\9aалÑ\96 лаÑ\81ка, паÑ\86Ñ\8cвеÑ\80дзÑ\96Ñ\86е, Ñ\88Ñ\82о Ð\92Ñ\8b зÑ\8cбÑ\96Ñ\80аеÑ\86еÑ\81Ñ\8f гÑ\8dÑ\82а зÑ\80абÑ\96Ñ\86Ñ\8c Ñ\96 Ñ\88Ñ\82о Ð\92ы разумееце ўсе наступствы, а таксама робіце гэта ў адпаведнасьці з [[{{MediaWiki:Policy-url}}|правіламі]].",
+ "confirmdeletetext": "Ð\97аÑ\80аз вÑ\8b вÑ\8bдалÑ\96Ñ\86е Ñ\81Ñ\82аÑ\80онкÑ\83 Ñ\80азам з Ñ\83Ñ\81Ñ\91й гÑ\96Ñ\81Ñ\82оÑ\80Ñ\8bÑ\8fй зÑ\8cменаÑ\9e.\nÐ\9aалÑ\96 лаÑ\81ка, паÑ\86Ñ\8cвеÑ\80дзÑ\96Ñ\86е, Ñ\88Ñ\82о вÑ\8b зÑ\8cбÑ\96Ñ\80аеÑ\86еÑ\81Ñ\8f гÑ\8dÑ\82а зÑ\80абÑ\96Ñ\86Ñ\8c Ñ\96 Ñ\88Ñ\82о вы разумееце ўсе наступствы, а таксама робіце гэта ў адпаведнасьці з [[{{MediaWiki:Policy-url}}|правіламі]].",
"actioncomplete": "Дзеяньне выкананае",
"actionfailed": "Дзеяньне ня выкананае",
"deletedtext": "«$1» была выдаленая.\nЗапісы пра выдаленыя старонкі зьмяшчаюцца ў $2.",
"autoblockedtext": "Deine IP-Adresse wurde automatisch gesperrt, da sie von einem anderen Benutzer genutzt wurde, der von $1 gesperrt wurde.\nAls Grund wurde angegeben:\n\n:''$2''\n\n* Beginn der Sperre: $8\n* Ende der Sperre: $6\n* Sperre betrifft: $7\n\nDu kannst $1 oder einen der anderen [[{{MediaWiki:Grouppage-sysop}}|Administratoren]] kontaktieren, um über die Sperre zu diskutieren.\n\nDu kannst die „{{int:emailuser}}“-Funktion nicht nutzen, solange keine gültige E-Mail-Adresse in deinen [[Special:Preferences|Benutzerkonto-Einstellungen]] eingetragen ist oder diese Funktion für dich gesperrt wurde.\n\nDeine aktuelle IP-Adresse ist $3, und die Sperr-ID ist $5.\nBitte füge alle Informationen jeder Anfrage hinzu, die du stellst.",
"systemblockedtext": "Dein Benutzername oder deine IP-Adresse wurde von MediaWiki automatisch gesperrt.\nDer angegebene Grund ist:\n\n:<em>$2</em>\n\n* Beginn der Sperre: $8\n* Ablauf der Sperre: $6\n* Sperre betrifft: $7\n\nDeine aktuelle IP-Adresse ist $3.\nBitte gib alle oben stehenden Details in jeder Anfrage an.",
"blockednoreason": "keine Begründung angegeben",
+ "blockedtext-composite": "<strong>Dein Benutzername oder deine IP-Adresse wurde gesperrt.</strong>\n\nDer Angegebene Grund ist:\n\n:<em>$2</em>\n\n* Beginn der Sperre: $8\n* Ablauf der längsten Sperre: $6\n\nDeine aktuelle IP-Adresse ist $3.\nBitte gib alle oben stehenden Details in jeder Anfrage an.",
+ "blockedtext-composite-reason": "Es gibt mehrere Sperren gegen dein Benutzerkonto und/oder deine IP-Adresse",
"whitelistedittext": "Du musst dich $1, um Seiten bearbeiten zu können.",
"confirmedittext": "Du musst deine E-Mail-Adresse erst bestätigen, bevor du Bearbeitungen durchführen kannst. Bitte ergänze und bestätige deine E-Mail in den [[Special:Preferences|Einstellungen]].",
"nosuchsectiontitle": "Abschnitt nicht gefunden",
"mw-widgets-abandonedit-title": "Bist du sicher?",
"mw-widgets-copytextlayout-copy": "Kopieren",
"mw-widgets-copytextlayout-copy-fail": "Der Text konnte nicht in die Zwischenablage kopiert werden.",
+ "mw-widgets-copytextlayout-copy-success": "Text in die Zwischenablage kopiert.",
"mw-widgets-dateinput-no-date": "Kein Datum ausgewählt",
"mw-widgets-dateinput-placeholder-day": "JJJJ-MM-TT",
"mw-widgets-dateinput-placeholder-month": "JJJJ-MM",
"restrictionsfield-help": "Eine IP-Adresse oder ein CIDR-Bereich pro Zeile. Um alles zu aktivieren, verwende:\n<pre>\n0.0.0.0/0\n::/0\n</pre>",
"edit-error-short": "Fehler: $1",
"edit-error-long": "Fehler:\n\n$1",
+ "specialmute": "Stumm",
+ "specialmute-success": "Deine Stummschaltungseinstellungen wurden aktualisiert. Schau dir alle stummgeschalteten Benutzer in [[Special:Preferences|deinen Einstellungen]] an.",
+ "specialmute-submit": "Bestätigen",
+ "specialmute-label-mute-email": "E-Mails von diesem Benutzer stummschalten",
+ "specialmute-header": "Bitte wähle deine Stummschaltungseinstellungen für {{BIDI:[[User:$1]]}}.",
+ "specialmute-error-invalid-user": "Der gesuchte Benutzername konnte nicht gefunden werden.",
+ "specialmute-error-email-blacklist-disabled": "Das Stummschalten von E-Mails von Benutzern ist nicht aktiviert.",
+ "specialmute-error-email-preferences": "Du musst deine E-Mail Adresse bestätigen bevor du einen Benutzer bestätigen kannst. Du kannst dies [[Special:Preferences|in deinen Einstellungen]] tun.",
+ "specialmute-email-footer": "Um deine E-Mail Einstellungen für {{BIDI:$2}} zu verwalten besuche bitte $1.",
+ "specialmute-login-required": "Bitte melde dich an um deine Stummschaltungseinstellungen zu ändern.",
"revid": "Version $1",
"pageid": "Seitenkennung $1",
"interfaceadmin-info": "$1\n\nBerechtigungen für das Bearbeiten von wikiweiten CSS/JS/JSON-Dateien wurden kürzlich vom Recht <code>editinterface</code> getrennt. Falls du nicht verstehst, warum du diesen Fehler erhältst, siehe [[mw:MediaWiki_1.32/interface-admin]].",
"logentry-pagelang-pagelang": "$1 {{GENDER:$2|promijenio|promijenila}} je jezik stranice $3 iz $4 u $5.",
"mediastatistics": "Statistika datoteka",
"mediastatistics-summary": "Slijede statistike postavljenih datoteka koje pokazuju zadnju inačicu datoteke. Starije ili izbrisane inačice nisu prikazane.",
- "mediastatistics-nfiles": "$1 ($2%)",
+ "mediastatistics-nfiles": "$1 ($2 %)",
"mediastatistics-nbytes": "{{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2; $3 %)",
"mediastatistics-bytespertype": "Ukupna veličina datoteka za ovaj odlomak: {{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2; $3%).",
"mediastatistics-allbytes": "Ukupna veličina svih datoteka: {{PLURAL:$1|$1 bajt|$1 bajta|$1 bajtova}} ($2).",
"specialmute-success": "Vaše postavke utišavanja su uspješno ažurirane. Vidite sve utišane korisnike ovdje: [[Special:Preferences]].",
"specialmute-submit": "Potvrdi",
"specialmute-error-invalid-user": "Korisničko ime koje ste tražili nije moguće pronaći.",
- "specialmute-error-email-preferences": "Morate potvrditi svoju email adresu prije nego što možete utišati ovoga korisnika. To možete učiniti putem [[Special:Preferences]].",
- "specialmute-login-required": "Molimo Vas prijavite se da biste promijenili postavke.",
+ "specialmute-error-email-preferences": "Morate potvrditi svoju adresu e-pošte prije nego što možete utišati ovoga korisnika. To možete učiniti putem [[Special:Preferences]].",
+ "specialmute-login-required": "Molimo Vas, prijavite se da biste promijenili postavke.",
"gotointerwiki": "Napuštate projekt {{SITENAME}}",
"gotointerwiki-invalid": "Navedeni naslov nije valjan.",
"gotointerwiki-external": "Napuštate projekt {{SITENAME}} da biste posjetili zasebno mrežno mjesto [[$2]].\n\n<strong>[$1 Nastavljate na $1]</strong>",
"history": "Riwayat halaman",
"history_short": "Versi terdahulu",
"history_small": "riwayat",
- "updatedmarker": "diubah sejak kunjungan terakhir saya",
+ "updatedmarker": "berubah sejak kunjungan terakhir saya",
"printableversion": "Versi cetak",
"permalink": "Pranala permanen",
"print": "Cetak",
"autoblockedtext": "Alamat IP Anda telah terblokir secara otomatis karena digunakan oleh pengguna lain, yang diblokir oleh $1. Pemblokiran dilakukan dengan alasan:\n\n:<em>$2</em>\n\n* Diblokir sejak: $8\n* Blokir kedaluwarsa pada: $6\n* Sasaran pemblokiran: $7\n\nAnda dapat menghubungi $1 atau [[{{MediaWiki:Grouppage-sysop}}|pengurus]] lainnya untuk membicarakan pemblokiran ini.\n\nAnda tidak dapat menggunakan fitur \"{{int:emailuser}}\" kecuali Anda telah memasukkan alamat surel yang sah di [[Special:Preferences|preferensi akun]] Anda dan Anda tidak diblokir untuk menggunakannya.\n\nAlamat IP Anda saat ini adalah $3, dan ID pemblokiran adalah #$5.\nTolong sertakan informasi-informasi ini dalam setiap pertanyaan Anda.",
"systemblockedtext": "Nama pengguna atau alamat IP Anda telah diblokir secara otomatis oleh MediaWiki.\nAlasan yang diberikan adalah:\n\n:<em>$2</em>\n\n* Diblokir sejak: $8\n* Blokir kedaluwarsa pada: $6\n* Sasaran pemblokiran: $7\n\nAlamat IP Anda saat ini adalah $3\nMohon sertakan semua perincian di atas dalam setiap pertanyaan yang Anda ajukan.",
"blockednoreason": "tidak ada alasan yang diberikan",
+ "blockedtext-composite-reason": "Ada pemblokiran berganda terhadap akun Anda dan/atau alamat IP Anda.",
"whitelistedittext": "Anda harus $1 untuk dapat menyunting halaman.",
"confirmedittext": "Anda harus mengkonfirmasikan dulu alamat surel Anda sebelum menyunting halaman.\nHarap masukkan dan validasikan alamat surel Anda melalui [[Special:Preferences|halaman preferensi pengguna]] Anda.",
"nosuchsectiontitle": "Bagian tidak ditemukan",
"mw-widgets-abandonedit-discard": "Buang suntingan",
"mw-widgets-abandonedit-keep": "Lanjutkan penyuntingan",
"mw-widgets-abandonedit-title": "Apakah Anda yakin?",
+ "mw-widgets-copytextlayout-copy": "Salin",
+ "mw-widgets-copytextlayout-copy-fail": "Gagal menyalin ke papan klip.",
+ "mw-widgets-copytextlayout-copy-success": "Salin ke papan klip.",
"mw-widgets-dateinput-no-date": "Tanggal tidak ada yang terpilih",
"mw-widgets-dateinput-placeholder-day": "TTTT-BB-HH",
"mw-widgets-dateinput-placeholder-month": "TTTT-BB",
"restrictionsfield-help": "Satu alamat IP atau rentang CIDR per baris. Untuk mengaktifkan semuanya, gunakan:\n<pre>0.0.0.0/0\n::/0</pre>",
"edit-error-short": "Galat: $1",
"edit-error-long": "Galat:\n\n$1",
+ "specialmute": "Diam",
+ "specialmute-submit": "Konfirmasi",
"revid": "revisi $1",
"pageid": "ID halaman $1",
"rawhtml-notallowed": "Tag <html> tidak dapat digunakan di luar halaman normal.",
"log-action-filter-suppress-block": "Сокрытие пользователя через блокировки",
"log-action-filter-suppress-reblock": "Сокрытие пользователя через повторное блокирование",
"log-action-filter-upload-upload": "Новая загрузка",
- "log-action-filter-upload-overwrite": "Ð\9fовÑ\82оÑ\80но загÑ\80Ñ\83зиÑ\82Ñ\8c",
- "log-action-filter-upload-revert": "Ð\9eÑ\82каÑ\82иÑ\82Ñ\8c",
+ "log-action-filter-upload-overwrite": "Ð\9fеÑ\80езапиÑ\81Ñ\8c Ñ\84айла",
+ "log-action-filter-upload-revert": "Ð\92озвÑ\80аÑ\82 Ñ\81Ñ\82аÑ\80ой веÑ\80Ñ\81ии Ñ\84айла",
"authmanager-authn-not-in-progress": "Проверка подлинности не выполняется или данные сессии были утеряны. Пожалуйста, начните снова с самого начала.",
"authmanager-authn-no-primary": "Предоставленные учётные данные не могут быть проверены на подлинность.",
"authmanager-authn-no-local-user": "Предоставленные учётные данные не связаны ни с одним участником этой вики.",
"revertmerge": "растави",
"mergelogpagetext": "Испод се налази списак најновијих обједињавања историја једне странице у другу.",
"history-title": "Историја измена странице „$1”",
- "difference-title": "Разлика између измена на страници „$1”",
+ "difference-title": "$1 — разлика између измена",
"difference-title-multipage": "Разлика између страница „$1“ и „$2“",
"difference-multipage": "(разлике између страница)",
"lineno": "Ред $1:",
"svg-long-desc": "SVG датотека, номинално $1 × $2 пиксела, величина: $3",
"svg-long-desc-animated": "Анимирана SVG датотека, номинално: $1 × $2 пиксела, величина: $3",
"svg-long-error": "Неважећа SVG датотека: $1",
- "show-big-image": "Ð\9fÑ\80вобиÑ\82на датотека",
+ "show-big-image": "Ð\9eÑ\80игинална датотека",
"show-big-image-preview": "Величина овог приказа: $1.",
"show-big-image-preview-differ": "Величина $3 прегледа за ову $2 датотеку је $1.",
"show-big-image-other": "$2 {{PLURAL:$2|друга резолуција|друге резолуције|других резолуција}}: $1.",
}
if ( count( $batchPaths ) ) { // left-overs
$this->copyFileBatch( array_keys( $batchPaths ), $backendRel, $src, $dst );
- $batchPaths = []; // done
}
$this->output( "\tCopied $count file(s).\n" );
}
if ( count( $batchPaths ) ) { // left-overs
$this->delFileBatch( array_keys( $batchPaths ), $backendRel, $dst );
- $batchPaths = []; // done
}
$this->output( "\tDeleted $count file(s).\n" );
$ops = [];
$fsFiles = [];
$copiedRel = []; // for output message
- $wikiId = $src->getWikiId();
+ $domainId = $src->getDomainId();
// Download the batch of source files into backend cache...
if ( $this->hasOption( 'missingonly' ) ) {
$srcPath = $src->getRootStoragePath() . "/$backendRel/$srcPathRel";
$dstPath = $dst->getRootStoragePath() . "/$backendRel/$srcPathRel";
if ( $this->hasOption( 'utf8only' ) && !mb_check_encoding( $srcPath, 'UTF-8' ) ) {
- $this->error( "$wikiId: Detected illegal (non-UTF8) path for $srcPath." );
+ $this->error( "$domainId: Detected illegal (non-UTF8) path for $srcPath." );
continue;
} elseif ( !$this->hasOption( 'missingonly' )
&& $this->filesAreSame( $src, $dst, $srcPath, $dstPath )
if ( !$fsFile ) {
$src->clearCache( [ $srcPath ] );
if ( $src->fileExists( [ 'src' => $srcPath, 'latest' => 1 ] ) === false ) {
- $this->error( "$wikiId: File '$srcPath' was listed but does not exist." );
+ $this->error( "$domainId: File '$srcPath' was listed but does not exist." );
} else {
- $this->error( "$wikiId: Could not get local copy of $srcPath." );
+ $this->error( "$domainId: Could not get local copy of $srcPath." );
}
continue;
} elseif ( !$fsFile->exists() ) {
// FSFileBackends just return the path for getLocalReference() and paths with
// illegal slashes may get normalized to a different path. This can cause the
// local reference to not exist...skip these broken files.
- $this->error( "$wikiId: Detected possible illegal path for $srcPath." );
+ $this->error( "$domainId: Detected possible illegal path for $srcPath." );
continue;
}
$fsFiles[] = $fsFile; // keep TempFSFile objects alive as needed
// Note: prepare() is usually fast for key/value backends
$status = $dst->prepare( [ 'dir' => dirname( $dstPath ), 'bypassReadOnly' => 1 ] );
if ( !$status->isOK() ) {
- $this->error( print_r( $status->getErrorsArray(), true ) );
- $this->fatalError( "$wikiId: Could not copy $srcPath to $dstPath." );
+ $this->error( print_r( Status::wrap( $status )->getWikiText(), true ) );
+ $this->fatalError( "$domainId: Could not copy $srcPath to $dstPath." );
}
$ops[] = [ 'op' => 'store',
'src' => $fsFile->getPath(), 'dst' => $dstPath, 'overwrite' => 1 ];
}
$elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
if ( !$status->isOK() ) {
- $this->error( print_r( $status->getErrorsArray(), true ) );
- $this->fatalError( "$wikiId: Could not copy file batch." );
+ $this->error( print_r( Status::wrap( $status )->getWikiText(), true ) );
+ $this->fatalError( "$domainId: Could not copy file batch." );
} elseif ( count( $copiedRel ) ) {
$this->output( "\n\tCopied these file(s) [{$elapsed_ms}ms]:\n\t" .
implode( "\n\t", $copiedRel ) . "\n\n" );
) {
$ops = [];
$deletedRel = []; // for output message
- $wikiId = $dst->getWikiId();
+ $domainId = $dst->getDomainId();
// Determine what files need to be copied over...
foreach ( $dstPathsRel as $dstPathRel ) {
}
$elapsed_ms = floor( ( microtime( true ) - $t_start ) * 1000 );
if ( !$status->isOK() ) {
- $this->error( print_r( $status->getErrorsArray(), true ) );
- $this->fatalError( "$wikiId: Could not delete file batch." );
+ $this->error( print_r( Status::wrap( $status )->getWikiText(), true ) );
+ $this->fatalError( "$domainId: Could not delete file batch." );
} elseif ( count( $deletedRel ) ) {
$this->output( "\n\tDeleted these file(s) [{$elapsed_ms}ms]:\n\t" .
implode( "\n\t", $deletedRel ) . "\n\n" );
if ( !$exists ) {
# Increment site_stats.ss_users
- $ssu = new SiteStatsUpdate( 0, 0, 0, 0, 1 );
+ $ssu = SiteStatsUpdate::factory( [ 'users' => 1 ] );
$ssu->doUpdate();
}
yuml
yyyymmddhhiiss
zcmd
-zerobanner
zerobar
zerobutton
-zeroconfig
zerodontask
-zerodot
zeroinfo
zeronet
-zeroportal
zfile
zhdaemon
zhengzhu
if ( $delete ) {
$this->output( "Updating site stats..." );
$ga = $isGoodArticle ? -1 : 0; // if it was good, decrement that too
- $stats = new SiteStatsUpdate( 0, -$count, $ga, -1 );
+ $stats = SiteStatsUpdate::factory( [
+ 'edits' => -$count,
+ 'articles' => $ga,
+ 'pages' => -1
+ ] );
$stats->doUpdate();
$this->output( "done.\n" );
}
isset( $param['require'] ) ? $param['require'] : false,
isset( $param['withArg'] ) ? $param['withArg'] : false,
isset( $param['shortName'] ) ? $param['shortName'] : false,
- $param['multiOccurrence'] ?? false
+ isset( $param['multiOccurrence'] ) ? $param['multiOccurrence'] : false
);
}
</exclude>
</groups>
<filter>
- <whitelist addUncoveredFilesFromWhitelist="true">
+ <whitelist addUncoveredFilesFromWhitelist="false">
<directory suffix=".php">includes</directory>
<directory suffix=".php">languages</directory>
<directory suffix=".php">maintenance</directory>
+ <directory suffix=".php">extensions</directory>
+ <directory suffix=".php">skins</directory>
<exclude>
<directory suffix=".php">languages/messages</directory>
<file>languages/data/normalize-ar.php</file>
protected $type = ResourceLoaderModule::LOAD_GENERAL;
protected $targets = [ 'phpunit' ];
protected $shouldEmbed = null;
+ protected $mayValidateScript = false;
public function __construct( $options = [] ) {
foreach ( $options as $key => $value ) {
}
public function getScript( ResourceLoaderContext $context ) {
- return $this->validateScriptFile( 'input', $this->script );
+ if ( $this->mayValidateScript ) {
+ // This enables the validation check that replaces invalid
+ // scripts with a warning message.
+ // Based on $wgResourceLoaderValidateJS
+ return $this->validateScriptFile( 'input', $this->script );
+ } else {
+ return $this->script;
+ }
}
public function getStyles( ResourceLoaderContext $context ) {
$job = new RefreshLinksJob( $page->getTitle(), [ 'parseThreshold' => 0 ] );
$job->run();
- // assert state
- $options = ParserOptions::newCanonical( 'canonical' );
- $out = $parserCache->get( $page, $options );
- $this->assertNotFalse( $out, 'parser cache entry' );
-
- $text = $out->getText();
- $this->assertContains( 'MAIN', $text );
- $this->assertContains( 'AUX', $text );
-
$this->assertSelect(
'pagelinks',
'pl_title',
);
}
+ public function testRunForMultiPage() {
+ MediaWikiServices::getInstance()->getSlotRoleRegistry()->defineRoleWithModel(
+ 'aux',
+ CONTENT_MODEL_WIKITEXT
+ );
+
+ $fname = __METHOD__;
+
+ $mainContent = new WikitextContent( 'MAIN [[Kittens]]' );
+ $auxContent = new WikitextContent( 'AUX [[Category:Goats]]' );
+ $page1 = $this->createPage( "$fname-1", [ 'main' => $mainContent, 'aux' => $auxContent ] );
+
+ $mainContent = new WikitextContent( 'MAIN [[Dogs]]' );
+ $auxContent = new WikitextContent( 'AUX [[Category:Hamsters]]' );
+ $page2 = $this->createPage( "$fname-2", [ 'main' => $mainContent, 'aux' => $auxContent ] );
+
+ // clear state
+ $parserCache = MediaWikiServices::getInstance()->getParserCache();
+ $parserCache->deleteOptionsKey( $page1 );
+ $parserCache->deleteOptionsKey( $page2 );
+
+ $this->db->delete( 'pagelinks', '*', __METHOD__ );
+ $this->db->delete( 'categorylinks', '*', __METHOD__ );
+
+ // run job
+ $job = new RefreshLinksJob(
+ Title::newMainPage(),
+ [ 'pages' => [ [ 0, "$fname-1" ], [ 0, "$fname-2" ] ] ]
+ );
+ $job->run();
+
+ $this->assertSelect(
+ 'pagelinks',
+ 'pl_title',
+ [ 'pl_from' => $page1->getId() ],
+ [ [ 'Kittens' ] ]
+ );
+ $this->assertSelect(
+ 'categorylinks',
+ 'cl_to',
+ [ 'cl_from' => $page1->getId() ],
+ [ [ 'Goats' ] ]
+ );
+ $this->assertSelect(
+ 'pagelinks',
+ 'pl_title',
+ [ 'pl_from' => $page2->getId() ],
+ [ [ 'Dogs' ] ]
+ );
+ $this->assertSelect(
+ 'categorylinks',
+ 'cl_to',
+ [ 'cl_from' => $page2->getId() ],
+ [ [ 'Hamsters' ] ]
+ );
+ }
}
);
}
+ /**
+ * @covers MapCacheLRU::has()
+ * @covers MapCacheLRU::get()
+ * @covers MapCacheLRU::set()
+ */
+ function testMissing() {
+ $raw = [ 'a' => 1, 'b' => 2, 'c' => 3 ];
+ $cache = MapCacheLRU::newFromArray( $raw, 3 );
+
+ $this->assertFalse( $cache->has( 'd' ) );
+ $this->assertNull( $cache->get( 'd' ) );
+ $this->assertNull( $cache->get( 'd', 0.0, null ) );
+ $this->assertFalse( $cache->get( 'd', 0.0, false ) );
+ }
+
/**
* @covers MapCacheLRU::has()
* @covers MapCacheLRU::get()
--- /dev/null
+<?php
+
+/**
+ * Holds tests for FakeResultWrapper MediaWiki class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use Wikimedia\Rdbms\FakeResultWrapper;
+
+/**
+ * @group Database
+ * @covers \Wikimedia\Rdbms\FakeResultWrapper
+ */
+class FakeResultWrapperTest extends PHPUnit\Framework\TestCase {
+ public function testIteration() {
+ $res = new FakeResultWrapper( [
+ [ 'colA' => 1, 'colB' => 'a' ],
+ [ 'colA' => 2, 'colB' => 'b' ],
+ (object)[ 'colA' => 3, 'colB' => 'c' ],
+ [ 'colA' => 4, 'colB' => 'd' ],
+ [ 'colA' => 5, 'colB' => 'e' ],
+ (object)[ 'colA' => 6, 'colB' => 'f' ],
+ (object)[ 'colA' => 7, 'colB' => 'g' ],
+ [ 'colA' => 8, 'colB' => 'h' ]
+ ] );
+
+ $expectedRows = [
+ 0 => (object)[ 'colA' => 1, 'colB' => 'a' ],
+ 1 => (object)[ 'colA' => 2, 'colB' => 'b' ],
+ 2 => (object)[ 'colA' => 3, 'colB' => 'c' ],
+ 3 => (object)[ 'colA' => 4, 'colB' => 'd' ],
+ 4 => (object)[ 'colA' => 5, 'colB' => 'e' ],
+ 5 => (object)[ 'colA' => 6, 'colB' => 'f' ],
+ 6 => (object)[ 'colA' => 7, 'colB' => 'g' ],
+ 7 => (object)[ 'colA' => 8, 'colB' => 'h' ]
+ ];
+
+ $this->assertEquals( 8, $res->numRows() );
+
+ $res->seek( 7 );
+ $this->assertEquals( [ 'colA' => 8, 'colB' => 'h' ], $res->fetchRow() );
+ $res->seek( 7 );
+ $this->assertEquals( (object)[ 'colA' => 8, 'colB' => 'h' ], $res->fetchObject() );
+
+ $this->assertEquals( $expectedRows, iterator_to_array( $res, true ) );
+
+ $rows = [];
+ foreach ( $res as $i => $row ) {
+ $rows[$i] = $row;
+ }
+ $this->assertEquals( $expectedRows, $rows );
+ }
+}
--- /dev/null
+<?php
+
+/**
+ * Holds tests for ResultWrapper MediaWiki class.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ */
+
+use Wikimedia\Rdbms\IDatabase;
+use Wikimedia\Rdbms\ResultWrapper;
+
+/**
+ * @group Database
+ * @covers \Wikimedia\Rdbms\ResultWrapper
+ */
+class ResultWrapperTest extends PHPUnit\Framework\TestCase {
+ /**
+ * @return IDatabase
+ * @param array[] $rows
+ */
+ private function getDatabaseMock( array $rows ) {
+ $db = $this->getMockBuilder( IDatabase::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+ $db->method( 'select' )->willReturnCallback(
+ function () use ( $db, $rows ) {
+ return new ResultWrapper( $db, $rows );
+ }
+ );
+ $db->method( 'dataSeek' )->willReturnCallback(
+ function ( ResultWrapper $res, $pos ) use ( $db ) {
+ // Position already set in ResultWrapper
+ }
+ );
+ $db->method( 'fetchRow' )->willReturnCallback(
+ function ( ResultWrapper $res ) use ( $db ) {
+ $row = $res::unwrap( $res )[$res->key()] ?? false;
+
+ return $row;
+ }
+ );
+ $db->method( 'fetchObject' )->willReturnCallback(
+ function ( ResultWrapper $res ) use ( $db ) {
+ $row = $res::unwrap( $res )[$res->key()] ?? false;
+
+ return $row ? (object)$row : false;
+ }
+ );
+ $db->method( 'numRows' )->willReturnCallback(
+ function ( ResultWrapper $res ) use ( $db ) {
+ return count( $res::unwrap( $res ) );
+ }
+ );
+
+ return $db;
+ }
+
+ public function testIteration() {
+ $db = $this->getDatabaseMock( [
+ [ 'colA' => 1, 'colB' => 'a' ],
+ [ 'colA' => 2, 'colB' => 'b' ],
+ [ 'colA' => 3, 'colB' => 'c' ],
+ [ 'colA' => 4, 'colB' => 'd' ],
+ [ 'colA' => 5, 'colB' => 'e' ],
+ [ 'colA' => 6, 'colB' => 'f' ],
+ [ 'colA' => 7, 'colB' => 'g' ],
+ [ 'colA' => 8, 'colB' => 'h' ]
+ ] );
+
+ $expectedRows = [
+ 0 => (object)[ 'colA' => 1, 'colB' => 'a' ],
+ 1 => (object)[ 'colA' => 2, 'colB' => 'b' ],
+ 2 => (object)[ 'colA' => 3, 'colB' => 'c' ],
+ 3 => (object)[ 'colA' => 4, 'colB' => 'd' ],
+ 4 => (object)[ 'colA' => 5, 'colB' => 'e' ],
+ 5 => (object)[ 'colA' => 6, 'colB' => 'f' ],
+ 6 => (object)[ 'colA' => 7, 'colB' => 'g' ],
+ 7 => (object)[ 'colA' => 8, 'colB' => 'h' ]
+ ];
+
+ $res = $db->select( 'faketable', [ 'colA', 'colB' ], '1 = 1', __METHOD__ );
+ $this->assertEquals( 8, $res->numRows() );
+
+ $res->seek( 7 );
+ $this->assertEquals( [ 'colA' => 8, 'colB' => 'h' ], $res->fetchRow() );
+ $res->seek( 7 );
+ $this->assertEquals( (object)[ 'colA' => 8, 'colB' => 'h' ], $res->fetchObject() );
+
+ $this->assertEquals( $expectedRows, iterator_to_array( $res, true ) );
+
+ $rows = [];
+ foreach ( $res as $i => $row ) {
+ $rows[$i] = $row;
+ }
+ $this->assertEquals( $expectedRows, $rows );
+ }
+}
use MediaWikiCoversValidator;
use PHPUnit4And6Compat;
+ const NAME = 'test.blobstore';
+
protected function setUp() {
parent::setUp();
// MediaWiki's test wrapper sets $wgMainWANCache to CACHE_NONE.
}
public function testBlobCreation() {
- $module = $this->makeModule( [ 'mainpage' ] );
$rl = new EmptyResourceLoader();
- $rl->register( $module->getName(), $module );
+ $rl->register( self::NAME, [
+ 'factory' => function () {
+ return $this->makeModule( [ 'mainpage' ] );
+ }
+ ] );
$blobStore = $this->makeBlobStore( null, $rl );
- $blob = $blobStore->getBlob( $module, 'en' );
+ $blob = $blobStore->getBlob( $rl->getModule( self::NAME ), 'en' );
$this->assertEquals( '{"mainpage":"Main Page"}', $blob, 'Generated blob' );
}
public function testBlobCreation_empty() {
$module = $this->makeModule( [] );
$rl = new EmptyResourceLoader();
- $rl->register( $module->getName(), $module );
$blobStore = $this->makeBlobStore( null, $rl );
$blob = $blobStore->getBlob( $module, 'en' );
public function testBlobCreation_unknownMessage() {
$module = $this->makeModule( [ 'i-dont-exist', 'mainpage', 'i-dont-exist2' ] );
$rl = new EmptyResourceLoader();
- $rl->register( $module->getName(), $module );
$blobStore = $this->makeBlobStore( null, $rl );
// Generating a blob should continue without errors,
}
public function testMessageCachingAndPurging() {
- $module = $this->makeModule( [ 'example' ] );
$rl = new EmptyResourceLoader();
- $rl->register( $module->getName(), $module );
+ // Register it so that MessageBlobStore::updateMessage can
+ // discover it from the registry as a module that uses this message.
+ $rl->register( self::NAME, [
+ 'factory' => function () {
+ return $this->makeModule( [ 'example' ] );
+ }
+ ] );
+ $module = $rl->getModule( self::NAME );
$blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
// Advance this new WANObjectCache instance to a normal state,
public function testPurgeEverything() {
$module = $this->makeModule( [ 'example' ] );
$rl = new EmptyResourceLoader();
- $rl->register( $module->getName(), $module );
$blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
// Advance this new WANObjectCache instance to a normal state.
$blobStore->getBlob( $module, 'en' );
// Arrange version 1 of a module
$module = $this->makeModule( [ 'foo' ] );
$rl = new EmptyResourceLoader();
- $rl->register( $module->getName(), $module );
$blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
$blobStore->expects( $this->once() )
->method( 'fetchMessage' )
// We do not receive purges for this because no messages were changed.
$module = $this->makeModule( [ 'foo', 'bar' ] );
$rl = new EmptyResourceLoader();
- $rl->register( $module->getName(), $module );
$blobStore = $this->makeBlobStore( [ 'fetchMessage' ], $rl );
$blobStore->expects( $this->exactly( 2 ) )
->method( 'fetchMessage' )
private function makeModule( array $messages ) {
$module = new ResourceLoaderTestModule( [ 'messages' => $messages ] );
- $module->setName( 'test.blobstore' );
+ $module->setName( self::NAME );
return $module;
}
}
}
private static function makeModule( array $options = [] ) {
- return new ResourceLoaderTestModule( $options );
+ return $options + [ 'class' => ResourceLoaderTestModule::class ];
}
private static function makeSampleModules() {
$context = $this->getResourceLoaderContext();
$module = new ResourceLoaderTestModule( [
+ 'mayValidateScript' => true,
'script' => "var a = 'this is';\n {\ninvalid"
] );
$this->assertEquals(
[ [
'msg' => 'Basic registry',
'modules' => [
- 'test.blank' => new ResourceLoaderTestModule(),
+ 'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
],
'out' => '
mw.loader.addSource( {
[ [
'msg' => 'Optimise the dependency tree (basic case)',
'modules' => [
- 'a' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'b', 'c', 'd' ] ] ),
- 'b' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'c' ] ] ),
- 'c' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
- 'd' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
+ 'a' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [ 'b', 'c', 'd' ],
+ ],
+ 'b' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [ 'c' ],
+ ],
+ 'c' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [],
+ ],
+ 'd' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [],
+ ],
],
'out' => '
mw.loader.addSource( {
[ [
'msg' => 'Optimise the dependency tree (tolerate unknown deps)',
'modules' => [
- 'a' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'b', 'c', 'x' ] ] ),
- 'b' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'c', 'x' ] ] ),
- 'c' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
+ 'a' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [ 'b', 'c', 'x' ]
+ ],
+ 'b' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [ 'c', 'x' ]
+ ],
+ 'c' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => []
+ ],
],
'out' => '
mw.loader.addSource( {
// Regression test for T223402.
'msg' => 'Optimise the dependency tree (indirect circular dependency)',
'modules' => [
- 'top' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'middle1', 'util' ] ] ),
- 'middle1' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'middle2', 'util' ] ] ),
- 'middle2' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'bottom' ] ] ),
- 'bottom' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'top' ] ] ),
- 'util' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
+ 'top' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [ 'middle1', 'util' ],
+ ],
+ 'middle1' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [ 'middle2', 'util' ],
+ ],
+ 'middle2' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [ 'bottom' ],
+ ],
+ 'bottom' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [ 'top' ],
+ ],
+ 'util' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [],
+ ],
],
'out' => '
mw.loader.addSource( {
// Regression test for T223402.
'msg' => 'Optimise the dependency tree (direct circular dependency)',
'modules' => [
- 'top' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'util', 'top' ] ] ),
- 'util' => new ResourceLoaderTestModule( [ 'dependencies' => [] ] ),
+ 'top' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [ 'util', 'top' ],
+ ],
+ 'util' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [],
+ ],
],
'out' => '
mw.loader.addSource( {
[ [
'msg' => 'Version falls back gracefully if getVersionHash throws',
'modules' => [
- 'test.fail' => (
- ( $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
- ->setMethods( [ 'getVersionHash' ] )->getMock() )
- && $mock->method( 'getVersionHash' )->will(
- $this->throwException( new Exception )
- )
- ) ? $mock : $mock
+ 'test.fail' => [
+ 'factory' => function () {
+ $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
+ ->setMethods( [ 'getVersionHash' ] )->getMock();
+ $mock->method( 'getVersionHash' )->will(
+ $this->throwException( new Exception )
+ );
+ return $mock;
+ }
+ ]
],
'out' => '
mw.loader.addSource( {
[ [
'msg' => 'Use version from getVersionHash',
'modules' => [
- 'test.version' => (
- ( $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
- ->setMethods( [ 'getVersionHash' ] )->getMock() )
- && $mock->method( 'getVersionHash' )->willReturn( '1234567' )
- ) ? $mock : $mock
+ 'test.version' => [
+ 'factory' => function () {
+ $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
+ ->setMethods( [ 'getVersionHash' ] )->getMock();
+ $mock->method( 'getVersionHash' )->willReturn( '1234567' );
+ return $mock;
+ }
+ ]
],
'out' => '
mw.loader.addSource( {
[ [
'msg' => 'Re-hash version from getVersionHash if too long',
'modules' => [
- 'test.version' => (
- ( $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
- ->setMethods( [ 'getVersionHash' ] )->getMock() )
- && $mock->method( 'getVersionHash' )->willReturn( '12345678' )
- ) ? $mock : $mock
+ 'test.version' => [
+ 'factory' => function () {
+ $mock = $this->getMockBuilder( ResourceLoaderTestModule::class )
+ ->setMethods( [ 'getVersionHash' ] )->getMock();
+ $mock->method( 'getVersionHash' )->willReturn( '12345678' );
+ return $mock;
+ }
+ ],
],
'out' => '
mw.loader.addSource( {
[ [
'msg' => 'Group signature',
'modules' => [
- 'test.blank' => new ResourceLoaderTestModule(),
- 'test.group.foo' => new ResourceLoaderTestModule( [ 'group' => 'x-foo' ] ),
- 'test.group.bar' => new ResourceLoaderTestModule( [ 'group' => 'x-bar' ] ),
+ 'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'test.group.foo' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'group' => 'x-foo',
+ ],
+ 'test.group.bar' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'group' => 'x-bar',
+ ],
],
'out' => '
mw.loader.addSource( {
[ [
'msg' => 'Different target (non-test should not be registered)',
'modules' => [
- 'test.blank' => new ResourceLoaderTestModule(),
- 'test.target.foo' => new ResourceLoaderTestModule( [ 'targets' => [ 'x-foo' ] ] ),
+ 'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'test.target.foo' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'targets' => [ 'x-foo' ],
+ ],
],
'out' => '
mw.loader.addSource( {
'msg' => 'Safemode disabled (default; register all modules)',
'modules' => [
// Default origin: ORIGIN_CORE_SITEWIDE
- 'test.blank' => new ResourceLoaderTestModule(),
- 'test.core-generated' => new ResourceLoaderTestModule( [
+ 'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'test.core-generated' => [
+ 'class' => ResourceLoaderTestModule::class,
'origin' => ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
- ] ),
- 'test.sitewide' => new ResourceLoaderTestModule( [
+ ],
+ 'test.sitewide' => [
+ 'class' => ResourceLoaderTestModule::class,
'origin' => ResourceLoaderModule::ORIGIN_USER_SITEWIDE
- ] ),
- 'test.user' => new ResourceLoaderTestModule( [
+ ],
+ 'test.user' => [
+ 'class' => ResourceLoaderTestModule::class,
'origin' => ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL
- ] ),
+ ],
],
'out' => '
mw.loader.addSource( {
'extraQuery' => [ 'safemode' => '1' ],
'modules' => [
// Default origin: ORIGIN_CORE_SITEWIDE
- 'test.blank' => new ResourceLoaderTestModule(),
- 'test.core-generated' => new ResourceLoaderTestModule( [
+ 'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'test.core-generated' => [
+ 'class' => ResourceLoaderTestModule::class,
'origin' => ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL
- ] ),
- 'test.sitewide' => new ResourceLoaderTestModule( [
+ ],
+ 'test.sitewide' => [
+ 'class' => ResourceLoaderTestModule::class,
'origin' => ResourceLoaderModule::ORIGIN_USER_SITEWIDE
- ] ),
- 'test.user' => new ResourceLoaderTestModule( [
+ ],
+ 'test.user' => [
+ 'class' => ResourceLoaderTestModule::class,
'origin' => ResourceLoaderModule::ORIGIN_USER_INDIVIDUAL
- ] ),
+ ],
],
'out' => '
mw.loader.addSource( {
],
],
'modules' => [
- 'test.blank' => new ResourceLoaderTestModule( [ 'source' => 'example' ] ),
+ 'test.blank' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'source' => 'example'
+ ],
],
'out' => '
mw.loader.addSource( {
[ [
'msg' => 'Conditional dependency function',
'modules' => [
- 'test.x.core' => new ResourceLoaderTestModule(),
- 'test.x.polyfill' => new ResourceLoaderTestModule( [
+ 'test.x.core' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'test.x.polyfill' => [
+ 'class' => ResourceLoaderTestModule::class,
'skipFunction' => 'return true;'
- ] ),
- 'test.y.polyfill' => new ResourceLoaderTestModule( [
+ ],
+ 'test.y.polyfill' => [
+ 'class' => ResourceLoaderTestModule::class,
'skipFunction' =>
'return !!(' .
' window.JSON &&' .
' JSON.parse &&' .
' JSON.stringify' .
');'
- ] ),
- 'test.z.foo' => new ResourceLoaderTestModule( [
+ ],
+ 'test.z.foo' => [
+ 'class' => ResourceLoaderTestModule::class,
'dependencies' => [
'test.x.core',
'test.x.polyfill',
'test.y.polyfill',
],
- ] ),
+ ],
],
'out' => '
mw.loader.addSource( {
],
],
'modules' => [
- 'test.blank' => new ResourceLoaderTestModule(),
- 'test.x.core' => new ResourceLoaderTestModule(),
- 'test.x.util' => new ResourceLoaderTestModule( [
+ 'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'test.x.core' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'test.x.util' => [
+ 'class' => ResourceLoaderTestModule::class,
'dependencies' => [
'test.x.core',
],
- ] ),
- 'test.x.foo' => new ResourceLoaderTestModule( [
+ ],
+ 'test.x.foo' => [
+ 'class' => ResourceLoaderTestModule::class,
'dependencies' => [
'test.x.core',
],
- ] ),
- 'test.x.bar' => new ResourceLoaderTestModule( [
+ ],
+ 'test.x.bar' => [
+ 'class' => ResourceLoaderTestModule::class,
'dependencies' => [
'test.x.core',
'test.x.util',
],
- ] ),
- 'test.x.quux' => new ResourceLoaderTestModule( [
+ ],
+ 'test.x.quux' => [
+ 'class' => ResourceLoaderTestModule::class,
'dependencies' => [
'test.x.foo',
'test.x.bar',
'test.x.util',
'test.x.unknown',
],
- ] ),
- 'test.group.foo.1' => new ResourceLoaderTestModule( [
+ ],
+ 'test.group.foo.1' => [
+ 'class' => ResourceLoaderTestModule::class,
'group' => 'x-foo',
- ] ),
- 'test.group.foo.2' => new ResourceLoaderTestModule( [
+ ],
+ 'test.group.foo.2' => [
+ 'class' => ResourceLoaderTestModule::class,
'group' => 'x-foo',
- ] ),
- 'test.group.bar.1' => new ResourceLoaderTestModule( [
+ ],
+ 'test.group.bar.1' => [
+ 'class' => ResourceLoaderTestModule::class,
'group' => 'x-bar',
- ] ),
- 'test.group.bar.2' => new ResourceLoaderTestModule( [
+ ],
+ 'test.group.bar.2' => [
+ 'class' => ResourceLoaderTestModule::class,
'group' => 'x-bar',
'source' => 'example',
- ] ),
- 'test.target.foo' => new ResourceLoaderTestModule( [
+ ],
+ 'test.target.foo' => [
+ 'class' => ResourceLoaderTestModule::class,
'targets' => [ 'x-foo' ],
- ] ),
- 'test.target.bar' => new ResourceLoaderTestModule( [
+ ],
+ 'test.target.bar' => [
+ 'class' => ResourceLoaderTestModule::class,
'source' => 'example',
'targets' => [ 'x-foo' ],
- ] ),
+ ],
],
'out' => '
mw.loader.addSource( {
public static function provideRegistrations() {
return [
[ [
- 'test.blank' => new ResourceLoaderTestModule(),
- 'test.min' => new ResourceLoaderTestModule( [
+ 'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'test.min' => [
+ 'class' => ResourceLoaderTestModule::class,
'skipFunction' =>
'return !!(' .
' window.JSON &&' .
'dependencies' => [
'test.blank',
],
- ] ),
+ ],
] ]
];
}
$context1 = $this->getResourceLoaderContext();
$rl1 = $context1->getResourceLoader();
$rl1->register( [
- 'test.a' => new ResourceLoaderTestModule(),
- 'test.b' => new ResourceLoaderTestModule(),
+ 'test.a' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'test.b' => [ 'class' => ResourceLoaderTestModule::class ],
] );
$module = new ResourceLoaderStartupModule();
$version1 = $module->getVersionHash( $context1 );
$context2 = $this->getResourceLoaderContext();
$rl2 = $context2->getResourceLoader();
$rl2->register( [
- 'test.b' => new ResourceLoaderTestModule(),
- 'test.c' => new ResourceLoaderTestModule(),
+ 'test.b' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'test.c' => [ 'class' => ResourceLoaderTestModule::class ],
] );
$module = new ResourceLoaderStartupModule();
$version2 = $module->getVersionHash( $context2 );
$context3 = $this->getResourceLoaderContext();
$rl3 = $context3->getResourceLoader();
$rl3->register( [
- 'test.a' => new ResourceLoaderTestModule(),
- 'test.b' => new ResourceLoaderTestModule( [ 'script' => 'different' ] ),
+ 'test.a' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'test.b' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'script' => 'different',
+ ],
] );
$module = new ResourceLoaderStartupModule();
$version3 = $module->getVersionHash( $context3 );
$context = $this->getResourceLoaderContext();
$rl = $context->getResourceLoader();
$rl->register( [
- 'test.a' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'x', 'y' ] ] ),
+ 'test.a' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [ 'x', 'y' ],
+ ],
] );
$module = new ResourceLoaderStartupModule();
$version1 = $module->getVersionHash( $context );
$context = $this->getResourceLoaderContext();
$rl = $context->getResourceLoader();
$rl->register( [
- 'test.a' => new ResourceLoaderTestModule( [ 'dependencies' => [ 'x', 'z' ] ] ),
+ 'test.a' => [
+ 'class' => ResourceLoaderTestModule::class,
+ 'dependencies' => [ 'x', 'z' ],
+ ],
] );
$module = new ResourceLoaderStartupModule();
$version2 = $module->getVersionHash( $context );
$this->assertTrue( ResourceLoader::isValidModuleName( $name ) );
}
- /**
- * @covers ResourceLoader::register
- * @covers ResourceLoader::getModule
- */
- public function testRegisterValidObject() {
- $module = new ResourceLoaderTestModule();
- $resourceLoader = new EmptyResourceLoader();
- $resourceLoader->register( 'test', $module );
- $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
- }
-
/**
* @covers ResourceLoader::register
* @covers ResourceLoader::getModule
*/
public function testRegisterValidArray() {
- $module = new ResourceLoaderTestModule();
$resourceLoader = new EmptyResourceLoader();
// Covers case of register() setting $rl->moduleInfos,
// but $rl->modules lazy-populated by getModule()
- $resourceLoader->register( 'test', [ 'object' => $module ] );
- $this->assertEquals( $module, $resourceLoader->getModule( 'test' ) );
+ $resourceLoader->register( 'test', [ 'class' => ResourceLoaderTestModule::class ] );
+ $this->assertInstanceOf(
+ ResourceLoaderTestModule::class,
+ $resourceLoader->getModule( 'test' )
+ );
}
/**
* @group medium
*/
public function testRegisterEmptyString() {
- $module = new ResourceLoaderTestModule();
$resourceLoader = new EmptyResourceLoader();
- $resourceLoader->register( '', $module );
- $this->assertEquals( $module, $resourceLoader->getModule( '' ) );
+ $resourceLoader->register( '', [ 'class' => ResourceLoaderTestModule::class ] );
+ $this->assertInstanceOf(
+ ResourceLoaderTestModule::class,
+ $resourceLoader->getModule( '' )
+ );
}
/**
public function testRegisterInvalidName() {
$resourceLoader = new EmptyResourceLoader();
$this->setExpectedException( MWException::class, "name 'test!invalid' is invalid" );
- $resourceLoader->register( 'test!invalid', new ResourceLoaderTestModule() );
+ $resourceLoader->register( 'test!invalid', [] );
}
/**
->method( 'warning' );
$resourceLoader = new EmptyResourceLoader( null, $logger );
- $module1 = new ResourceLoaderTestModule();
- $module2 = new ResourceLoaderTestModule();
- $resourceLoader->register( 'test', $module1 );
- $resourceLoader->register( 'test', $module2 );
- $this->assertSame( $module2, $resourceLoader->getModule( 'test' ) );
+ $resourceLoader->register( 'test', [ 'class' => ResourceLoaderSkinModule::class ] );
+ $resourceLoader->register( 'test', [ 'class' => ResourceLoaderStartUpModule::class ] );
+ $this->assertInstanceOf(
+ ResourceLoaderStartUpModule::class,
+ $resourceLoader->getModule( 'test' ),
+ 'last one wins'
+ );
}
/**
public function testGetModuleNames() {
// Use an empty one so that core and extension modules don't get in.
$resourceLoader = new EmptyResourceLoader();
- $resourceLoader->register( 'test.foo', new ResourceLoaderTestModule() );
- $resourceLoader->register( 'test.bar', new ResourceLoaderTestModule() );
+ $resourceLoader->register( 'test.foo', [] );
+ $resourceLoader->register( 'test.bar', [] );
$this->assertEquals(
[ 'startup', 'test.foo', 'test.bar' ],
$resourceLoader->getModuleNames()
}
public function provideTestIsFileModule() {
- $fileModuleObj = $this->getMockBuilder( ResourceLoaderFileModule::class )
- ->disableOriginalConstructor()
- ->getMock();
+ $fileModuleObj = $this->createMock( ResourceLoaderFileModule::class );
return [
- 'object' => [ false,
- new ResourceLoaderTestModule()
+ 'factory ignored' => [ false,
+ [
+ 'factory' => function () {
+ return new ResourceLoaderTestModule();
+ }
+ ]
],
- 'FileModule object' => [ false,
- $fileModuleObj
+ 'factory ignored (actual FileModule)' => [ false,
+ [
+ 'factory' => function () use ( $fileModuleObj ) {
+ return $fileModuleObj;
+ }
+ ]
],
'simple empty' => [ true,
[]
*/
public function testIsModuleRegistered() {
$rl = new EmptyResourceLoader();
- $rl->register( 'test', new ResourceLoaderTestModule() );
+ $rl->register( 'test', [] );
$this->assertTrue( $rl->isModuleRegistered( 'test' ) );
$this->assertFalse( $rl->isModuleRegistered( 'test.unknown' ) );
}
// Disable log from outputErrorAndLog
->setMethods( [ 'outputErrorAndLog' ] )->getMock();
$rl->register( [
- 'foo' => self::getSimpleModuleMock(),
- 'ferry' => self::getFailFerryMock(),
- 'bar' => self::getSimpleModuleMock(),
+ 'foo' => [ 'class' => ResourceLoaderTestModule::class ],
+ 'ferry' => [
+ 'factory' => function () {
+ return self::getFailFerryMock();
+ }
+ ],
+ 'bar' => [ 'class' => ResourceLoaderTestModule::class ],
] );
$context = $this->getResourceLoaderContext( [], $rl );
$modules = array_map( function ( $script ) {
return self::getSimpleModuleMock( $script );
}, $scripts );
- $rl->register( $modules );
$context = $this->getResourceLoaderContext(
[
'bar' => self::getSimpleModuleMock( 'bar();' ),
];
$rl = new EmptyResourceLoader();
- $rl->register( $modules );
$context = $this->getResourceLoaderContext(
[
'modules' => 'foo|ferry|bar',
'bar' => self::getSimpleStyleModuleMock( '.bar{}' ),
];
$rl = new EmptyResourceLoader();
- $rl->register( $modules );
$context = $this->getResourceLoaderContext(
[
'modules' => 'foo|ferry|bar',
// provide the full Config object here.
$rl = new EmptyResourceLoader( MediaWikiServices::getInstance()->getMainConfig() );
$rl->register( [
- 'foo' => self::getSimpleModuleMock( 'foo();' ),
- 'ferry' => self::getFailFerryMock(),
- 'bar' => self::getSimpleModuleMock( 'bar();' ),
+ 'foo' => [ 'factory' => function () {
+ return self::getSimpleModuleMock( 'foo();' );
+ } ],
+ 'ferry' => [ 'factory' => function () {
+ return self::getFailFerryMock();
+ } ],
+ 'bar' => [ 'factory' => function () {
+ return self::getSimpleModuleMock( 'bar();' );
+ } ],
] );
$context = $this->getResourceLoaderContext(
[
] );
$rl = new EmptyResourceLoader();
- $rl->register( [
- 'foo' => $module,
- ] );
$context = $this->getResourceLoaderContext(
[ 'modules' => 'foo', 'only' => 'scripts' ],
$rl
);
- $modules = [ 'foo' => $rl->getModule( 'foo' ) ];
+ $modules = [ 'foo' => $module ];
$response = $rl->makeModuleResponse( $context, $modules );
$extraHeaders = TestingAccessWrapper::newFromObject( $rl )->extraHeaders;
] );
$rl = new EmptyResourceLoader();
- $rl->register( [ 'foo' => $foo, 'bar' => $bar ] );
$context = $this->getResourceLoaderContext(
[ 'modules' => 'foo|bar', 'only' => 'scripts' ],
$rl
);
- $modules = [ 'foo' => $rl->getModule( 'foo' ), 'bar' => $rl->getModule( 'bar' ) ];
+ $modules = [ 'foo' => $foo, 'bar' => $bar ];
$response = $rl->makeModuleResponse( $context, $modules );
$extraHeaders = TestingAccessWrapper::newFromObject( $rl )->extraHeaders;
$this->assertEquals(
'makeModuleResponse',
] )
->getMock();
- $rl->register( 'test', $module );
+ $rl->register( 'test', [
+ 'factory' => function () use ( $module ) {
+ return $module;
+ }
+ ] );
$context = $this->getResourceLoaderContext(
[ 'modules' => 'test', 'only' => null ],
$rl
'sendResponseHeaders',
] )
->getMock();
- $rl->register( 'test', $module );
+ $rl->register( 'test', [
+ 'factory' => function () use ( $module ) {
+ return $module;
+ }
+ ] );
$context = $this->getResourceLoaderContext( [ 'modules' => 'test' ], $rl );
// Disable logging from outputErrorAndLog
$this->setLogger( 'exception', new Psr\Log\NullLogger() );
$module::$returnFetchTitleInfo = $titleInfo;
$rl = new EmptyResourceLoader();
- $rl->register( 'testmodule', $module );
$context = new ResourceLoaderContext( $rl, new FauxRequest() );
TestResourceLoaderWikiModule::invalidateModuleCache(
* @covers ResourceLoaderWikiModule::preloadTitleInfo
*/
public function testGetPreloadedBadTitle() {
- // Mock values
- $pages = [
- // Covers else branch for invalid page name
- '[x]' => [ 'type' => 'styles' ],
- ];
- $titleInfo = [];
-
- // Set up objects
- $module = $this->getMockBuilder( TestResourceLoaderWikiModule::class )
- ->setMethods( [ 'getPages' ] )->getMock();
- $module->method( 'getPages' )->willReturn( $pages );
- $module::$returnFetchTitleInfo = $titleInfo;
+ // Set up
+ TestResourceLoaderWikiModule::$returnFetchTitleInfo = [];
$rl = new EmptyResourceLoader();
- $rl->register( 'testmodule', $module );
+ $rl->getConfig()->set( 'UseSiteJs', true );
+ $rl->getConfig()->set( 'UseSiteCss', true );
+ $rl->register( 'testmodule', [
+ 'class' => TestResourceLoaderWikiModule::class,
+ // Covers preloadTitleInfo branch for invalid page name
+ 'styles' => [ '[x]' ],
+ ] );
$context = new ResourceLoaderContext( $rl, new FauxRequest() );
// Act
);
// Assert
- $module = TestingAccessWrapper::newFromObject( $module );
- $this->assertEquals( $titleInfo, $module->getTitleInfo( $context ), 'Title info' );
+ $module = TestingAccessWrapper::newFromObject( $rl->getModule( 'testmodule' ) );
+ $this->assertSame( [], $module->getTitleInfo( $context ), 'Title info' );
}
/**
$module->method( 'getPages' )->willReturn( $pages );
$rl = new EmptyResourceLoader();
- $rl->register( 'testmodule', $module );
$context = new DerivativeResourceLoaderContext(
new ResourceLoaderContext( $rl, new FauxRequest() )
);
+++ /dev/null
-<?php
-
-/**
- * @covers LanguageCode
- * @group Language
- *
- * @author Thiemo Kreuz
- */
-class LanguageCodeTest extends PHPUnit\Framework\TestCase {
-
- use MediaWikiCoversValidator;
-
- public function testConstructor() {
- $instance = new LanguageCode();
-
- $this->assertInstanceOf( LanguageCode::class, $instance );
- }
-
- public function testGetDeprecatedCodeMapping() {
- $map = LanguageCode::getDeprecatedCodeMapping();
-
- $this->assertInternalType( 'array', $map );
- $this->assertContainsOnly( 'string', array_keys( $map ) );
- $this->assertArrayNotHasKey( '', $map );
- $this->assertContainsOnly( 'string', $map );
- $this->assertNotContains( '', $map );
-
- // Codes special to MediaWiki should never appear in a map of "deprecated" codes
- $this->assertArrayNotHasKey( 'qqq', $map, 'documentation' );
- $this->assertNotContains( 'qqq', $map, 'documentation' );
- $this->assertArrayNotHasKey( 'qqx', $map, 'debug code' );
- $this->assertNotContains( 'qqx', $map, 'debug code' );
-
- // Valid language codes that are currently not "deprecated"
- $this->assertArrayNotHasKey( 'bh', $map, 'family of Bihari languages' );
- $this->assertArrayNotHasKey( 'no', $map, 'family of Norwegian languages' );
- $this->assertArrayNotHasKey( 'simple', $map );
- }
-
- public function testReplaceDeprecatedCodes() {
- $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'als' ) );
- $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'gsw' ) );
- $this->assertEquals( null, LanguageCode::replaceDeprecatedCodes( null ) );
- }
-
- /**
- * test @see LanguageCode::bcp47().
- * Please note the BCP 47 explicitly state that language codes are case
- * insensitive, there are some exceptions to the rule :)
- * This test is used to verify our formatting against all lower and
- * all upper cases language code.
- *
- * @see https://tools.ietf.org/html/bcp47
- * @dataProvider provideLanguageCodes()
- */
- public function testBcp47( $code, $expected ) {
- $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
- "Applying BCP 47 standard to '$code'"
- );
-
- $code = strtolower( $code );
- $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
- "Applying BCP 47 standard to lower case '$code'"
- );
-
- $code = strtoupper( $code );
- $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
- "Applying BCP 47 standard to upper case '$code'"
- );
- }
-
- /**
- * Array format is ($code, $expected)
- */
- public static function provideLanguageCodes() {
- return [
- // Extracted from BCP 47 (list not exhaustive)
- # 2.1.1
- [ 'en-ca-x-ca', 'en-CA-x-ca' ],
- [ 'sgn-be-fr', 'sgn-BE-FR' ],
- [ 'az-latn-x-latn', 'az-Latn-x-latn' ],
- # 2.2
- [ 'sr-Latn-RS', 'sr-Latn-RS' ],
- [ 'az-arab-ir', 'az-Arab-IR' ],
-
- # 2.2.5
- [ 'sl-nedis', 'sl-nedis' ],
- [ 'de-ch-1996', 'de-CH-1996' ],
-
- # 2.2.6
- [
- 'en-latn-gb-boont-r-extended-sequence-x-private',
- 'en-Latn-GB-boont-r-extended-sequence-x-private'
- ],
-
- // Examples from BCP 47 Appendix A
- # Simple language subtag:
- [ 'DE', 'de' ],
- [ 'fR', 'fr' ],
- [ 'ja', 'ja' ],
-
- # Language subtag plus script subtag:
- [ 'zh-hans', 'zh-Hans' ],
- [ 'sr-cyrl', 'sr-Cyrl' ],
- [ 'sr-latn', 'sr-Latn' ],
-
- # Extended language subtags and their primary language subtag
- # counterparts:
- [ 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ],
- [ 'cmn-hans-cn', 'cmn-Hans-CN' ],
- [ 'zh-yue-hk', 'zh-yue-HK' ],
- [ 'yue-hk', 'yue-HK' ],
-
- # Language-Script-Region:
- [ 'zh-hans-cn', 'zh-Hans-CN' ],
- [ 'sr-latn-RS', 'sr-Latn-RS' ],
-
- # Language-Variant:
- [ 'sl-rozaj', 'sl-rozaj' ],
- [ 'sl-rozaj-biske', 'sl-rozaj-biske' ],
- [ 'sl-nedis', 'sl-nedis' ],
-
- # Language-Region-Variant:
- [ 'de-ch-1901', 'de-CH-1901' ],
- [ 'sl-it-nedis', 'sl-IT-nedis' ],
-
- # Language-Script-Region-Variant:
- [ 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ],
-
- # Language-Region:
- [ 'de-de', 'de-DE' ],
- [ 'en-us', 'en-US' ],
- [ 'es-419', 'es-419' ],
-
- # Private use subtags:
- [ 'de-ch-x-phonebk', 'de-CH-x-phonebk' ],
- [ 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ],
- /**
- * Previous test does not reflect the BCP 47 which states:
- * az-Arab-x-AZE-derbend
- * AZE being private, it should be lower case, hence the test above
- * should probably be:
- * [ 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ],
- */
-
- # Private use registry values:
- [ 'x-whatever', 'x-whatever' ],
- [ 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ],
- [ 'de-qaaa', 'de-Qaaa' ],
- [ 'sr-latn-qm', 'sr-Latn-QM' ],
- [ 'sr-qaaa-rs', 'sr-Qaaa-RS' ],
-
- # Tags that use extensions
- [ 'en-us-u-islamcal', 'en-US-u-islamcal' ],
- [ 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ],
- [ 'en-a-myext-b-another', 'en-a-myext-b-another' ],
-
- # Invalid:
- // de-419-DE
- // a-DE
- // ar-a-aaa-b-bbb-a-ccc
-
- # Non-standard and deprecated language codes used by MediaWiki
- [ 'als', 'gsw' ],
- [ 'bat-smg', 'sgs' ],
- [ 'be-x-old', 'be-tarask' ],
- [ 'fiu-vro', 'vro' ],
- [ 'roa-rup', 'rup' ],
- [ 'zh-classical', 'lzh' ],
- [ 'zh-min-nan', 'nan' ],
- [ 'zh-yue', 'yue' ],
- [ 'cbk-zam', 'cbk' ],
- [ 'de-formal', 'de-x-formal' ],
- [ 'eml', 'egl' ],
- [ 'en-rtl', 'en-x-rtl' ],
- [ 'es-formal', 'es-x-formal' ],
- [ 'hu-formal', 'hu-x-formal' ],
- [ 'kk-Arab', 'kk-Arab' ],
- [ 'kk-Cyrl', 'kk-Cyrl' ],
- [ 'kk-Latn', 'kk-Latn' ],
- [ 'map-bms', 'jv-x-bms' ],
- [ 'mo', 'ro-Cyrl-MD' ],
- [ 'nrm', 'nrf' ],
- [ 'nl-informal', 'nl-x-informal' ],
- [ 'roa-tara', 'nap-x-tara' ],
- [ 'simple', 'en-simple' ],
- [ 'sr-ec', 'sr-Cyrl' ],
- [ 'sr-el', 'sr-Latn' ],
- [ 'zh-cn', 'zh-Hans-CN' ],
- [ 'zh-sg', 'zh-Hans-SG' ],
- [ 'zh-my', 'zh-Hans-MY' ],
- [ 'zh-tw', 'zh-Hant-TW' ],
- [ 'zh-hk', 'zh-Hant-HK' ],
- [ 'zh-mo', 'zh-Hant-MO' ],
- [ 'zh-hans', 'zh-Hans' ],
- [ 'zh-hant', 'zh-Hant' ],
- ];
- }
-
-}
--- /dev/null
+<?php
+
+/**
+ * @covers LanguageCode
+ * @group Language
+ *
+ * @author Thiemo Kreuz
+ */
+class LanguageCodeTest extends MediaWikiUnitTestCase {
+
+ public function testConstructor() {
+ $instance = new LanguageCode();
+
+ $this->assertInstanceOf( LanguageCode::class, $instance );
+ }
+
+ public function testGetDeprecatedCodeMapping() {
+ $map = LanguageCode::getDeprecatedCodeMapping();
+
+ $this->assertInternalType( 'array', $map );
+ $this->assertContainsOnly( 'string', array_keys( $map ) );
+ $this->assertArrayNotHasKey( '', $map );
+ $this->assertContainsOnly( 'string', $map );
+ $this->assertNotContains( '', $map );
+
+ // Codes special to MediaWiki should never appear in a map of "deprecated" codes
+ $this->assertArrayNotHasKey( 'qqq', $map, 'documentation' );
+ $this->assertNotContains( 'qqq', $map, 'documentation' );
+ $this->assertArrayNotHasKey( 'qqx', $map, 'debug code' );
+ $this->assertNotContains( 'qqx', $map, 'debug code' );
+
+ // Valid language codes that are currently not "deprecated"
+ $this->assertArrayNotHasKey( 'bh', $map, 'family of Bihari languages' );
+ $this->assertArrayNotHasKey( 'no', $map, 'family of Norwegian languages' );
+ $this->assertArrayNotHasKey( 'simple', $map );
+ }
+
+ public function testReplaceDeprecatedCodes() {
+ $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'als' ) );
+ $this->assertEquals( 'gsw', LanguageCode::replaceDeprecatedCodes( 'gsw' ) );
+ $this->assertEquals( null, LanguageCode::replaceDeprecatedCodes( null ) );
+ }
+
+ /**
+ * test @see LanguageCode::bcp47().
+ * Please note the BCP 47 explicitly state that language codes are case
+ * insensitive, there are some exceptions to the rule :)
+ * This test is used to verify our formatting against all lower and
+ * all upper cases language code.
+ *
+ * @see https://tools.ietf.org/html/bcp47
+ * @dataProvider provideLanguageCodes()
+ */
+ public function testBcp47( $code, $expected ) {
+ $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+ "Applying BCP 47 standard to '$code'"
+ );
+
+ $code = strtolower( $code );
+ $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+ "Applying BCP 47 standard to lower case '$code'"
+ );
+
+ $code = strtoupper( $code );
+ $this->assertEquals( $expected, LanguageCode::bcp47( $code ),
+ "Applying BCP 47 standard to upper case '$code'"
+ );
+ }
+
+ /**
+ * Array format is ($code, $expected)
+ */
+ public static function provideLanguageCodes() {
+ return [
+ // Extracted from BCP 47 (list not exhaustive)
+ # 2.1.1
+ [ 'en-ca-x-ca', 'en-CA-x-ca' ],
+ [ 'sgn-be-fr', 'sgn-BE-FR' ],
+ [ 'az-latn-x-latn', 'az-Latn-x-latn' ],
+ # 2.2
+ [ 'sr-Latn-RS', 'sr-Latn-RS' ],
+ [ 'az-arab-ir', 'az-Arab-IR' ],
+
+ # 2.2.5
+ [ 'sl-nedis', 'sl-nedis' ],
+ [ 'de-ch-1996', 'de-CH-1996' ],
+
+ # 2.2.6
+ [
+ 'en-latn-gb-boont-r-extended-sequence-x-private',
+ 'en-Latn-GB-boont-r-extended-sequence-x-private'
+ ],
+
+ // Examples from BCP 47 Appendix A
+ # Simple language subtag:
+ [ 'DE', 'de' ],
+ [ 'fR', 'fr' ],
+ [ 'ja', 'ja' ],
+
+ # Language subtag plus script subtag:
+ [ 'zh-hans', 'zh-Hans' ],
+ [ 'sr-cyrl', 'sr-Cyrl' ],
+ [ 'sr-latn', 'sr-Latn' ],
+
+ # Extended language subtags and their primary language subtag
+ # counterparts:
+ [ 'zh-cmn-hans-cn', 'zh-cmn-Hans-CN' ],
+ [ 'cmn-hans-cn', 'cmn-Hans-CN' ],
+ [ 'zh-yue-hk', 'zh-yue-HK' ],
+ [ 'yue-hk', 'yue-HK' ],
+
+ # Language-Script-Region:
+ [ 'zh-hans-cn', 'zh-Hans-CN' ],
+ [ 'sr-latn-RS', 'sr-Latn-RS' ],
+
+ # Language-Variant:
+ [ 'sl-rozaj', 'sl-rozaj' ],
+ [ 'sl-rozaj-biske', 'sl-rozaj-biske' ],
+ [ 'sl-nedis', 'sl-nedis' ],
+
+ # Language-Region-Variant:
+ [ 'de-ch-1901', 'de-CH-1901' ],
+ [ 'sl-it-nedis', 'sl-IT-nedis' ],
+
+ # Language-Script-Region-Variant:
+ [ 'hy-latn-it-arevela', 'hy-Latn-IT-arevela' ],
+
+ # Language-Region:
+ [ 'de-de', 'de-DE' ],
+ [ 'en-us', 'en-US' ],
+ [ 'es-419', 'es-419' ],
+
+ # Private use subtags:
+ [ 'de-ch-x-phonebk', 'de-CH-x-phonebk' ],
+ [ 'az-arab-x-aze-derbend', 'az-Arab-x-aze-derbend' ],
+ /**
+ * Previous test does not reflect the BCP 47 which states:
+ * az-Arab-x-AZE-derbend
+ * AZE being private, it should be lower case, hence the test above
+ * should probably be:
+ * [ 'az-arab-x-aze-derbend', 'az-Arab-x-AZE-derbend' ],
+ */
+
+ # Private use registry values:
+ [ 'x-whatever', 'x-whatever' ],
+ [ 'qaa-qaaa-qm-x-southern', 'qaa-Qaaa-QM-x-southern' ],
+ [ 'de-qaaa', 'de-Qaaa' ],
+ [ 'sr-latn-qm', 'sr-Latn-QM' ],
+ [ 'sr-qaaa-rs', 'sr-Qaaa-RS' ],
+
+ # Tags that use extensions
+ [ 'en-us-u-islamcal', 'en-US-u-islamcal' ],
+ [ 'zh-cn-a-myext-x-private', 'zh-CN-a-myext-x-private' ],
+ [ 'en-a-myext-b-another', 'en-a-myext-b-another' ],
+
+ # Invalid:
+ // de-419-DE
+ // a-DE
+ // ar-a-aaa-b-bbb-a-ccc
+
+ # Non-standard and deprecated language codes used by MediaWiki
+ [ 'als', 'gsw' ],
+ [ 'bat-smg', 'sgs' ],
+ [ 'be-x-old', 'be-tarask' ],
+ [ 'fiu-vro', 'vro' ],
+ [ 'roa-rup', 'rup' ],
+ [ 'zh-classical', 'lzh' ],
+ [ 'zh-min-nan', 'nan' ],
+ [ 'zh-yue', 'yue' ],
+ [ 'cbk-zam', 'cbk' ],
+ [ 'de-formal', 'de-x-formal' ],
+ [ 'eml', 'egl' ],
+ [ 'en-rtl', 'en-x-rtl' ],
+ [ 'es-formal', 'es-x-formal' ],
+ [ 'hu-formal', 'hu-x-formal' ],
+ [ 'kk-Arab', 'kk-Arab' ],
+ [ 'kk-Cyrl', 'kk-Cyrl' ],
+ [ 'kk-Latn', 'kk-Latn' ],
+ [ 'map-bms', 'jv-x-bms' ],
+ [ 'mo', 'ro-Cyrl-MD' ],
+ [ 'nrm', 'nrf' ],
+ [ 'nl-informal', 'nl-x-informal' ],
+ [ 'roa-tara', 'nap-x-tara' ],
+ [ 'simple', 'en-simple' ],
+ [ 'sr-ec', 'sr-Cyrl' ],
+ [ 'sr-el', 'sr-Latn' ],
+ [ 'zh-cn', 'zh-Hans-CN' ],
+ [ 'zh-sg', 'zh-Hans-SG' ],
+ [ 'zh-my', 'zh-Hans-MY' ],
+ [ 'zh-tw', 'zh-Hant-TW' ],
+ [ 'zh-hk', 'zh-Hant-HK' ],
+ [ 'zh-mo', 'zh-Hant-MO' ],
+ [ 'zh-hans', 'zh-Hans' ],
+ [ 'zh-hant', 'zh-Hant' ],
+ ];
+ }
+
+}