Content::getNativeData() for text-based content models.
* (T214706) LinksUpdate::getAddedExternalLinks() and
LinksUpdate::getRemovedExternalLinks() were introduced.
+* (T213893) Added 'MaintenanceUpdateAddParams' hook
=== External library changes in 1.33 ===
==== New external libraries ====
'MagicWordwgVariableIDs': When defining new magic words IDs.
&$variableIDs: array of strings
+'MaintenanceUpdateAddParams': allow extensions to add params to the update.php
+maintenance script.
+&$params: array to populate with the params to be added. Array elements are keyed by
+the param name. Each param is an associative array that must include the following keys:
+ - desc The description of the param to show on --help
+ - require Is the param required? Defaults to false if not set.
+ - withArg Is an argument required with this option? Defaults to false if not set.
+ - shortName Character to use as short name, or false if none. Defaults to false if not set.
+ - multiOccurrence Can this option be passed multiple times? Defaults to false if not set.
+
'MaintenanceRefreshLinksInit': before executing the refreshLinks.php maintenance
script.
$refreshLinks: RefreshLinks object
} elseif ( $this->section == 'new' ) {
// Nothing *to* preview for new sections
return false;
- } elseif ( ( $request->getVal( 'preload' ) !== null || $this->mTitle->exists() )
+ } elseif ( ( $request->getCheck( 'preload' ) || $this->mTitle->exists() )
&& $this->context->getUser()->getOption( 'previewonfirst' )
) {
// Standard preference behavior
$this->scrolltop = $request->getIntOrNull( 'wpScrolltop' );
- if ( $this->textbox1 === '' && $request->getVal( 'wpTextbox1' ) === null ) {
+ if ( $this->textbox1 === '' && !$request->getCheck( 'wpTextbox1' ) ) {
// wpTextbox1 field is missing, possibly due to being "too big"
// according to some filter rules such as Suhosin's setting for
// suhosin.request.max_value_length (d'oh)
if ( $request->getVal( 'action', 'view' ) != 'view'
|| $request->wasPosted()
- || ( $request->getVal( 'title' ) !== null
+ || ( $request->getCheck( 'title' )
&& $title->getPrefixedDBkey() == $request->getVal( 'title' ) )
|| count( $request->getValueNames( [ 'action', 'title' ] ) )
|| !Hooks::run( 'TestCanonicalRedirect', [ $request, $title, $output ] )
},
'ResourceLoader' => function ( MediaWikiServices $services ) : ResourceLoader {
- return new ResourceLoader(
- $services->getMainConfig(),
+ $config = $services->getMainConfig();
+
+ $rl = new ResourceLoader(
+ $config,
LoggerFactory::getInstance( 'resourceloader' )
);
+ $rl->addSource( $config->get( 'ResourceLoaderSources' ) );
+
+ return $rl;
},
'RevisionFactory' => function ( MediaWikiServices $services ) : RevisionFactory {
$update->setRevision( $legacyRevision );
$update->setTriggeringUser( $triggeringUser );
}
+
if ( $options['defer'] === false ) {
- if ( $options['transactionTicket'] !== null ) {
+ if ( $update instanceof DataUpdate && $options['transactionTicket'] !== null ) {
$update->setTransactionTicket( $options['transactionTicket'] );
}
$update->doUpdate();
private $tableCache = null;
/** @var bool|string */
- private $wikiId = false;
+ private $domain = false;
/** @var int */
private $cacheTTL;
* @param string $nameField
* @param callable|null $normalizationCallback Normalization to be applied to names before being
* saved or queried. This should be a callback that accepts and returns a single string.
- * @param bool|string $wikiId The ID of the target wiki database. Use false for the local wiki.
+ * @param bool|string $dbDomain Database domain ID. Use false for the local database domain.
* @param callable|null $insertCallback Callback to change insert fields accordingly.
* This parameter was introduced in 1.32
*/
$idField,
$nameField,
callable $normalizationCallback = null,
- $wikiId = false,
+ $dbDomain = false,
callable $insertCallback = null
) {
$this->loadBalancer = $dbLoadBalancer;
$this->idField = $idField;
$this->nameField = $nameField;
$this->normalizationCallback = $normalizationCallback;
- $this->wikiId = $wikiId;
+ $this->domain = $dbDomain;
$this->cacheTTL = IExpiringStore::TTL_MONTH;
$this->insertCallback = $insertCallback;
}
* @return IDatabase
*/
private function getDBConnection( $index, $flags = 0 ) {
- return $this->loadBalancer->getConnection( $index, [], $this->wikiId, $flags );
+ return $this->loadBalancer->getConnection( $index, [], $this->domain, $flags );
}
/**
return $this->cache->makeGlobalKey(
'NameTableSqlStore',
$this->table,
- $this->loadBalancer->resolveDomainID( $this->wikiId )
+ $this->loadBalancer->resolveDomainID( $this->domain )
);
}
/**
* Get the wiki ID of a database domain
*
- * This is like DatabaseDomain::getId() without encoding (for legacy reasons)
- * and without the schema if it merely set to the generic value "mediawiki"
+ * This is like DatabaseDomain::getId() without encoding (for legacy reasons) and
+ * without the schema if it is the generic installer default of "mediawiki"/"dbo"
+ *
+ * @see $wgDBmwschema
+ * @see PostgresInstaller
+ * @see MssqlInstaller
*
* @param string|DatabaseDomain $domain
* @return string
*/
public static function getWikiIdFromDbDomain( $domain ) {
$domain = DatabaseDomain::newFromId( $domain );
-
+ // Since the schema was not always part of the wiki ID, try to maintain backwards
+ // compatibility with some common cases. Assume that if the DB domain schema is just
+ // the installer default then it is probably the case that the schema is the same for
+ // all wikis in the farm. Historically, any wiki farm had to make the database/prefix
+ // combination unique per wiki. Ommit the schema if it does not seem wiki specific.
if ( !in_array( $domain->getSchema(), [ null, 'mediawiki', 'dbo' ], true ) ) {
- // Include the schema if it is set and is not the default placeholder.
// This means a site admin may have specifically taylored the schemas.
- // Domain IDs might use the form <DB>-<project>-<language>, meaning that
- // the schema portion must be accounted for to disambiguate wikis.
+ // Domain IDs might use the form <DB>-<project>- or <DB>-<project>-<language>_,
+ // meaning that the schema portion must be accounted for to disambiguate wikis.
return "{$domain->getDatabase()}-{$domain->getSchema()}-{$domain->getTablePrefix()}";
}
-
// Note that if this wiki ID is passed a a domain ID to LoadBalancer, then it can
// handle the schema by assuming the generic "mediawiki" schema if needed.
return strlen( $domain->getTablePrefix() )
/**
* @param DatabaseDomain|string $domain
- * @return bool Whether $domain has the same DB/prefix as the current wiki
+ * @return bool Whether $domain matches the DB domain of the current wiki
* @since 1.33
*/
public static function isCurrentWikiDbDomain( $domain ) {
- $domain = DatabaseDomain::newFromId( $domain );
- $curDomain = self::getCurrentWikiDbDomain();
-
- if ( !in_array( $curDomain->getSchema(), [ null, 'mediawiki', 'dbo' ], true ) ) {
- // Include the schema if it is set and is not the default placeholder.
- // This means a site admin may have specifically taylored the schemas.
- // Domain IDs might use the form <DB>-<project>-<language>, meaning that
- // the schema portion must be accounted for to disambiguate wikis.
- return $curDomain->equals( $domain );
- }
-
- return (
- $curDomain->getDatabase() === $domain->getDatabase() &&
- $curDomain->getTablePrefix() === $domain->getTablePrefix()
- );
+ return self::getCurrentWikiDbDomain()->equals( DatabaseDomain::newFromId( $domain ) );
}
/**
* @param string $wikiId
- * @return bool Whether $wikiId has the same DB/prefix as the current wiki
+ * @return bool Whether $wikiId matches the wiki ID of the current wiki
* @since 1.33
*/
public static function isCurrentWikiId( $wikiId ) {
}
// Add the general form.
- $action = htmlspecialchars( wfScript() );
$fields = [
[
'name' => 'title',
$htmlForm = HTMLForm::factory( 'ooui', $fields, $this->getContext() );
$htmlForm
->setMethod( 'get' )
- ->setAction( $action )
+ ->setAction( wfScript() )
->setId( 'mw-history-searchform' )
->setSubmitText( $this->msg( 'historyaction-submit' )->text() )
+ ->setWrapperAttributes( [ 'id' => 'mw-history-search' ] )
->setWrapperLegend( $this->msg( 'history-fieldset-title' )->text() );
$htmlForm->loadData();
$request = $this->getRequest();
// JSONP mode
- if ( $request->getVal( 'callback' ) !== null ) {
+ if ( $request->getCheck( 'callback' ) ) {
$this->lacksSameOriginSecurity = true;
return true;
}
foreach ( $updates as $update ) {
if ( $update instanceof EnqueueableDataUpdate ) {
$spec = $update->getAsJobSpecification();
- JobQueueGroup::singleton( $spec['wiki'] )->push( $spec['job'] );
+ $domain = $spec['domain'] ?? $spec['wiki'];
+ JobQueueGroup::singleton( $domain )->push( $spec['job'] );
} else {
$remaining[] = $update;
}
}
$this->fileExists = true;
- $this->maybeUpgradeRow();
}
/**
/**
* Upgrade a row if it needs it
*/
- function maybeUpgradeRow() {
+ protected function maybeUpgradeRow() {
global $wgUpdateCompatibleMetadata;
if ( wfReadOnly() || $this->upgrading ) {
*/
function purgeCache( $options = [] ) {
// Refresh metadata cache
+ $this->maybeUpgradeRow();
$this->purgeMetadataCache();
// Delete thumbnails
protected $mButtons = [];
protected $mWrapperLegend = false;
+ protected $mWrapperAttributes = [];
/**
* Salt for the edit token.
# Include a <fieldset> wrapper for style, if requested.
if ( $this->mWrapperLegend !== false ) {
$legend = is_string( $this->mWrapperLegend ) ? $this->mWrapperLegend : false;
- $html = Xml::fieldset( $legend, $html );
+ $html = Xml::fieldset( $legend, $html, $this->mWrapperAttributes );
}
return Html::rawElement(
return $this;
}
+ /**
+ * For internal use only. Use is discouraged, and should only be used where
+ * support for gadgets/user scripts is warranted.
+ * @param array $attributes
+ * @internal
+ * @return HTMLForm $this for chaining calls
+ */
+ public function setWrapperAttributes( $attributes ) {
+ $this->mWrapperAttributes = $attributes;
+
+ return $this;
+ }
+
/**
* Prompt the whole form to be wrapped in a "<fieldset>", with
* this message as its "<legend>" element.
'content' => new OOUI\HtmlSnippet( $html )
] ),
],
- ] );
+ ] + OOUI\Element::configFromHtmlAttributes( $this->mWrapperAttributes ) );
} else {
$content = new OOUI\HtmlSnippet( $html );
}
// Fetch the value in either one of the two following case:
// - we have a valid submit attempt (form was just submitted)
// - we have a value (an URL manually built by the user, or GET form with no wpFormIdentifier)
- if ( $this->isSubmitAttempt( $request ) || $request->getVal( $this->mName ) !== null ) {
+ if ( $this->isSubmitAttempt( $request ) || $request->getCheck( $this->mName ) ) {
return $invert
? !$request->getBool( $this->mName )
: $request->getBool( $this->mName );
}
foreach ( $namespaces as $namespace ) {
+ if ( $namespace < 0 ) {
+ return $this->msg( 'htmlform-select-badoption' );
+ }
+
$result = parent::validate( $namespace, $alldata );
if ( $result !== true ) {
return $result;
protected $title;
/** @var bool Expensive jobs may set this to true */
- protected $removeDuplicates;
+ protected $removeDuplicates = false;
/** @var string Text for error that occurred last */
protected $error;
* Create the appropriate object to handle a specific job
*
* @param string $command Job command
- * @param Title $title Associated title
* @param array $params Job parameters
* @throws InvalidArgumentException
* @return Job
*/
- public static function factory( $command, Title $title, $params = [] ) {
+ public static function factory( $command, $params = [] ) {
global $wgJobClasses;
+ if ( $params instanceof Title ) {
+ // Backwards compatibility for old signature ($command, $title, $params)
+ $title = $params;
+ $params = func_num_args() >= 3 ? func_get_arg( 2 ) : [];
+ } else {
+ // Subclasses can override getTitle() to return something more meaningful
+ $title = Title::makeTitle( NS_SPECIAL, 'Blankpage' );
+ }
+
if ( isset( $wgJobClasses[$command] ) ) {
$handler = $wgJobClasses[$command];
if ( $job instanceof Job ) {
$job->command = $command;
+
return $job;
} else {
- throw new InvalidArgumentException( "Cannot instantiate job '$command': bad spec!" );
+ throw new InvalidArgumentException( "Could instantiate job '$command': bad spec!" );
}
}
/**
* @param string $command
- * @param Title $title
- * @param array|bool $params Can not be === true
+ * @param array $params
*/
- public function __construct( $command, $title, $params = false ) {
+ public function __construct( $command, $params = [] ) {
+ if ( $params instanceof Title ) {
+ // Backwards compatibility for old signature ($command, $title, $params)
+ $title = $params;
+ $params = func_num_args() >= 3 ? func_get_arg( 2 ) : [];
+ } else {
+ // Subclasses can override getTitle() to return something more meaningful
+ $title = Title::makeTitle( NS_SPECIAL, 'Blankpage' );
+ }
+
$this->command = $command;
$this->title = $title;
$this->params = is_array( $params ) ? $params : []; // sanity
-
- // expensive jobs may set this to true
- $this->removeDuplicates = false;
-
if ( !isset( $this->params['requestId'] ) ) {
$this->params['requestId'] = WebRequest::getRequestId();
}
* @deprecated 1.33
*/
final public function getWiki() {
- return $this->domain;
+ return WikiMap::getWikiIdFromDbDomain( $this->domain );
}
/**
$this->type = $type;
$this->params = $params;
- $this->title = $title ?: Title::makeTitle( NS_SPECIAL, 'Badtitle/' . static::class );
+ $this->title = $title ?: Title::makeTitle( NS_SPECIAL, 'Blankpage' );
$this->opts = $opts;
}
public static function newFromLocalJobs( $jobs ) {
$jobs = is_array( $jobs ) ? $jobs : [ $jobs ];
- return self::newFromJobsByWiki( [ wfWikiID() => $jobs ] );
+ return self::newFromJobsByDomain( [
+ WikiMap::getCurrentWikiDbDomain()->getId() => $jobs
+ ] );
}
/**
- * @param array $jobsByWiki Map of (wiki => JobSpecification list)
+ * @param array $jobsByDomain Map of (wiki => JobSpecification list)
* @return EnqueueJob
*/
- public static function newFromJobsByWiki( array $jobsByWiki ) {
+ public static function newFromJobsByDomain( array $jobsByDomain ) {
$deduplicate = true;
- $jobMapsByWiki = [];
- foreach ( $jobsByWiki as $wiki => $jobs ) {
- $jobMapsByWiki[$wiki] = [];
+ $jobMapsByDomain = [];
+ foreach ( $jobsByDomain as $domain => $jobs ) {
+ $jobMapsByDomain[$domain] = [];
foreach ( $jobs as $job ) {
if ( $job instanceof JobSpecification ) {
- $jobMapsByWiki[$wiki][] = $job->toSerializableArray();
+ $jobMapsByDomain[$domain][] = $job->toSerializableArray();
} else {
throw new InvalidArgumentException( "Jobs must be of type JobSpecification." );
}
$eJob = new self(
Title::makeTitle( NS_SPECIAL, 'Badtitle/' . __CLASS__ ),
- [ 'jobsByWiki' => $jobMapsByWiki ]
+ [ 'jobsByDomain' => $jobMapsByDomain ]
);
// If *all* jobs to be pushed are to be de-duplicated (a common case), then
// de-duplicate this whole job itself to avoid build up in high traffic cases
return $eJob;
}
+ /**
+ * @param array $jobsByWiki
+ * @return EnqueueJob
+ * @deprecated Since 1.33; use newFromJobsByDomain()
+ */
+ public static function newFromJobsByWiki( array $jobsByWiki ) {
+ return self::newFromJobsByDomain( $jobsByWiki );
+ }
+
public function run() {
- foreach ( $this->params['jobsByWiki'] as $wiki => $jobMaps ) {
+ $jobsByDomain = $this->params['jobsByDomain'] ?? $this->params['jobsByWiki']; // b/c
+
+ foreach ( $jobsByDomain as $domain => $jobMaps ) {
$jobSpecs = [];
foreach ( $jobMaps as $jobMap ) {
$jobSpecs[] = JobSpecification::newFromArray( $jobMap );
}
- JobQueueGroup::singleton( $wiki )->push( $jobSpecs );
+ JobQueueGroup::singleton( $domain )->push( $jobSpecs );
}
return true;
/**
* This is a wrapper for APC's shared memory functions
*
+ * Use PHP serialization to avoid bugs and easily create CAS tokens.
+ * APCu has a memory corruption bug when the serializer is set to 'default'.
+ * See T120267, and upstream bug reports:
+ * - https://github.com/krakjoe/apcu/issues/38
+ * - https://github.com/krakjoe/apcu/issues/35
+ * - https://github.com/krakjoe/apcu/issues/111
+ *
* @ingroup Cache
*/
class APCBagOStuff extends BagOStuff {
-
- /**
- * @var bool If true, trust the APC implementation to serialize and
- * deserialize objects correctly. If false, (de-)serialize in PHP.
- */
- protected $nativeSerialize;
-
/**
* @var string String to append to each APC key. This may be changed
* whenever the handling of values is changed, to prevent existing code
* from encountering older values which it cannot handle.
*/
- const KEY_SUFFIX = ':2';
-
- /**
- * Available parameters are:
- * - nativeSerialize: If true, pass objects to apc_store(), and trust it
- * to serialize them correctly. If false, serialize
- * all values in PHP.
- *
- * @param array $params
- */
- public function __construct( array $params = [] ) {
- parent::__construct( $params );
-
- if ( isset( $params['nativeSerialize'] ) ) {
- $this->nativeSerialize = $params['nativeSerialize'];
- } elseif ( extension_loaded( 'apcu' ) && ini_get( 'apc.serializer' ) === 'default' ) {
- // APCu has a memory corruption bug when the serializer is set to 'default'.
- // See T120267, and upstream bug reports:
- // - https://github.com/krakjoe/apcu/issues/38
- // - https://github.com/krakjoe/apcu/issues/35
- // - https://github.com/krakjoe/apcu/issues/111
- $this->logger->warning(
- 'The APCu extension is loaded and the apc.serializer INI setting ' .
- 'is set to "default". This can cause memory corruption! ' .
- 'You should change apc.serializer to "php" instead. ' .
- 'See <https://github.com/krakjoe/apcu/issues/38>.'
- );
- $this->nativeSerialize = false;
- } else {
- $this->nativeSerialize = true;
- }
- }
+ const KEY_SUFFIX = ':3';
protected function doGet( $key, $flags = 0, &$casToken = null ) {
$casToken = null;
}
protected function serialize( $value ) {
- if ( !$this->nativeSerialize && !$this->isInteger( $value ) ) {
- $value = serialize( $value );
- }
- return $value;
+ return $this->isInteger( $value ) ? (int)$value : serialize( $value );
}
protected function unserialize( $value ) {
- if ( is_string( $value ) && !$this->nativeSerialize ) {
- $value = $this->isInteger( $value )
- ? intval( $value )
- : unserialize( $value );
- }
- return $value;
+ return $this->isInteger( $value ) ? (int)$value : unserialize( $value );
}
}
/**
* This is a wrapper for APCU's shared memory functions
*
+ * Use PHP serialization to avoid bugs and easily create CAS tokens.
+ * APCu has a memory corruption bug when the serializer is set to 'default'.
+ * See T120267, and upstream bug reports:
+ * - https://github.com/krakjoe/apcu/issues/38
+ * - https://github.com/krakjoe/apcu/issues/35
+ * - https://github.com/krakjoe/apcu/issues/111
+ *
* @ingroup Cache
*/
-class APCUBagOStuff extends APCBagOStuff {
+class APCUBagOStuff extends BagOStuff {
/**
- * Available parameters are:
- * - nativeSerialize: If true, pass objects to apcu_store(), and trust it
- * to serialize them correctly. If false, serialize
- * all values in PHP.
- *
- * @param array $params
+ * @var string String to append to each APC key. This may be changed
+ * whenever the handling of values is changed, to prevent existing code
+ * from encountering older values which it cannot handle.
*/
- public function __construct( array $params = [] ) {
- parent::__construct( $params );
- }
+ const KEY_SUFFIX = ':3';
protected function doGet( $key, $flags = 0, &$casToken = null ) {
$casToken = null;
return false;
}
}
+
+ protected function serialize( $value ) {
+ return $this->isInteger( $value ) ? (int)$value : serialize( $value );
+ }
+
+ protected function unserialize( $value ) {
+ return $this->isInteger( $value ) ? (int)$value : unserialize( $value );
+ }
}
}
public function dbSchema( $schema = null ) {
+ if ( strlen( $schema ) && $this->getDBname() === null ) {
+ throw new DBUnexpectedError( $this, "Cannot set schema to '$schema'; no database set." );
+ }
+
$old = $this->currentDomain->getSchema();
if ( $schema !== null ) {
$this->currentDomain = new DatabaseDomain(
$this->database = $database;
if ( $schema !== null && ( !is_string( $schema ) || $schema === '' ) ) {
throw new InvalidArgumentException( 'Schema must be null or a non-empty string.' );
+ } elseif ( $database === null && $schema !== null ) {
+ throw new InvalidArgumentException( 'Schema must be null if database is null.' );
}
$this->schema = $schema;
if ( !is_string( $prefix ) ) {
* Check whether the domain $other meets the specifications of this domain
*
* If this instance has a null database specifier, then $other can have any database
- * specified, including the null, and likewise if the schema specifier is null. This
- * is not transitive like equals() since a domain that explicitly wants a certain
+ * specifier, including null. This is likewise true if the schema specifier is null.
+ * This is not transitive like equals() since a domain that explicitly wants a certain
* database or schema cannot be satisfied by one of another (nor null). If the prefix
* is empty and the DB and schema are both null, then the entire domain is considered
* unspecified, and any prefix of $other is considered compatible.
$ser = $image->getMetadata();
if ( $ser ) {
$metadata = unserialize( $ser );
-
- return $image->getWidth() * $image->getHeight() * $metadata['frameCount'];
+ if ( isset( $metadata['frameCount'] ) && $metadata['frameCount'] > 0 ) {
+ return $image->getWidth() * $image->getHeight() * $metadata['frameCount'];
+ } else {
+ return $image->getWidth() * $image->getHeight();
+ }
} else {
return $image->getWidth() * $image->getHeight();
}
$ser = $image->getMetadata();
if ( $ser ) {
$metadata = unserialize( $ser );
- if ( $metadata['frameCount'] > 1 ) {
+ if ( isset( $metadata['frameCount'] ) && $metadata['frameCount'] > 1 ) {
return true;
}
}
// Add 'local' source first
$this->addSource( 'local', $config->get( 'LoadScript' ) );
- // Add other sources
- $this->addSource( $config->get( 'ResourceLoaderSources' ) );
-
// Register core modules
$this->register( include "$IP/resources/Resources.php" );
// Register extension modules
use Wikimedia\WrappedStringList;
/**
- * Bootstrap a ResourceLoader client on an HTML page.
+ * Load and configure a ResourceLoader client on an HTML page.
*
* @since 1.28
*/
$this->load();
// TODO: This performs database actions on GET request, which is going to
// be a problem for our multi-datacenter work.
- if ( !is_null( $request->getVal( 'nsRemember' ) ) ) {
+ if ( $request->getCheck( 'nsRemember' ) ) {
$this->saveNamespaces();
// Remove the token from the URL to prevent the user from inadvertently
// exposing it (e.g. by pasting it into a public wiki page) or undoing
}
$this->searchEngineType = $request->getVal( 'srbackend' );
- if (
- !$request->getVal( 'fulltext' ) &&
- $request->getVal( 'offset' ) === null
- ) {
+ if ( !$request->getVal( 'fulltext' ) && !$request->getCheck( 'offset' ) ) {
$url = $this->goResult( $term );
if ( $url !== null ) {
// successful 'go'
"right-reupload-own": "Overwrite existing files uploaded by oneself",
"right-reupload-shared": "Override files on the shared media repository locally",
"right-upload_by_url": "Upload files from a URL",
- "right-purge": "Purge the site cache for a page without confirmation",
+ "right-purge": "Purge the site cache for a page",
"right-autoconfirmed": "Not be affected by IP-based rate limits",
"right-bot": "Be treated as an automated process",
"right-nominornewtalk": "Not have minor edits to discussion pages trigger the new messages prompt",
"right-reupload-own": "{{doc-right|reupload-own}}\nRight to upload a file under a file name that already exists, and that the same user has uploaded.\n\nRelated messages:\n* {{msg-mw|right-upload}}\n* {{msg-mw|right-reupload}}",
"right-reupload-shared": "{{doc-right|reupload-shared}}\nThe right to upload a file locally under a file name that already exists in a shared database (for example Commons).\n\nRelated messages:\n* {{msg-mw|right-upload}}\n* {{msg-mw|right-reupload}}",
"right-upload_by_url": "{{doc-right|upload_by_url}}",
- "right-purge": "{{doc-right|purge}}\nThe right to use <code>&action=purge</code> in the URL, without needing to confirm it (by default, anonymous users need to confirm it).",
+ "right-purge": "{{doc-right|purge}}\nThe right to use <code>&action=purge</code> in the URL.",
"right-autoconfirmed": "{{doc-right|autoconfirmed}}\nIf your account is older than [[mw:Manual:$wgAutoConfirmAge|wgAutoConfirmAge]] and if you have at least [[mw:Manual:$wgAutoConfirmCount|$wgAutoConfirmCount]] edits, you are in the '''group \"autoconfirmed\"''' (note that you can't see this group at [[Special:ListUsers]]).\nIf you are in that group, you have (by default) the '''right \"autoconfirmed\"''', which exempts you from certain rate limits (those based on your IP address or otherwise intended solely for new users). Other rate limits may still apply; see {{msg-mw|right-noratelimit}}.",
"right-bot": "{{doc-right|bot}}",
"right-nominornewtalk": "{{doc-right|nominornewtalk}}\nIf someone with this right (bots by default) edits a user talk page and marks it as minor (requires {{msg-mw|right-minoredit}}), the user will not get a notification \"You have new messages\".",
* is the execute() method. See docs/maintenance.txt for more info
* and a quick demo of how to use it.
*
+ * Terminology:
+ * params: registry of named values that may be passed to the script
+ * arg list: registry of positional values that may be passed to the script
+ * options: passed param values
+ * args: passed positional values
+ *
+ * In the command:
+ * mwscript somescript.php --foo=bar baz
+ * foo is a param
+ * bar is the option value of the option for param foo
+ * baz is the arg value at index 0 in the arg list
+ *
* @since 1.16
* @ingroup Maintenance
*/
// Const for getStdin()
const STDIN_ALL = 'all';
- // This is the desired params
+ // Array of desired/allowed params
protected $mParams = [];
// Array of mapping short parameters to long ones
protected $mShortParamsMap = [];
- // Array of desired args
+ // Array of desired/allowed args
protected $mArgList = [];
// This is the list of options that were actually passed
}
$this->loadParamsAndArgs();
- $this->maybeHelp();
# Set the memory limit
# Note we need to set it again later in cache LocalSettings changed it
while ( ob_get_level() > 0 ) {
ob_end_flush();
}
-
- $this->validateParamsAndArgs();
}
/**
/**
* Run some validation checks on the params, etc
*/
- protected function validateParamsAndArgs() {
+ public function validateParamsAndArgs() {
$die = false;
# Check to make sure we've got all the required options
foreach ( $this->mParams as $opt => $info ) {
}
}
- if ( $die ) {
- $this->maybeHelp( true );
- }
+ $this->maybeHelp( $die );
}
/**
// This avoids having long running scripts just OOM and lose all the updates.
$maintenance->setAgentAndTriggers();
+$maintenance->validateParamsAndArgs();
+
// Do the work
$success = $maintenance->execute();
$this->addOption( 'format', implode( ', ', self::$outFormats ), false, true );
}
- protected function validateParamsAndArgs() {
+ public function validateParamsAndArgs() {
$error_out = false;
# Get the format and make sure it is set to a valid default value
'manualRecache' => false,
];
}
+
+ public function validateParamsAndArgs() {
+ // Allow extensions to add additional params.
+ $params = [];
+ Hooks::run( 'MaintenanceUpdateAddParams', [ &$params ] );
+ foreach ( $params as $name => $param ) {
+ $this->addOption(
+ $name,
+ $param['desc'],
+ $param['require'] ?? false,
+ $param['withArg'] ?? false,
+ $param['shortName'] ?? false,
+ $param['multiOccurrence'] ?? false
+ );
+ }
+
+ parent::validateParamsAndArgs();
+ }
}
$maintClass = UpdateMediaWiki::class;
$session = array();
}
- if ( $request->getVal( 'uselang' ) !== null ) {
+ if ( $request->getCheck( 'uselang' ) ) {
$langCode = $request->getVal( 'uselang' );
} elseif ( isset( $session['settings']['_UserLang'] ) ) {
$langCode = $session['settings']['_UserLang'];
/* MediaWiki Special pages */
'mediawiki.rcfilters.filters.base.styles' => [
- 'styles' => [
- 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less',
+ 'skinStyles' => [
+ 'default' => 'resources/src/mediawiki.rcfilters/styles/mw.rcfilters.less',
],
],
'mediawiki.rcfilters.highlightCircles.seenunseen.styles' => [
- 'styles' => [
- 'resources/src/mediawiki.rcfilters/' .
- 'styles/mw.rcfilters.ui.ChangesListWrapperWidget.highlightCircles.seenunseen.less',
+ 'skinStyles' => [
+ 'default' => [
+ 'resources/src/mediawiki.rcfilters/' .
+ 'styles/mw.rcfilters.ui.ChangesListWrapperWidget.highlightCircles.seenunseen.less',
+ ],
],
],
'mediawiki.rcfilters.filters.dm' => [
+/*!
+ * Styles for Special:ApiSandbox
+ */
.client-js .mw-apisandbox-nojs {
display: none;
}
-/* Special:Block styles */
+/*!
+ * Styles for Special:Block
+ */
-// OOUIHTMLForm styles
-@ooui-font-size-browser: 16; // Assumed browser default of `16px`
-@ooui-font-size-base: 0.875em; // Equals `14px` at browser default of `16px`
+// OOUIHTMLForm specifics
+@ooui-font-size-browser: 16; // Assumed browser default of `16px`.
+@ooui-font-size-base: 0.875em; // Equals `14px` at browser default of `16px`.
-@ooui-spacing-radio-label: 26 / @ooui-font-size-browser / @ooui-font-size-base; // Equals `1.85714286em`≈`26px`
+@ooui-spacing-radio-label: 26 / @ooui-font-size-browser / @ooui-font-size-base; // Equals `1.85714286em`≈`26px`.
body.mw-special-Block {
.mw-block-editing-restriction.oo-ui-fieldLayout {
+/*!
+ * Styles for Special:ComparePages
+ */
@import 'mediawiki.mixins';
.mw-special-ComparePages .mw-htmlform-ooui-wrapper {
/*!
- * Styling for Special:EditTags and action=editchangetags
+ * Styles for Special:EditTags and action=editchangetags
*/
#mw-edittags-tags-selector td {
vertical-align: top;
/*!
- * Styling for Special:NewPages
+ * Styles for Special:NewPages
*/
// OOUIHTMLForm styles
-/* Distinguish actual data from information about it being hidden visually */
+/*!
+ * Styles for Special:PagesWithProp
+ */
+
+/* Distinguish actual data from information about it being hidden visually. */
.prop-value-hidden {
font-style: italic;
}
-/* Special:AllMessages */
+/*!
+ * Styles shared across various special pages.
+ */
@import 'mediawiki.mixins';
+/* Special:AllMessages */
/* Visually hide repeating text, but leave in for better form navigation on screen readers */
.mw-special-Allmessages .mw-htmlform-ooui .oo-ui-fieldsetLayout:first-child .oo-ui-fieldsetLayout-header {
.mixin-screen-reader-text();
/*!
- * Styling for Special:Upload
+ * Styles for Special:Upload
*/
.mw-destfile-warning {
border: 1px solid #fde29b;
/*!
- * Styling for Special:UserRights
+ * Styles for Special:UserRights
*/
.mw-userrights-nested {
margin-left: 1.2em;
/*!
- * Styling for elements generated by JavaScript on Special:Watchlist
+ * Styles for elements generated by JavaScript on Special:Watchlist
*/
.mw-changelist-line-inner-unwatched {
text-decoration: line-through;
// Avoid influence from wgInvalidateCacheOnLocalSettingsChange
'CacheEpoch' => '20140101000000',
- // For ResourceLoader::__construct()
- 'ResourceLoaderSources' => [],
-
// For wfScript()
'ScriptPath' => '/w',
'Script' => '/w/index.php',
[
'page',
'revision',
+ 'comment',
'ip_changes',
'text',
'archive',
$this->setService( 'MainWANObjectCache', $cache );
$db = wfGetDB( DB_MASTER );
+ $now = 1553893742;
+ $cache->setMockTime( $now );
+
// Get a fresh revision to use during testing
$this->testPage->doEditContent( new WikitextContent( __METHOD__ ), __METHOD__ );
$rev = $this->testPage->getRevision();
$cache->delete( $key, WANObjectCache::HOLDOFF_NONE );
$this->assertFalse( $cache->get( $key ) );
+ ++$now;
+
// Get the new revision and make sure it is in the cache and correct
$newRev = Revision::newKnownCurrent( $db, $rev->getPage(), $rev->getId() );
$this->assertRevEquals( $rev, $newRev );
* @covers WikiMap::getCurrentWikiDbDomain()
*/
public function testIsCurrentWikiDomain() {
- $this->assertTrue( WikiMap::isCurrentWikiDbDomain( wfWikiID() ) );
+ $this->setMwGlobals( 'wgDBmwschema', 'mediawiki' );
- $localDomain = DatabaseDomain::newFromId( wfWikiID() );
+ $localDomain = WikiMap::getCurrentWikiDbDomain()->getId();
+ $this->assertTrue( WikiMap::isCurrentWikiDbDomain( $localDomain ) );
+
+ $localDomain = DatabaseDomain::newFromId( $localDomain );
$domain1 = new DatabaseDomain(
$localDomain->getDatabase(), 'someschema', $localDomain->getTablePrefix() );
$domain2 = new DatabaseDomain(
$localDomain->getDatabase(), null, $localDomain->getTablePrefix() );
- $this->assertTrue( WikiMap::isCurrentWikiDbDomain( $domain1 ), 'Schema ignored' );
- $this->assertTrue( WikiMap::isCurrentWikiDbDomain( $domain2 ), 'Schema ignored' );
+ $this->assertFalse( WikiMap::isCurrentWikiDbDomain( $domain1 ), 'Schema not ignored' );
+ $this->assertFalse( WikiMap::isCurrentWikiDbDomain( $domain2 ), 'Null schema not ignored' );
$this->assertTrue( WikiMap::isCurrentWikiDbDomain( WikiMap::getCurrentWikiDbDomain() ) );
}
private function newJobQueue() {
return JobQueue::factory( [
'class' => JobQueueMemory::class,
- 'wiki' => wfWikiID(),
+ 'domain' => WikiMap::getCurrentWikiDbDomain()->getId(),
'type' => 'null',
] );
}
$baseConfig = [ 'class' => JobQueueDBSingle::class ];
}
$baseConfig['type'] = 'null';
- $baseConfig['wiki'] = wfWikiID();
+ $baseConfig['domain'] = WikiMap::getCurrentWikiDbDomain()->getId();
$variants = [
'queueRand' => [ 'order' => 'random', 'claimTTL' => 0 ],
'queueRandTTL' => [ 'order' => 'random', 'claimTTL' => 10 ],
$this->markTestSkipped( $desc );
}
$this->assertEquals( wfWikiID(), $queue->getWiki(), "Proper wiki ID ($desc)" );
- $this->assertEquals( wfWikiID(), $queue->getDomain(), "Proper wiki ID ($desc)" );
+ $this->assertEquals(
+ WikiMap::getCurrentWikiDbDomain()->getId(),
+ $queue->getDomain(),
+ "Proper wiki ID ($desc)" );
}
/**
$this->assertEquals( $n, $calls );
}
- /**
- * @covers BagOStuff::merge
- * @dataProvider provideTestMerge_fork
- */
- public function testMerge_fork( $exists, $childWins, $resCAS ) {
- $key = $this->cache->makeKey( self::TEST_KEY );
- $pCallback = function ( BagOStuff $cache, $key, $oldVal ) {
- return ( $oldVal === false ) ? 'init-parent' : $oldVal . '-merged-parent';
- };
- $cCallback = function ( BagOStuff $cache, $key, $oldVal ) {
- return ( $oldVal === false ) ? 'init-child' : $oldVal . '-merged-child';
- };
-
- if ( $exists ) {
- $this->cache->set( $key, 'x', 5 );
- }
-
- /*
- * Test concurrent merges by forking this process, if:
- * - not manually called with --use-bagostuff
- * - pcntl_fork is supported by the system
- * - cache type will correctly support calls over forks
- */
- $fork = (bool)$this->getCliArg( 'use-bagostuff' );
- $fork &= function_exists( 'pcntl_fork' );
- $fork &= !$this->cache instanceof HashBagOStuff;
- $fork &= !$this->cache instanceof EmptyBagOStuff;
- $fork &= !$this->cache instanceof MultiWriteBagOStuff;
- if ( $fork ) {
- $pid = null;
- // Function to start merge(), run another merge() midway through, then finish
- $func = function ( $cache, $key, $cur ) use ( $pCallback, $cCallback, &$pid ) {
- $pid = pcntl_fork();
- if ( $pid == -1 ) {
- return false;
- } elseif ( $pid ) {
- pcntl_wait( $status );
-
- return $pCallback( $cache, $key, $cur );
- } else {
- $this->cache->merge( $key, $cCallback, 0, 1 );
- // Bail out of the outer merge() in the child process since it does not
- // need to attempt to write anything. Success is checked by the parent.
- parent::tearDown(); // avoid phpunit notices
- exit;
- }
- };
-
- // attempt a merge - this should fail
- $merged = $this->cache->merge( $key, $func, 0, 1 );
-
- if ( $pid == -1 ) {
- return; // can't fork, ignore this test...
- }
-
- // merge has failed because child process was merging (and we only attempted once)
- $this->assertEquals( !$childWins, $merged );
- $this->assertEquals( $this->cache->get( $key ), $resCAS );
- } else {
- $this->markTestSkipped( 'No pcntl methods available' );
- }
- }
-
- function provideTestMerge_fork() {
- return [
- // (already exists, child wins CAS, result of CAS)
- [ false, true, 'init-child' ],
- [ true, true, 'x-merged-child' ]
- ];
- }
-
/**
* @covers BagOStuff::changeTTL
*/
[ '', null, null, '', true ],
'dontcaredb+dontcaredbschema+prefix' =>
[ 'mywiki-mediawiki-prefix_', null, null, 'prefix_', false ],
- 'dontcaredb+schema+prefix' =>
- [ 'mywiki-schema-prefix_', null, 'schema', 'prefix_', false ],
'db+dontcareschema+prefix' =>
[ 'mywiki-schema-prefix_', 'mywiki', null, 'prefix_', false ],
'postgres-db-jobqueue' =>
[ 'mywiki-schema-prefix_', 'thatwiki', 'schema', 'prefix_' ],
'dontcaredb+dontcaredbschema+prefix' =>
[ 'thatwiki-mediawiki-otherprefix_', null, null, 'prefix_' ],
- 'dontcaredb+schema+prefix' =>
- [ 'mywiki-otherschema-prefix_', null, 'schema', 'prefix_' ],
'db+dontcareschema+prefix' =>
[ 'notmywiki-schema-prefix_', 'mywiki', null, 'prefix_' ],
];
$this->assertFalse( $fromId->isCompatible( $compareIdObj ), 'fromId equals string' );
}
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testSchemaWithNoDB1() {
+ new DatabaseDomain( null, 'schema', '' );
+ }
+
+ /**
+ * @expectedException InvalidArgumentException
+ */
+ public function testSchemaWithNoDB2() {
+ DatabaseDomain::newFromId( '-schema-prefix' );
+ }
+
/**
* @covers Wikimedia\Rdbms\DatabaseDomain::isUnspecified
*/
use Wikimedia\Rdbms\DBUnexpectedError;
class DatabaseTest extends PHPUnit\Framework\TestCase {
+ /** @var DatabaseTestHelper */
+ private $db;
use MediaWikiCoversValidator;
* @covers Wikimedia\Rdbms\Database::dbSchema
*/
public function testSchemaAndPrefixMutators() {
+ $ud = DatabaseDomain::newUnspecified();
+
+ $this->assertEquals( $ud->getId(), $this->db->getDomainID() );
+
$old = $this->db->tablePrefix();
$oldDomain = $this->db->getDomainId();
$this->assertInternalType( 'string', $old, 'Prefix is string' );
$oldDomain = $this->db->getDomainId();
$this->assertInternalType( 'string', $old, 'Schema is string' );
$this->assertSame( $old, $this->db->dbSchema(), "Schema unchanged" );
+
+ $this->db->selectDB( 'y' );
$this->assertSame( $old, $this->db->dbSchema( 'xxx' ) );
$this->assertSame( 'xxx', $this->db->dbSchema(), "Schema set" );
$this->db->dbSchema( $old );
$this->assertNotEquals( 'xxx', $this->db->dbSchema() );
- $this->assertSame( $oldDomain, $this->db->getDomainId() );
+ $this->assertSame( "y", $this->db->getDomainId() );
+ }
+
+ /**
+ * @covers Wikimedia\Rdbms\Database::tablePrefix
+ * @covers Wikimedia\Rdbms\Database::dbSchema
+ * @expectedException DBUnexpectedError
+ */
+ public function testSchemaWithNoDB() {
+ $ud = DatabaseDomain::newUnspecified();
+
+ $this->assertEquals( $ud->getId(), $this->db->getDomainID() );
+ $this->assertSame( '', $this->db->dbSchema() );
+
+ $this->db->dbSchema( 'xxx' );
}
/**
/**
* @group ResourceLoader
+ * @covers ResourceLoaderClientHtml
*/
class ResourceLoaderClientHtmlTest extends PHPUnit\Framework\TestCase {
use MediaWikiCoversValidator;
- protected static function expandVariables( $text ) {
- return strtr( $text, [
- '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
- ] );
- }
-
- protected static function makeContext( $extraQuery = [] ) {
- $conf = new HashConfig( [
- 'ResourceLoaderSources' => [],
- 'ResourceModuleSkinStyles' => [],
- 'ResourceModules' => [],
- 'EnableJavaScriptTest' => false,
- 'LoadScript' => '/w/load.php',
- ] );
- return new ResourceLoaderContext(
- new ResourceLoader( $conf ),
- new FauxRequest( array_merge( [
- 'lang' => 'nl',
- 'skin' => 'fallback',
- 'user' => 'Example',
- 'target' => 'phpunit',
- ], $extraQuery ) )
- );
- }
-
- protected static function makeModule( array $options = [] ) {
- return new ResourceLoaderTestModule( $options );
- }
-
- protected static function makeSampleModules() {
- $modules = [
- 'test' => [],
- 'test.private' => [ 'group' => 'private' ],
- 'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
- 'test.shouldembed' => [ 'shouldEmbed' => true ],
- 'test.user' => [ 'group' => 'user' ],
-
- 'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ],
- 'test.styles.mixed' => [],
- 'test.styles.noscript' => [
- 'type' => ResourceLoaderModule::LOAD_STYLES,
- 'group' => 'noscript',
- ],
- 'test.styles.user' => [
- 'type' => ResourceLoaderModule::LOAD_STYLES,
- 'group' => 'user',
- ],
- 'test.styles.user.empty' => [
- 'type' => ResourceLoaderModule::LOAD_STYLES,
- 'group' => 'user',
- 'isKnownEmpty' => true,
- ],
- 'test.styles.private' => [
- 'type' => ResourceLoaderModule::LOAD_STYLES,
- 'group' => 'private',
- 'styles' => '.private{}',
- ],
- 'test.styles.shouldembed' => [
- 'type' => ResourceLoaderModule::LOAD_STYLES,
- 'shouldEmbed' => true,
- 'styles' => '.shouldembed{}',
- ],
- 'test.styles.deprecated' => [
- 'type' => ResourceLoaderModule::LOAD_STYLES,
- 'deprecated' => 'Deprecation message.',
- ],
-
- 'test.scripts' => [],
- 'test.scripts.user' => [ 'group' => 'user' ],
- 'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
- 'test.scripts.raw' => [ 'isRaw' => true ],
- 'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
-
- 'test.ordering.a' => [ 'shouldEmbed' => false ],
- 'test.ordering.b' => [ 'shouldEmbed' => false ],
- 'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ],
- 'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ],
- 'test.ordering.e' => [ 'shouldEmbed' => false ],
- ];
- return array_map( function ( $options ) {
- return self::makeModule( $options );
- }, $modules );
- }
-
- /**
- * @covers ResourceLoaderClientHtml::getDocumentAttributes
- */
- public function testGetDocumentAttributes() {
- $client = new ResourceLoaderClientHtml( self::makeContext() );
- $this->assertInternalType( 'array', $client->getDocumentAttributes() );
- }
-
- /**
- * @covers ResourceLoaderClientHtml::__construct
- * @covers ResourceLoaderClientHtml::setModules
- * @covers ResourceLoaderClientHtml::setModuleStyles
- * @covers ResourceLoaderClientHtml::getData
- * @covers ResourceLoaderClientHtml::getContext
- */
public function testGetData() {
$context = self::makeContext();
$context->getResourceLoader()->register( self::makeSampleModules() );
$expected = [
'states' => [
+ // The below are NOT queued for loading via `mw.loader.load(Array)`.
+ // Instead we tell the client to set their state to "loading" so that
+ // if they are needed as dependencies, the client will not try to
+ // load them on-demand, because the server is taking care of them already.
+ // Either:
+ // - Embedded as inline scripts in the HTML (e.g. user-private code, and
+ // previews). Once that script tag is reached, the state is "loaded".
+ // - Loaded directly from the HTML with a dedicated HTTP request (e.g.
+ // user scripts, which vary by a 'user' and 'version' parameter that
+ // the static user-agnostic startup module won't have).
'test.private' => 'loading',
- 'test.shouldembed.empty' => 'ready',
'test.shouldembed' => 'loading',
'test.user' => 'loading',
+ // The below are known to the server to be empty scripts, or to be
+ // synchronously loaded stylesheets. These start in the "ready" state.
+ 'test.shouldembed.empty' => 'ready',
'test.styles.pure' => 'ready',
'test.styles.user.empty' => 'ready',
'test.styles.private' => 'ready',
$this->assertEquals( $expected, $access->getData() );
}
- /**
- * @covers ResourceLoaderClientHtml::setConfig
- * @covers ResourceLoaderClientHtml::setExemptStates
- * @covers ResourceLoaderClientHtml::getHeadHtml
- * @covers ResourceLoaderClientHtml::getLoad
- * @covers ResourceLoader::makeLoaderStateScript
- */
public function testGetHeadHtml() {
$context = self::makeContext();
$context->getResourceLoader()->register( self::makeSampleModules() );
/**
* Confirm that 'target' is passed down to the startup module's load url.
- *
- * @covers ResourceLoaderClientHtml::getHeadHtml
*/
public function testGetHeadHtmlWithTarget() {
$client = new ResourceLoaderClientHtml(
/**
* Confirm that 'safemode' is passed down to startup.
- *
- * @covers ResourceLoaderClientHtml::getHeadHtml
*/
public function testGetHeadHtmlWithSafemode() {
$client = new ResourceLoaderClientHtml(
/**
* Confirm that a null 'target' is the same as no target.
- *
- * @covers ResourceLoaderClientHtml::getHeadHtml
*/
public function testGetHeadHtmlWithNullTarget() {
$client = new ResourceLoaderClientHtml(
$this->assertEquals( $expected, $client->getHeadHtml() );
}
- /**
- * @covers ResourceLoaderClientHtml::getBodyHtml
- * @covers ResourceLoaderClientHtml::getLoad
- */
public function testGetBodyHtml() {
$context = self::makeContext();
$context->getResourceLoader()->register( self::makeSampleModules() );
/**
* @dataProvider provideMakeLoad
- * @covers ResourceLoaderClientHtml::makeLoad
- * @covers ResourceLoaderClientHtml::makeContext
- * @covers ResourceLoader::makeModuleResponse
+ * @covers ResourceLoaderClientHtml
* @covers ResourceLoaderModule::getModuleContent
- * @covers ResourceLoader::getCombinedVersion
- * @covers ResourceLoader::createLoaderURL
- * @covers ResourceLoader::createLoaderQuery
- * @covers ResourceLoader::makeLoaderQuery
- * @covers ResourceLoader::makeInlineScript
+ * @covers ResourceLoader
*/
public function testMakeLoad(
array $contextQuery,
$expected = self::expandVariables( $expected );
$this->assertEquals( $expected, (string)$actual );
}
+
+ public function testGetDocumentAttributes() {
+ $client = new ResourceLoaderClientHtml( self::makeContext() );
+ $this->assertInternalType( 'array', $client->getDocumentAttributes() );
+ }
+
+ private static function expandVariables( $text ) {
+ return strtr( $text, [
+ '{blankVer}' => ResourceLoaderTestCase::BLANK_VERSION
+ ] );
+ }
+
+ private static function makeContext( $extraQuery = [] ) {
+ $conf = new HashConfig( [
+ 'ResourceModuleSkinStyles' => [],
+ 'ResourceModules' => [],
+ 'EnableJavaScriptTest' => false,
+ 'LoadScript' => '/w/load.php',
+ ] );
+ return new ResourceLoaderContext(
+ new ResourceLoader( $conf ),
+ new FauxRequest( array_merge( [
+ 'lang' => 'nl',
+ 'skin' => 'fallback',
+ 'user' => 'Example',
+ 'target' => 'phpunit',
+ ], $extraQuery ) )
+ );
+ }
+
+ private static function makeModule( array $options = [] ) {
+ return new ResourceLoaderTestModule( $options );
+ }
+
+ private static function makeSampleModules() {
+ $modules = [
+ 'test' => [],
+ 'test.private' => [ 'group' => 'private' ],
+ 'test.shouldembed.empty' => [ 'shouldEmbed' => true, 'isKnownEmpty' => true ],
+ 'test.shouldembed' => [ 'shouldEmbed' => true ],
+ 'test.user' => [ 'group' => 'user' ],
+
+ 'test.styles.pure' => [ 'type' => ResourceLoaderModule::LOAD_STYLES ],
+ 'test.styles.mixed' => [],
+ 'test.styles.noscript' => [
+ 'type' => ResourceLoaderModule::LOAD_STYLES,
+ 'group' => 'noscript',
+ ],
+ 'test.styles.user' => [
+ 'type' => ResourceLoaderModule::LOAD_STYLES,
+ 'group' => 'user',
+ ],
+ 'test.styles.user.empty' => [
+ 'type' => ResourceLoaderModule::LOAD_STYLES,
+ 'group' => 'user',
+ 'isKnownEmpty' => true,
+ ],
+ 'test.styles.private' => [
+ 'type' => ResourceLoaderModule::LOAD_STYLES,
+ 'group' => 'private',
+ 'styles' => '.private{}',
+ ],
+ 'test.styles.shouldembed' => [
+ 'type' => ResourceLoaderModule::LOAD_STYLES,
+ 'shouldEmbed' => true,
+ 'styles' => '.shouldembed{}',
+ ],
+ 'test.styles.deprecated' => [
+ 'type' => ResourceLoaderModule::LOAD_STYLES,
+ 'deprecated' => 'Deprecation message.',
+ ],
+
+ 'test.scripts' => [],
+ 'test.scripts.user' => [ 'group' => 'user' ],
+ 'test.scripts.user.empty' => [ 'group' => 'user', 'isKnownEmpty' => true ],
+ 'test.scripts.raw' => [ 'isRaw' => true ],
+ 'test.scripts.shouldembed' => [ 'shouldEmbed' => true ],
+
+ 'test.ordering.a' => [ 'shouldEmbed' => false ],
+ 'test.ordering.b' => [ 'shouldEmbed' => false ],
+ 'test.ordering.c' => [ 'shouldEmbed' => true, 'styles' => '.orderingC{}' ],
+ 'test.ordering.d' => [ 'shouldEmbed' => true, 'styles' => '.orderingD{}' ],
+ 'test.ordering.e' => [ 'shouldEmbed' => false ],
+ ];
+ return array_map( function ( $options ) {
+ return self::makeModule( $options );
+ }, $modules );
+ }
}
* @covers ResourceLoader::makeLoaderRegisterScript
*/
public function testGetModuleRegistrations( $case ) {
- if ( isset( $case['sources'] ) ) {
- $this->setMwGlobals( 'wgResourceLoaderSources', $case['sources'] );
- }
-
$extraQuery = $case['extraQuery'] ?? [];
$context = $this->getResourceLoaderContext( $extraQuery );
$rl = $context->getResourceLoader();
+ if ( isset( $case['sources'] ) ) {
+ $rl->addSource( $case['sources'] );
+ }
$rl->register( $case['modules'] );
$module = new ResourceLoaderStartUpModule();
$out = ltrim( $case['out'], "\n" );
* @covers ResourceLoader::getLoadScript
*/
public function testGetLoadScript() {
- $this->setMwGlobals( 'wgResourceLoaderSources', [] );
$rl = new ResourceLoader();
$sources = self::fakeSources();
$rl->addSource( $sources );
it( 'should offer a way to cancel rollbacks', function () {
HistoryPage.rollback.click();
+ browser.pause( 300 );
HistoryPage.rollbackConfirmableNo.click();
browser.pause( 500 );