from other users originating from Special:EmailUser.
=== New developer features in 1.34 ===
+* The ImgAuthModifyHeaders hook was added to img_auth.php to allow modification
+ of headers in private wikis.
* Language::formatTimePeriod now supports the new 'avoidhours' option to output
strings like "5 days ago" instead of "5 days 13 hours ago".
specified, deprecated in 1.30, have been removed.
* BufferingStatsdDataFactory::getBuffer(), deprecated in 1.30, has been removed.
* The constant DB_SLAVE, deprecated in 1.28, has been removed. Use DB_REPLICA.
+* The constants NS_IMAGE and NS_IMAGE_TALK, deprecated in 1.14, have been
+ removed. Use NS_FILE and NS_FILE_TALK respectively.
* Replacer, DoubleReplacer, HashtableReplacer and RegexlikeReplacer
(deprecated in 1.32) have been removed. Closures should be used instead.
* OutputPage::addWikiText(), ::addWikiTextWithTitle(), ::addWikiTextTitleTidy(),
AuthChangeFormFields hook or security levels instead.
* WikiMap::getWikiIdFromDomain(), deprecated in 1.33, has been removed.
Use WikiMap::getWikiIdFromDbDomain() instead.
+* The config variables $wgHtml5, $wgJsMimeType, and $wgXhtmlDefaultNamespace,
+ which were deprecated and ignored by core since 1.22, are no longer set to any
+ value, and SkinTemplate no longer emits a 'jsmimetype' key. Any extensions not
+ updated since 2013 to cope with this deprecation may now break.
+* (T222637) Passing ResourceLoaderModule objects to ResourceLoader::register()
+ or $wgResourceModules is no longer supported.
+ Use the 'class' or 'factory' option of the array format instead.
+* The parameter $lang of the functions generateTOC and tocList in Linker and
+ DummyLinker must be in type Language when present. Other types are
+ deprecated since 1.33.
* …
=== Deprecations in 1.34 ===
been deprecated.
* User::getRights() and User::$mRights have been deprecated. Use
PermissionManager::getUserPermissions() instead.
+* The LocalisationCacheRecache hook no longer allows purging of message blobs
+ to be prevented. Modifying the $purgeBlobs parameter now has no effect.
=== Other changes in 1.34 ===
* …
"SkinOOUIThemes": {
"type": "object"
},
+ "OOUIThemePaths": {
+ "type": "object",
+ "description": "Map of custom OOUI theme names to paths to load them from. Same format as ResourceLoaderOOUIModule::$builtinThemePaths.",
+ "patternProperties": {
+ "^[A-Za-z]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "scripts": {
+ "type": "string",
+ "description": "Path to script file."
+ },
+ "styles": {
+ "type": "string",
+ "description": "Path to style files. '{module}' will be replaced with the module's name."
+ },
+ "images": {
+ "type": [ "string", "null" ],
+ "description": "Path to images (optional). '{module}' will be replaced with the module's name."
+ }
+ }
+ }
+ }
+ },
"PasswordPolicy": {
"type": "object",
"description": "Password policies"
"type": "object",
"description": "Map of skin names to OOUI themes to use. Same format as ResourceLoaderOOUIModule::$builtinSkinThemeMap."
},
+ "OOUIThemePaths": {
+ "type": "object",
+ "description": "Map of custom OOUI theme names to paths to load them from. Same format as ResourceLoaderOOUIModule::$builtinThemePaths.",
+ "patternProperties": {
+ "^[A-Za-z]+$": {
+ "type": "object",
+ "additionalProperties": false,
+ "properties": {
+ "scripts": {
+ "type": "string",
+ "description": "Path to script file."
+ },
+ "styles": {
+ "type": "string",
+ "description": "Path to style files. '{module}' will be replaced with the module's name."
+ },
+ "images": {
+ "type": [ "string", "null" ],
+ "description": "Path to images (optional). '{module}' will be replaced with the module's name."
+ }
+ }
+ }
+ }
+ },
"PasswordPolicy": {
"type": "object",
"description": "Password policies"
$page: ImagePage object
&$toc: Array of <li> strings
-'ImgAuthBeforeStream': executed before file is streamed to user, but only when
+'ImgAuthBeforeStream': Executed before file is streamed to user, but only when
using img_auth.php.
&$title: the Title object of the file as it would appear for the upload page
&$path: the original file and path name when img_auth was invoked by the web
$result[2 through n]=Parameters passed to body text message. Please note the
header message cannot receive/use parameters.
+'ImgAuthModifyHeaders': Executed just before a file is streamed to a user via
+img_auth.php, allowing headers to be modified beforehand.
+$title: LinkTarget object
+&$headers: HTTP headers ( name => value, names are case insensitive ).
+ Two headers get special handling: If-Modified-Since (value must be
+ a valid HTTP date) and Range (must be of the form "bytes=(\d*-\d*)")
+ will be honored when streaming the file.
+
'ImportHandleLogItemXMLTag': When parsing a XML tag in a log item.
Return false to stop further processing of the tag
$reader: XMLReader object
$cache: The LocalisationCache object
$code: language code
&$alldata: The localisation data from core and extensions
-&$purgeBlobs: whether to purge/update the message blobs via
- MessageBlobStore::clear()
'LocalisationCacheRecacheFallback': Called for each language when merging
fallback data into the cache.
$headers = []; // extra HTTP headers to send
+ $title = Title::makeTitleSafe( NS_FILE, $name );
+
if ( !$publicWiki ) {
// For private wikis, run extra auth checks and set cache control headers
- $headers[] = 'Cache-Control: private';
- $headers[] = 'Vary: Cookie';
+ $headers['Cache-Control'] = 'private';
+ $headers['Vary'] = 'Cookie';
- $title = Title::makeTitleSafe( NS_FILE, $name );
if ( !$title instanceof Title ) { // files have valid titles
wfForbidden( 'img-auth-accessdenied', 'img-auth-badtitle', $name );
return;
}
}
- $options = []; // HTTP header options
if ( isset( $_SERVER['HTTP_RANGE'] ) ) {
- $options['range'] = $_SERVER['HTTP_RANGE'];
+ $headers['Range'] = $_SERVER['HTTP_RANGE'];
}
if ( isset( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) ) {
- $options['if-modified-since'] = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
+ $headers['If-Modified-Since'] = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
}
if ( $request->getCheck( 'download' ) ) {
- $headers[] = 'Content-Disposition: attachment';
+ $headers['Content-Disposition'] = 'attachment';
}
+ // Allow modification of headers before streaming a file
+ Hooks::run( 'ImgAuthModifyHeaders', [ $title->getTitleValue(), &$headers ] );
+
// Stream the requested file
+ list( $headers, $options ) = HTTPFileStreamer::preprocessHeaders( $headers );
wfDebugLog( 'img_auth', "Streaming `" . $filename . "`." );
$repo->streamFileWithStatus( $filename, $headers, $options );
}
*/
$wgMimeType = 'text/html';
-/**
- * Previously used as content type in HTML script tags. This is now ignored since
- * HTML5 doesn't require a MIME type for script tags (javascript is the default).
- * It was also previously used by RawAction to determine the ctype query parameter
- * value that will result in a javascript response.
- * @deprecated since 1.22
- */
-$wgJsMimeType = null;
-
-/**
- * The default xmlns attribute. The option to define this has been removed.
- * The value of this variable is no longer used by core and is set to a fixed
- * value in Setup.php for compatibility with extensions that depend on the value
- * of this variable being set. Such a dependency however is deprecated.
- * @deprecated since 1.22
- */
-$wgXhtmlDefaultNamespace = null;
-
-/**
- * Previously used to determine if we should output an HTML5 doctype.
- * This is no longer used as we always output HTML5 now. For compatibility with
- * extensions that still check the value of this config it's value is now forced
- * to true by Setup.php.
- * @deprecated since 1.22
- */
-$wgHtml5 = true;
-
/**
* Defines the value of the version attribute in the <html> tag, if any.
*
define( 'NS_HELP_TALK', 13 );
define( 'NS_CATEGORY', 14 );
define( 'NS_CATEGORY_TALK', 15 );
-
-/**
- * NS_IMAGE and NS_IMAGE_TALK are the pre-v1.14 names for NS_FILE and
- * NS_FILE_TALK respectively, and are kept for compatibility.
- *
- * When writing code that should be compatible with older MediaWiki
- * versions, either stick to the old names or define the new constants
- * yourself, if they're not defined already.
- *
- * @deprecated since 1.14
- */
-define( 'NS_IMAGE', NS_FILE );
-/**
- * @deprecated since 1.14
- */
-define( 'NS_IMAGE_TALK', NS_FILE_TALK );
/**@}*/
/**@{
return Linker::tocLineEnd();
}
- public function tocList( $toc, $lang = null ) {
+ public function tocList( $toc, Language $lang = null ) {
return Linker::tocList( $toc, $lang );
}
- public function generateTOC( $tree, $lang = null ) {
+ public function generateTOC( $tree, Language $lang = null ) {
return Linker::generateTOC( $tree, $lang );
}
*
* @since 1.16.3
* @param string $toc Html of the Table Of Contents
- * @param string|Language|bool|null $lang Language for the toc title, defaults to user language.
- * The types string and bool are deprecated.
+ * @param Language|null $lang Language for the toc title, defaults to user language
* @return string Full html of the TOC
*/
- public static function tocList( $toc, $lang = null ) {
+ public static function tocList( $toc, Language $lang = null ) {
$lang = $lang ?? RequestContext::getMain()->getLanguage();
- if ( !$lang instanceof Language ) {
- wfDeprecated( __METHOD__ . ' with type other than Language for $lang', '1.33' );
- $lang = wfGetLangObj( $lang );
- }
$title = wfMessage( 'toc' )->inLanguage( $lang )->escaped();
*
* @since 1.16.3. $lang added in 1.17
* @param array $tree Return value of ParserOutput::getSections()
- * @param string|Language|bool|null $lang Language for the toc title, defaults to user language.
- * The types string and bool are deprecated.
+ * @param Language|null $lang Language for the toc title, defaults to user language
* @return string HTML fragment
*/
- public static function generateTOC( $tree, $lang = null ) {
+ public static function generateTOC( $tree, Language $lang = null ) {
$toc = '';
$lastLevel = 0;
foreach ( $tree as $section ) {
) {
list( , $subpage ) = $spFactory->resolveAlias( $title->getDBkey() );
$target = $specialPage->getRedirect( $subpage );
- // Target can also be true. We let that case fall through to normal processing.
+ // target can also be true. We let that case fall through to normal processing.
if ( $target instanceof Title ) {
- if ( $target->isExternal() ) {
- // Handle interwiki redirects
- $target = SpecialPage::getTitleFor(
- 'GoToInterwiki',
- $target->getPrefixedDBkey()
- );
- }
-
$query = $specialPage->getRedirectQuery( $subpage ) ?: [];
$request = new DerivativeRequest( $this->context->getRequest(), $query );
$request->setRequestURL( $this->context->getRequest()->getRequestURL() );
$wgDebugToolbar = false;
}
-// We always output HTML5 since 1.22, overriding these is no longer supported
-// we set them here for extensions that depend on its value.
-$wgHtml5 = true;
-$wgXhtmlDefaultNamespace = 'http://www.w3.org/1999/xhtml';
-$wgJsMimeType = 'text/javascript';
-
// Blacklisted file extensions shouldn't appear on the "allowed" list
$wgFileExtensions = array_values( array_diff( $wgFileExtensions, $wgFileBlacklist ) );
$allData['list'][$key] = array_keys( $allData[$key] );
}
# Run hooks
- $purgeBlobs = true;
- Hooks::run( 'LocalisationCacheRecache', [ $this, $code, &$allData, &$purgeBlobs ] );
+ $unused = true; // Used to be $purgeBlobs, removed in 1.34
+ Hooks::run( 'LocalisationCacheRecache', [ $this, $code, &$allData, &$unused ] );
if ( is_null( $allData['namespaceNames'] ) ) {
throw new MWException( __METHOD__ . ': Localisation data failed sanity check! ' .
# Clear out the MessageBlobStore
# HACK: If using a null (i.e. disabled) storage backend, we
# can't write to the MessageBlobStore either
- if ( $purgeBlobs && !$this->store instanceof LCStoreNull ) {
+ if ( !$this->store instanceof LCStoreNull ) {
$blobStore = MediaWikiServices::getInstance()->getResourceLoader()->getMessageBlobStore();
$blobStore->clear();
}
<?php
use MediaWiki\Widget\NamespacesMultiselectWidget;
+use MediaWiki\MediaWikiServices;
/**
* Implements a tag multiselect input field for namespaces.
}
foreach ( $namespaces as $namespace ) {
- if ( $namespace < 0 ) {
+ if (
+ $namespace < 0 ||
+ !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $namespace )
+ ) {
return $this->msg( 'htmlform-select-badoption' );
}
return $this->__call( __FUNCTION__, func_get_args() );
}
- public function setLBInfo( $name, $value = null ) {
+ public function setLBInfo( $nameOrArray, $value = null ) {
// Disallow things that might confuse the LoadBalancer tracking
throw new DBUnexpectedError( $this, "Changing LB info is disallowed to enable reuse." );
}
return null;
}
- public function setLBInfo( $name, $value = null ) {
- if ( is_null( $value ) ) {
- $this->lbInfo = $name;
+ public function setLBInfo( $nameOrArray, $value = null ) {
+ if ( is_array( $nameOrArray ) ) {
+ $this->lbInfo = $nameOrArray;
+ } elseif ( is_string( $nameOrArray ) ) {
+ if ( $value !== null ) {
+ $this->lbInfo[$nameOrArray] = $value;
+ } else {
+ unset( $this->lbInfo[$nameOrArray] );
+ }
} else {
- $this->lbInfo[$name] = $value;
+ throw new InvalidArgumentException( "Got non-string key" );
}
}
if ( self::$fulltextEnabled === null ) {
self::$fulltextEnabled = false;
$table = $this->tableName( 'searchindex' );
- $res = $this->query( "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'", __METHOD__ );
+ $res = $this->query(
+ "SELECT sql FROM sqlite_master WHERE tbl_name = '$table'",
+ __METHOD__,
+ self::QUERY_IGNORE_DBO_TRX
+ );
if ( $res ) {
$row = $res->fetchRow();
self::$fulltextEnabled = stristr( $row['sql'], 'fts' ) !== false;
$file = is_string( $file ) ? $file : self::generateFileName( $this->dbDir, $name );
$encFile = $this->addQuotes( $file );
- return $this->query( "ATTACH DATABASE $encFile AS $name", $fname );
+ return $this->query(
+ "ATTACH DATABASE $encFile AS $name",
+ $fname,
+ self::QUERY_IGNORE_DBO_TRX
+ );
}
protected function isWriteQuery( $sql ) {
$encTable = $this->addQuotes( $tableRaw );
$res = $this->query(
- "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable" );
+ "SELECT 1 FROM sqlite_master WHERE type='table' AND name=$encTable",
+ __METHOD__,
+ self::QUERY_IGNORE_DBO_TRX
+ );
return $res->numRows() ? true : false;
}
*/
function indexInfo( $table, $index, $fname = __METHOD__ ) {
$sql = 'PRAGMA index_info(' . $this->addQuotes( $this->indexName( $index ) ) . ')';
- $res = $this->query( $sql, $fname );
+ $res = $this->query( $sql, $fname, self::QUERY_IGNORE_DBO_TRX );
if ( !$res || $res->numRows() == 0 ) {
return false;
}
function fieldInfo( $table, $field ) {
$tableName = $this->tableName( $table );
$sql = 'PRAGMA table_info(' . $this->addQuotes( $tableName ) . ')';
- $res = $this->query( $sql, __METHOD__ );
+ $res = $this->query( $sql, __METHOD__, self::QUERY_IGNORE_DBO_TRX );
foreach ( $res as $row ) {
if ( $row->name == $field ) {
return new SQLiteField( $row, $tableName );
}
$sql = "DROP TABLE " . $this->tableName( $tableName );
- return $this->query( $sql, $fName );
+ return $this->query( $sql, $fName, self::QUERY_IGNORE_DBO_TRX );
}
public function setTableAliases( array $aliases ) {
public function resetSequenceForTable( $table, $fname = __METHOD__ ) {
$encTable = $this->addIdentifierQuotes( 'sqlite_sequence' );
$encName = $this->addQuotes( $this->tableName( $table, 'raw' ) );
- $this->query( "DELETE FROM $encTable WHERE name = $encName", $fname );
+ $this->query(
+ "DELETE FROM $encTable WHERE name = $encName",
+ $fname,
+ self::QUERY_IGNORE_DBO_TRX
+ );
}
public function databasesAreIndependent() {
public function getLBInfo( $name = null );
/**
- * Set the LB info array, or a member of it. If called with one parameter,
- * the LB info array is set to that parameter. If it is called with two
- * parameters, the member with the given name is set to the given value.
+ * Set the entire array or a particular key of the managing load balancer info array
*
- * @param array|string $name
- * @param array|null $value
+ * @param array|string $nameOrArray The new array or the name of a key to set
+ * @param array|null $value If $nameOrArray is a string, the new key value (null to unset)
*/
- public function setLBInfo( $name, $value = null );
+ public function setLBInfo( $nameOrArray, $value = null );
/**
* Set a lazy-connecting DB handle to the master DB (for replication status purposes)
/**
* Change the current database
*
- * This should not be called outside LoadBalancer for connections managed by a LoadBalancer
+ * This should only be called by a load balancer or if the handle is not attached to one
*
* @param string $db
* @return bool True unless an exception was thrown
/**
* Set the current domain (database, schema, and table prefix)
*
- * This will throw an error for some database types if the database unspecified
+ * This will throw an error for some database types if the database is unspecified
*
- * This should not be called outside LoadBalancer for connections managed by a LoadBalancer
+ * This should only be called by a load balancer or if the handle is not attached to one
*
* @param string|DatabaseDomain $domain
* @since 1.32
}
if ( $conn->getFlag( $conn::DBO_TRX ) ) {
- $conn->setLBInfo( 'trxRoundId', false );
+ $conn->setLBInfo( 'trxRoundId', null ); // remove the round ID
}
if ( $conn->getFlag( $conn::DBO_DEFAULT ) ) {
'ResourceFileModulePaths',
'ResourceModules',
'ResourceModuleSkinStyles',
+ 'OOUIThemePaths',
'QUnitTestModule',
'ExtensionMessagesFiles',
'MessagesDirs',
}
}
- foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles' ] as $setting ) {
+ foreach ( [ 'ResourceModules', 'ResourceModuleSkinStyles', 'OOUIThemePaths' ] as $setting ) {
if ( isset( $info[$setting] ) ) {
foreach ( $info[$setting] as $name => $data ) {
if ( isset( $data['localBasePath'] ) ) {
if ( $defaultPaths ) {
$data += $defaultPaths;
}
- $this->globals["wg$setting"][$name] = $data;
+ if ( $setting === 'OOUIThemePaths' ) {
+ $this->attributes[$setting][$name] = $data;
+ } else {
+ $this->globals["wg$setting"][$name] = $data;
+ }
}
}
}
/**
* Register a module with the ResourceLoader system.
*
- * @param mixed $name Name of module as a string or List of name/object pairs as an array
- * @param array|null $info Module info array. For backwards compatibility with 1.17alpha,
- * this may also be a ResourceLoaderModule object. Optional when using
- * multiple-registration calling style.
+ * @param string|array[] $name Module name as a string or, array of module info arrays
+ * keyed by name.
+ * @param array|null $info Module info array. When using the first parameter to register
+ * multiple modules at once, this parameter is optional.
* @throws MWException If a duplicate module registration is attempted
* @throws MWException If a module name contains illegal characters (pipes or commas)
- * @throws MWException If something other than a ResourceLoaderModule is being registered
+ * @throws InvalidArgumentException If the module info is not an array
*/
public function register( $name, $info = null ) {
$moduleSkinStyles = $this->config->get( 'ResourceModuleSkinStyles' );
);
}
- // Check $name for validity
+ // Check validity
if ( !self::isValidModuleName( $name ) ) {
throw new MWException( "ResourceLoader module name '$name' is invalid, "
. "see ResourceLoader::isValidModuleName()" );
}
-
- // Attach module
- if ( $info instanceof ResourceLoaderModule ) {
- $this->moduleInfos[$name] = [ 'object' => $info ];
- $info->setName( $name );
- $this->modules[$name] = $info;
- } elseif ( is_array( $info ) ) {
- // New calling convention
- $this->moduleInfos[$name] = $info;
- } else {
- throw new MWException(
- 'ResourceLoader module info type error for module \'' . $name .
- '\': expected ResourceLoaderModule or array (got: ' . gettype( $info ) . ')'
+ if ( !is_array( $info ) ) {
+ throw new InvalidArgumentException(
+ 'Invalid module info for "' . $name . '": expected array, got ' . gettype( $info )
);
}
- // Last-minute changes
+ // Attach module
+ $this->moduleInfos[$name] = $info;
+ // Last-minute changes
// Apply custom skin-defined styles to existing modules.
if ( $this->isFileModule( $name ) ) {
foreach ( $moduleSkinStyles as $skinName => $skinStyles ) {
// No such module
return null;
}
- // Construct the requested object
+ // Construct the requested module object
$info = $this->moduleInfos[$name];
- /** @var ResourceLoaderModule $object */
- if ( isset( $info['object'] ) ) {
- // Object given in info array
- $object = $info['object'];
- } elseif ( isset( $info['factory'] ) ) {
+ if ( isset( $info['factory'] ) ) {
+ /** @var ResourceLoaderModule $object */
$object = call_user_func( $info['factory'], $info );
- $object->setConfig( $this->getConfig() );
- $object->setLogger( $this->logger );
} else {
$class = $info['class'] ?? ResourceLoaderFileModule::class;
/** @var ResourceLoaderModule $object */
$object = new $class( $info );
- $object->setConfig( $this->getConfig() );
- $object->setLogger( $this->logger );
}
+ $object->setConfig( $this->getConfig() );
+ $object->setLogger( $this->logger );
$object->setName( $name );
$this->modules[$name] = $object;
}
return false;
}
$info = $this->moduleInfos[$name];
- if ( isset( $info['object'] ) ) {
- return false;
- }
return !isset( $info['factory'] ) && (
// The implied default for 'class' is ResourceLoaderFileModule
!isset( $info['class'] ) ||
* @return string JavaScript code
*/
public static function makeMessageSetScript( $messages ) {
- return Xml::encodeJsCall(
- 'mw.messages.set',
- [ (object)$messages ],
- self::inDebugMode()
- );
+ return 'mw.messages.set('
+ . self::encodeJsonForScript( (object)$messages )
+ . ');';
}
/**
if ( !is_array( $states ) ) {
$states = [ $states => $state ];
}
- return Xml::encodeJsCall(
- 'mw.loader.state',
- [ $states ],
- self::inDebugMode()
- );
+ return 'mw.loader.state('
+ . self::encodeJsonForScript( $states )
+ . ');';
}
private static function isEmptyObject( stdClass $obj ) {
array_walk( $modules, [ self::class, 'trimArray' ] );
- return Xml::encodeJsCall(
- 'mw.loader.register',
- [ $modules ],
- self::inDebugMode()
- );
+ return 'mw.loader.register('
+ . self::encodeJsonForScript( $modules )
+ . ');';
}
/**
if ( !is_array( $sources ) ) {
$sources = [ $sources => $loadUrl ];
}
- return Xml::encodeJsCall(
- 'mw.loader.addSource',
- [ $sources ],
- self::inDebugMode()
- );
+ return 'mw.loader.addSource('
+ . self::encodeJsonForScript( $sources )
+ . ');';
}
/**
case 'debugScripts':
case 'styles':
case 'packageFiles':
- $this->{$member} = (array)$option;
+ $this->{$member} = is_array( $option ) ? $option : [ $option ];
break;
case 'templates':
$hasTemplates = true;
- $this->{$member} = (array)$option;
+ $this->{$member} = is_array( $option ) ? $option : [ $option ];
break;
// Collated lists of file paths
case 'languageScripts':
"'$key' given, string expected."
);
}
- $this->{$member}[$key] = (array)$value;
+ $this->{$member}[$key] = is_array( $value ) ? $value : [ $value ];
}
break;
case 'deprecated':
// Ensure relevant template compiler module gets loaded
foreach ( $this->templates as $alias => $templatePath ) {
if ( is_int( $alias ) ) {
- $alias = $templatePath;
+ $alias = $this->getPath( $templatePath );
}
$suffix = explode( '.', $alias );
$suffix = end( $suffix );
return $summary;
}
+ /**
+ * @param string|ResourceLoaderFilePath $path
+ * @return string
+ */
+ protected function getPath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getPath();
+ }
+
+ return $path;
+ }
+
/**
* @param string|ResourceLoaderFilePath $path
* @return string
foreach ( $this->templates as $alias => $templatePath ) {
// Alias is optional
if ( is_int( $alias ) ) {
- $alias = $templatePath;
+ $alias = $this->getPath( $templatePath );
}
$localPath = $this->getLocalPath( $templatePath );
if ( file_exists( $localPath ) ) {
return "{$this->remoteBasePath}/{$this->path}";
}
+ /**
+ * @return string
+ */
+ public function getLocalBasePath() {
+ return $this->localBasePath;
+ }
+
+ /**
+ * @return string
+ */
+ public function getRemoteBasePath() {
+ return $this->remoteBasePath;
+ }
+
/**
* @return string
*/
// Ensure that all files have common extension.
$extensions = [];
- $descriptor = (array)$this->descriptor;
+ $descriptor = is_array( $this->descriptor ) ? $this->descriptor : [ $this->descriptor ];
array_walk_recursive( $descriptor, function ( $path ) use ( &$extensions ) {
- $extensions[] = pathinfo( $path, PATHINFO_EXTENSION );
+ $extensions[] = pathinfo( $this->getLocalPath( $path ), PATHINFO_EXTENSION );
} );
$extensions = array_unique( $extensions );
if ( count( $extensions ) !== 1 ) {
*/
public function getPath( ResourceLoaderContext $context ) {
$desc = $this->descriptor;
- if ( is_string( $desc ) ) {
- return $this->basePath . '/' . $desc;
+ if ( !is_array( $desc ) ) {
+ return $this->getLocalPath( $desc );
}
if ( isset( $desc['lang'] ) ) {
$contextLang = $context->getLanguage();
if ( isset( $desc['lang'][$contextLang] ) ) {
- return $this->basePath . '/' . $desc['lang'][$contextLang];
+ return $this->getLocalPath( $desc['lang'][$contextLang] );
}
$fallbacks = Language::getFallbacksFor( $contextLang, Language::STRICT_FALLBACKS );
foreach ( $fallbacks as $lang ) {
if ( isset( $desc['lang'][$lang] ) ) {
- return $this->basePath . '/' . $desc['lang'][$lang];
+ return $this->getLocalPath( $desc['lang'][$lang] );
}
}
}
if ( isset( $desc[$context->getDirection()] ) ) {
- return $this->basePath . '/' . $desc[$context->getDirection()];
+ return $this->getLocalPath( $desc[$context->getDirection()] );
}
if ( isset( $desc['default'] ) ) {
- return $this->basePath . '/' . $desc['default'];
+ return $this->getLocalPath( $desc['default'] );
} else {
throw new MWException( 'No matching path found' );
}
}
+ /**
+ * @param string|ResourceLoaderFilePath $path
+ * @return string
+ */
+ protected function getLocalPath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getLocalPath();
+ }
+
+ return "{$this->basePath}/$path";
+ }
+
/**
* Get the extension of the image.
*
$this->definition = null;
if ( isset( $options['data'] ) ) {
- $dataPath = $this->localBasePath . '/' . $options['data'];
+ $dataPath = $this->getLocalPath( $options['data'] );
$data = json_decode( file_get_contents( $dataPath ), true );
$options = array_merge( $data, $options );
}
$this->images[$skin] = $this->images['default'] ?? [];
}
foreach ( $this->images[$skin] as $name => $options ) {
- $fileDescriptor = is_string( $options ) ? $options : $options['file'];
+ $fileDescriptor = is_array( $options ) ? $options['file'] : $options;
$allowedVariants = array_merge(
( is_array( $options ) && isset( $options['variants'] ) ) ? $options['variants'] : [],
return array_map( [ __CLASS__, 'safeFileHash' ], $files );
}
+ /**
+ * @param string|ResourceLoaderFilePath $path
+ * @return string
+ */
+ protected function getLocalPath( $path ) {
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ return $path->getLocalPath();
+ }
+
+ return "{$this->localBasePath}/$path";
+ }
+
/**
* Extract a local base path from module definition information.
*
// Find the path to the JSON file which contains the actual image definitions for this theme
if ( $module ) {
$dataPath = $this->getThemeImagesPath( $theme, $module );
+ if ( !$dataPath ) {
+ return false;
+ }
} else {
// Backwards-compatibility for things that probably shouldn't have used this class...
$dataPath =
* @return array|false
*/
protected function readJSONFile( $dataPath ) {
- $localDataPath = $this->localBasePath . '/' . $dataPath;
+ $localDataPath = $this->getLocalPath( $dataPath );
if ( !file_exists( $localDataPath ) ) {
return false;
// Expand the paths to images (since they are relative to the JSON file that defines them, not
// our base directory)
$fixPath = function ( &$path ) use ( $dataPath ) {
- $path = dirname( $dataPath ) . '/' . $path;
+ if ( $dataPath instanceof ResourceLoaderFilePath ) {
+ $path = new ResourceLoaderFilePath(
+ dirname( $dataPath->getPath() ) . '/' . $path,
+ $dataPath->getLocalBasePath(),
+ $dataPath->getRemoteBasePath()
+ );
+ } else {
+ $path = dirname( $dataPath ) . '/' . $path;
+ }
};
array_walk( $data['images'], function ( &$value ) use ( $fixPath ) {
if ( is_string( $value['file'] ) ) {
* Return a map of theme names to lists of paths from which a given theme should be loaded.
*
* Keys are theme names, values are associative arrays. Keys of the inner array are 'scripts',
- * 'styles', or 'images', and values are string paths.
+ * 'styles', or 'images', and values are paths. Paths may be strings or ResourceLoaderFilePaths.
*
* Additionally, the string '{module}' in paths represents the name of the module to load.
*
*/
protected static function getThemePaths() {
$themePaths = self::$builtinThemePaths;
+ $themePaths += ExtensionRegistry::getInstance()->getAttribute( 'OOUIThemePaths' );
+
+ list( $defaultLocalBasePath, $defaultRemoteBasePath ) =
+ ResourceLoaderFileModule::extractBasePaths();
+
+ // Allow custom themes' paths to be relative to the skin/extension that defines them,
+ // like with ResourceModuleSkinStyles
+ foreach ( $themePaths as $theme => &$paths ) {
+ list( $localBasePath, $remoteBasePath ) =
+ ResourceLoaderFileModule::extractBasePaths( $paths );
+ if ( $localBasePath !== $defaultLocalBasePath || $remoteBasePath !== $defaultRemoteBasePath ) {
+ foreach ( $paths as &$path ) {
+ $path = new ResourceLoaderFilePath( $path, $localBasePath, $remoteBasePath );
+ }
+ }
+ }
+
return $themePaths;
}
/**
* Return a path to load given module of given theme from.
*
+ * The file at this path may not exist. This should be handled by the caller (throwing an error or
+ * falling back to default theme).
+ *
* @param string $theme OOUI theme name, for example 'WikimediaUI' or 'Apex'
* @param string $kind Kind of the module: 'scripts', 'styles', or 'images'
* @param string $module Module name, for valid values see $knownScriptsModules,
* $knownStylesModules, $knownImagesModules
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemePath( $theme, $kind, $module ) {
$paths = self::getThemePaths();
$path = $paths[$theme][$kind];
- $path = str_replace( '{module}', $module, $path );
+ if ( $path instanceof ResourceLoaderFilePath ) {
+ $path = new ResourceLoaderFilePath(
+ str_replace( '{module}', $module, $path->getPath() ),
+ $path->getLocalBasePath(),
+ $path->getRemoteBasePath()
+ );
+ } else {
+ $path = str_replace( '{module}', $module, $path );
+ }
return $path;
}
/**
* @param string $theme See getThemePath()
* @param string $module See getThemePath()
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemeScriptsPath( $theme, $module ) {
if ( !in_array( $module, self::$knownScriptsModules ) ) {
/**
* @param string $theme See getThemePath()
* @param string $module See getThemePath()
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemeStylesPath( $theme, $module ) {
if ( !in_array( $module, self::$knownStylesModules ) ) {
/**
* @param string $theme See getThemePath()
* @param string $module See getThemePath()
- * @return string
+ * @return string|ResourceLoaderFilePath
*/
protected function getThemeImagesPath( $theme, $module ) {
if ( !in_array( $module, self::$knownImagesModules ) ) {
$toolbox['feeds']['links'][$key]['class'] = 'feedlink';
}
}
- foreach ( [ 'contributions', 'log', 'blockip', 'emailuser',
+ foreach ( [ 'contributions', 'log', 'blockip', 'emailuser', 'mute',
'userrights', 'upload', 'specialpages' ] as $special
) {
if ( isset( $this->data['nav_urls'][$special] ) && $this->data['nav_urls'][$special] ) {
* @return QuickTemplate The template to be executed by outputPage
*/
protected function prepareQuickTemplate() {
- global $wgScript, $wgStylePath, $wgMimeType, $wgJsMimeType,
+ global $wgScript, $wgStylePath, $wgMimeType,
$wgSitename, $wgLogo, $wgMaxCredits,
$wgShowCreditsIfMax, $wgArticlePath,
$wgScriptPath, $wgServer;
}
$tpl->set( 'mimetype', $wgMimeType );
- $tpl->set( 'jsmimetype', $wgJsMimeType );
$tpl->set( 'charset', 'UTF-8' );
$tpl->set( 'wgScript', $wgScript );
$tpl->set( 'skinname', $this->skinname );
$nav_urls['contributions'] = false;
$nav_urls['log'] = false;
$nav_urls['blockip'] = false;
+ $nav_urls['mute'] = false;
$nav_urls['emailuser'] = false;
$nav_urls['userrights'] = false;
}
if ( !$user->isAnon() ) {
+ if ( $this->getUser()->isRegistered() && $this->getConfig()->get( 'EnableSpecialMute' ) ) {
+ $nav_urls['mute'] = [
+ 'text' => $this->msg( 'mute-preferences' )->text(),
+ 'href' => self::makeSpecialUrlSubpage( 'Mute', $rootUser )
+ ];
+ }
+
$sur = new UserrightsPage;
$sur->setContext( $this->getContext() );
$canChange = $sur->userCanChangeRights( $user );
"specialmute-error-email-preferences": "You must confirm your email address before you can mute a user. You may do so from [[Special:Preferences]].",
"specialmute-email-footer": "To manage email preferences for {{BIDI:$2}} please visit <$1>.",
"specialmute-login-required": "Please log in to change your mute preferences.",
+ "mute-preferences": "Mute preferences",
"revid": "revision $1",
"pageid": "page ID $1",
"interfaceadmin-info": "$1\n\nPermissions for editing of sitewide CSS/JS/JSON files were recently separated from the <code>editinterface</code> right. If you do not understand why you are getting this error, see [[mw:MediaWiki_1.32/interface-admin]].",
"specialmute-error-email-preferences": "Error displayed when the user has not confirmed their email address.",
"specialmute-email-footer": "Email footer in plain text linking to [[Special:Mute]] preselecting the sender to manage muting options.\n* $1 - Url linking to [[Special:Mute]].\n* $2 - The user sending the email.",
"specialmute-login-required": "Error displayed when a user tries to access [[Special:Mute]] before logging in.",
+ "mute-preferences": "Link in the sidebar to manage muting preferences for a user. It links to [[Special:Mute]] with the user in context as the subpage.",
"revid": "Used to format a revision ID number in text. Parameters:\n* $1 - Revision ID number.\n{{Identical|Revision}}",
"pageid": "Used to format a page ID number in text. Parameters:\n* $1 - Page ID number.",
"interfaceadmin-info": "Part of the error message shown when someone with the <code>editinterface</code> right but without the appropriate <code>editsite*</code> right tries to edit a sitewide CSS/JSON/JS page.",
*/
WikitextMessagePoster.prototype.post = function ( subject, body, options ) {
var additionalParams;
+ options = options || {};
mw.messagePoster.WikitextMessagePoster.parent.prototype.post.call( this, subject, body, options );
// Add signature if needed
$setup['wgNoFollowDomainExceptions'] = [ 'no-nofollow.org' ];
$setup['wgExternalLinkTarget'] = false;
$setup['wgLocaltimezone'] = 'UTC';
- $setup['wgHtml5'] = true;
$setup['wgDisableLangConversion'] = false;
$setup['wgDisableTitleConversion'] = false;
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>eye</title><path d="M10 7.5a2.5 2.5 0 1 0 2.5 2.5A2.5 2.5 0 0 0 10 7.5zm0 7a4.5 4.5 0 1 1 4.5-4.5 4.5 4.5 0 0 1-4.5 4.5zM10 3C3 3 0 10 0 10s3 7 10 7 10-7 10-7-3-7-10-7z"/></svg>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>flag</title><path d="M17 6L3 1v18h2v-6.87L17 6z"/></svg>
\ No newline at end of file
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20"><title>flag</title><path d="M3 6l14-5v18h-2v-6.87L3 6z"/></svg>
\ No newline at end of file
--- /dev/null
+mw.test();
--- /dev/null
+body {
+ color: red;
+}
--- /dev/null
+body {
+ color: black;
+}
--- /dev/null
+<div></div>
$this->assertSame( $oldDomain, $this->db->getDomainId() );
}
+ /**
+ * @covers Wikimedia\Rdbms\Database::getLBInfo
+ * @covers Wikimedia\Rdbms\Database::setLBInfo
+ */
+ public function testGetSetLBInfo() {
+ $db = $this->getMockDB();
+
+ $this->assertEquals( [], $db->getLBInfo() );
+ $this->assertNull( $db->getLBInfo( 'pringles' ) );
+
+ $db->setLBInfo( 'soda', 'water' );
+ $this->assertEquals( [ 'soda' => 'water' ], $db->getLBInfo() );
+ $this->assertNull( $db->getLBInfo( 'pringles' ) );
+ $this->assertEquals( 'water', $db->getLBInfo( 'soda' ) );
+
+ $db->setLBInfo( 'basketball', 'Lebron' );
+ $this->assertEquals( [ 'soda' => 'water', 'basketball' => 'Lebron' ], $db->getLBInfo() );
+ $this->assertEquals( 'water', $db->getLBInfo( 'soda' ) );
+ $this->assertEquals( 'Lebron', $db->getLBInfo( 'basketball' ) );
+
+ $db->setLBInfo( 'soda', null );
+ $this->assertEquals( [ 'basketball' => 'Lebron' ], $db->getLBInfo() );
+
+ $db->setLBInfo( [ 'King' => 'James' ] );
+ $this->assertNull( $db->getLBInfo( 'basketball' ) );
+ $this->assertEquals( [ 'King' => 'James' ], $db->getLBInfo() );
+ }
}
);
}
+ /**
+ * Test reading files from elsewhere than localBasePath using ResourceLoaderFilePath.
+ *
+ * This mimics modules modified by skins using 'ResourceModuleSkinStyles' and 'OOUIThemePaths'
+ * skin attributes.
+ *
+ * @covers ResourceLoaderFilePath::getLocalBasePath
+ * @covers ResourceLoaderFilePath::getRemoteBasePath
+ */
+ public function testResourceLoaderFilePath() {
+ $basePath = __DIR__ . '/../../data/blahblah';
+ $filePath = __DIR__ . '/../../data/rlfilepath';
+ $testModule = new ResourceLoaderFileModule( [
+ 'localBasePath' => $basePath,
+ 'remoteBasePath' => 'blahblah',
+ 'styles' => new ResourceLoaderFilePath( 'style.css', $filePath, 'rlfilepath' ),
+ 'skinStyles' => [
+ 'vector' => new ResourceLoaderFilePath( 'skinStyle.css', $filePath, 'rlfilepath' ),
+ ],
+ 'scripts' => new ResourceLoaderFilePath( 'script.js', $filePath, 'rlfilepath' ),
+ 'templates' => new ResourceLoaderFilePath( 'template.html', $filePath, 'rlfilepath' ),
+ ] );
+ $expectedModule = new ResourceLoaderFileModule( [
+ 'localBasePath' => $filePath,
+ 'remoteBasePath' => 'rlfilepath',
+ 'styles' => 'style.css',
+ 'skinStyles' => [
+ 'vector' => 'skinStyle.css',
+ ],
+ 'scripts' => 'script.js',
+ 'templates' => 'template.html',
+ ] );
+
+ $context = $this->getResourceLoaderContext();
+ $this->assertEquals(
+ $expectedModule->getModuleContent( $context ),
+ $testModule->getModuleContent( $context ),
+ "Using ResourceLoaderFilePath works correctly"
+ );
+ }
+
public static function providerGetTemplates() {
$modules = self::getModules();
--- /dev/null
+<?php
+
+class ResourceLoaderFilePathTest extends PHPUnit\Framework\TestCase {
+ /**
+ * @covers ResourceLoaderFilePath::__construct
+ */
+ public function testConstructor() {
+ $resourceLoaderFilePath = new ResourceLoaderFilePath(
+ 'dummy/path', 'localBasePath', 'remoteBasePath'
+ );
+
+ $this->assertInstanceOf( ResourceLoaderFilePath::class, $resourceLoaderFilePath );
+ }
+
+ /**
+ * @covers ResourceLoaderFilePath::getLocalPath
+ */
+ public function testGetLocalPath() {
+ $resourceLoaderFilePath = new ResourceLoaderFilePath(
+ 'dummy/path', 'localBasePath', 'remoteBasePath'
+ );
+
+ $this->assertSame(
+ 'localBasePath/dummy/path', $resourceLoaderFilePath->getLocalPath()
+ );
+ }
+
+ /**
+ * @covers ResourceLoaderFilePath::getRemotePath
+ */
+ public function testGetRemotePath() {
+ $resourceLoaderFilePath = new ResourceLoaderFilePath(
+ 'dummy/path', 'localBasePath', 'remoteBasePath'
+ );
+
+ $this->assertSame(
+ 'remoteBasePath/dummy/path', $resourceLoaderFilePath->getRemotePath()
+ );
+ }
+
+ /**
+ * @covers ResourceLoaderFilePath::getPath
+ */
+ public function testGetPath() {
+ $resourceLoaderFilePath = new ResourceLoaderFilePath(
+ 'dummy/path', 'localBasePath', 'remoteBasePath'
+ );
+
+ $this->assertSame(
+ 'dummy/path', $resourceLoaderFilePath->getPath()
+ );
+ }
+}
];
}
+ /**
+ * Test reading files from elsewhere than localBasePath using ResourceLoaderFilePath.
+ *
+ * This mimics modules modified by skins using 'ResourceModuleSkinStyles' and 'OOUIThemePaths'
+ * skin attributes.
+ *
+ * @covers ResourceLoaderFilePath::getLocalBasePath
+ * @covers ResourceLoaderFilePath::getRemoteBasePath
+ */
+ public function testResourceLoaderFilePath() {
+ $basePath = __DIR__ . '/../../data/blahblah';
+ $filePath = __DIR__ . '/../../data/rlfilepath';
+ $testModule = new ResourceLoaderImageModule( [
+ 'localBasePath' => $basePath,
+ 'remoteBasePath' => 'blahblah',
+ 'prefix' => 'foo',
+ 'images' => [
+ 'eye' => new ResourceLoaderFilePath( 'eye.svg', $filePath, 'rlfilepath' ),
+ 'flag' => [
+ 'file' => [
+ 'ltr' => new ResourceLoaderFilePath( 'flag-ltr.svg', $filePath, 'rlfilepath' ),
+ 'rtl' => new ResourceLoaderFilePath( 'flag-rtl.svg', $filePath, 'rlfilepath' ),
+ ],
+ ],
+ ],
+ ] );
+ $expectedModule = new ResourceLoaderImageModule( [
+ 'localBasePath' => $filePath,
+ 'remoteBasePath' => 'rlfilepath',
+ 'prefix' => 'foo',
+ 'images' => [
+ 'eye' => 'eye.svg',
+ 'flag' => [
+ 'file' => [
+ 'ltr' => 'flag-ltr.svg',
+ 'rtl' => 'flag-rtl.svg',
+ ],
+ ],
+ ],
+ ] );
+
+ $context = $this->getResourceLoaderContext();
+ $this->assertEquals(
+ $expectedModule->getModuleContent( $context ),
+ $testModule->getModuleContent( $context ),
+ "Using ResourceLoaderFilePath works correctly"
+ );
+ }
+
/**
* @dataProvider providerGetModules
* @covers ResourceLoaderImageModule::getStyles
'msg' => 'Empty registry',
'modules' => [],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [] );'
+});
+mw.loader.register([]);'
] ],
[ [
'msg' => 'Basic registry',
'test.blank' => [ 'class' => ResourceLoaderTestModule::class ],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.blank",
"{blankVer}"
]
-] );',
+]);',
] ],
[ [
'msg' => 'Optimise the dependency tree (basic case)',
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"a",
"{blankVer}",
"d",
"{blankVer}"
]
-] );',
+]);',
] ],
[ [
'msg' => 'Optimise the dependency tree (tolerate unknown deps)',
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"a",
"{blankVer}",
"c",
"{blankVer}"
]
-] );',
+]);',
] ],
[ [
// Regression test for T223402.
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"top",
"{blankVer}",
"util",
"{blankVer}"
]
-] );',
+]);',
] ],
[ [
// Regression test for T223402.
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"top",
"{blankVer}",
"util",
"{blankVer}"
]
-] );',
+]);',
] ],
[ [
'msg' => 'Version falls back gracefully if getVersionHash throws',
]
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.fail",
""
]
-] );
-mw.loader.state( {
+]);
+mw.loader.state({
"test.fail": "error"
-} );',
+});',
] ],
[ [
'msg' => 'Use version from getVersionHash',
]
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.version",
"1234567"
]
-] );',
+]);',
] ],
[ [
'msg' => 'Re-hash version from getVersionHash if too long',
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.version",
"016es8l"
]
-] );',
+]);',
] ],
[ [
'msg' => 'Group signature',
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.blank",
"{blankVer}"
[],
"x-bar"
]
-] );'
+]);'
] ],
[ [
'msg' => 'Different target (non-test should not be registered)',
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.blank",
"{blankVer}"
]
-] );'
+]);'
] ],
[ [
'msg' => 'Safemode disabled (default; register all modules)',
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.blank",
"{blankVer}"
"test.user",
"{blankVer}"
]
-] );'
+]);'
] ],
[ [
'msg' => 'Safemode enabled (filter modules with user/site origin)',
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.blank",
"{blankVer}"
"test.core-generated",
"{blankVer}"
]
-] );'
+]);'
] ],
[ [
'msg' => 'Foreign source',
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php",
"example": "http://example.org/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.blank",
"{blankVer}",
null,
"example"
]
-] );'
+]);'
] ],
[ [
'msg' => 'Conditional dependency function',
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.x.core",
"{blankVer}"
2
]
]
-] );',
+]);',
] ],
[ [
// This may seem like an edge case, but a plain MediaWiki core install
],
],
'out' => '
-mw.loader.addSource( {
+mw.loader.addSource({
"local": "/w/load.php",
"example": "http://example.org/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.blank",
"{blankVer}"
"x-bar",
"example"
]
-] );'
+]);'
] ],
];
}
$rl->register( $modules );
$module = new ResourceLoaderStartUpModule();
$out =
-'mw.loader.addSource( {
+'mw.loader.addSource({
"local": "/w/load.php"
-} );
-mw.loader.register( [
+});
+mw.loader.register([
[
"test.blank",
"{blankVer}"
null,
"return !!( window.JSON \u0026\u0026 JSON.parse \u0026\u0026 JSON.stringify);"
]
-] );';
+]);';
$this->assertEquals(
self::expandPlaceholders( $out ),
*/
public function testRegisterInvalidType() {
$resourceLoader = new EmptyResourceLoader();
- $this->setExpectedException( MWException::class, 'ResourceLoader module info type error' );
+ $this->setExpectedException( InvalidArgumentException::class, 'Invalid module info' );
$resourceLoader->register( 'test', new stdClass() );
}
*/
public function testMakeLoaderRegisterScript() {
$this->assertEquals(
- 'mw.loader.register( [
+ 'mw.loader.register([
[
"test.name",
"1234567"
]
-] );',
+]);',
ResourceLoader::makeLoaderRegisterScript( [
[ 'test.name', '1234567' ],
] ),
);
$this->assertEquals(
- 'mw.loader.register( [
+ 'mw.loader.register([
[
"test.foo",
"100"
null,
"return true;"
]
-] );',
+]);',
ResourceLoader::makeLoaderRegisterScript( [
[ 'test.foo', '100' , [], null, null ],
[ 'test.bar', '200', [ 'test.unknown' ], null ],
*/
public function testMakeLoaderSourcesScript() {
$this->assertEquals(
- 'mw.loader.addSource( {
+ 'mw.loader.addSource({
"local": "/w/load.php"
-} );',
+});',
ResourceLoader::makeLoaderSourcesScript( 'local', '/w/load.php' )
);
$this->assertEquals(
- 'mw.loader.addSource( {
+ 'mw.loader.addSource({
"local": "/w/load.php"
-} );',
+});',
ResourceLoader::makeLoaderSourcesScript( [ 'local' => '/w/load.php' ] )
);
$this->assertEquals(
- 'mw.loader.addSource( {
+ 'mw.loader.addSource({
"local": "/w/load.php",
"example": "https://example.org/w/load.php"
-} );',
+});',
ResourceLoader::makeLoaderSourcesScript( [
'local' => '/w/load.php',
'example' => 'https://example.org/w/load.php'
] )
);
$this->assertEquals(
- 'mw.loader.addSource( [] );',
+ 'mw.loader.addSource([]);',
ResourceLoader::makeLoaderSourcesScript( [] )
);
}
'modules' => [
'foo' => 'foo()',
],
- 'expected' => "foo()\n" . 'mw.loader.state( {
+ 'expected' => "foo()\n" . 'mw.loader.state({
"foo": "ready"
-} );',
+});',
'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
'message' => 'Script without semi-colon',
],
'foo' => 'foo()',
'bar' => 'bar()',
],
- 'expected' => "foo()\nbar()\n" . 'mw.loader.state( {
+ 'expected' => "foo()\nbar()\n" . 'mw.loader.state({
"foo": "ready",
"bar": "ready"
-} );',
+});',
'minified' => "foo()\nbar()\n" . 'mw.loader.state({"foo":"ready","bar":"ready"});',
'message' => 'Two scripts without semi-colon',
],
'modules' => [
'foo' => "foo()\n// bar();"
],
- 'expected' => "foo()\n// bar();\n" . 'mw.loader.state( {
+ 'expected' => "foo()\n// bar();\n" . 'mw.loader.state({
"foo": "ready"
-} );',
+});',
'minified' => "foo()\n" . 'mw.loader.state({"foo":"ready"});',
'message' => 'Script with semi-colon in comment (T162719)',
],
$this->assertCount( 1, $errors );
$this->assertRegExp( '/Ferry not found/', $errors[0] );
$this->assertEquals(
- "foo();\nbar();\n" . 'mw.loader.state( {
+ "foo();\nbar();\n" . 'mw.loader.state({
"ferry": "error",
"foo": "ready",
"bar": "ready"
-} );',
+});',
$response
);
}
use Wikimedia\Rdbms\IDatabase;
use Wikimedia\TestingAccessWrapper;
+/**
+ * @covers ResourceLoaderWikiModule
+ */
class ResourceLoaderWikiModuleTest extends ResourceLoaderTestCase {
/**
- * @covers ResourceLoaderWikiModule::__construct
* @dataProvider provideConstructor
*/
public function testConstructor( $params ) {
$this->assertInstanceOf( ResourceLoaderWikiModule::class, $module );
}
+ public static function provideConstructor() {
+ yield 'null' => [ null ];
+ yield 'empty' => [ [] ];
+ yield 'unknown settings' => [ [ 'foo' => 'baz' ] ];
+ yield 'real settings' => [ [ 'MediaWiki:Common.js' ] ];
+ }
+
private function prepareTitleInfo( array $mockInfo ) {
$module = TestingAccessWrapper::newFromClass( ResourceLoaderWikiModule::class );
$info = [];
return $info;
}
- public static function provideConstructor() {
- return [
- // Nothing
- [ null ],
- [ [] ],
- // Unrecognized settings
- [ [ 'foo' => 'baz' ] ],
- // Real settings
- [ [ 'scripts' => [ 'MediaWiki:Common.js' ] ] ],
- ];
- }
-
/**
* @dataProvider provideGetPages
- * @covers ResourceLoaderWikiModule::getPages
*/
public function testGetPages( $params, Config $config, $expected ) {
$module = new ResourceLoaderWikiModule( $params );
$getPages = new ReflectionMethod( $module, 'getPages' );
$getPages->setAccessible( true );
$out = $getPages->invoke( $module, ResourceLoaderContext::newDummyContext() );
- $this->assertEquals( $expected, $out );
+ $this->assertSame( $expected, $out );
}
public static function provideGetPages() {
}
/**
- * @covers ResourceLoaderWikiModule::getGroup
* @dataProvider provideGetGroup
*/
public function testGetGroup( $params, $expected ) {
$module = new ResourceLoaderWikiModule( $params );
- $this->assertEquals( $expected, $module->getGroup() );
+ $this->assertSame( $expected, $module->getGroup() );
}
public static function provideGetGroup() {
- return [
- // No group specified
- [ [], null ],
- // A random group
- [ [ 'group' => 'foobar' ], 'foobar' ],
+ yield 'no group' => [ [], null ];
+ yield 'some group' => [ [ 'group' => 'foobar' ], 'foobar' ];
+ }
+
+ /**
+ * @dataProvider provideGetType
+ */
+ public function testGetType( $params, $expected ) {
+ $module = new ResourceLoaderWikiModule( $params );
+ $this->assertSame( $expected, $module->getType() );
+ }
+
+ public static function provideGetType() {
+ yield 'empty' => [
+ [],
+ ResourceLoaderWikiModule::LOAD_GENERAL,
+ ];
+ yield 'scripts' => [
+ [ 'scripts' => [ 'Example.js' ] ],
+ ResourceLoaderWikiModule::LOAD_GENERAL,
+ ];
+ yield 'styles' => [
+ [ 'styles' => [ 'Example.css' ] ],
+ ResourceLoaderWikiModule::LOAD_STYLES,
+ ];
+ yield 'styles and scripts' => [
+ [ 'styles' => [ 'Example.css' ], 'scripts' => [ 'Example.js' ] ],
+ ResourceLoaderWikiModule::LOAD_GENERAL,
];
}
/**
- * @covers ResourceLoaderWikiModule::isKnownEmpty
* @dataProvider provideIsKnownEmpty
*/
public function testIsKnownEmpty( $titleInfo, $group, $dependencies, $expected ) {
$module = $this->getMockBuilder( ResourceLoaderWikiModule::class )
- ->setMethods( [ 'getTitleInfo', 'getGroup', 'getDependencies' ] )
- ->getMock();
- $module->expects( $this->any() )
- ->method( 'getTitleInfo' )
- ->will( $this->returnValue( $this->prepareTitleInfo( $titleInfo ) ) );
- $module->expects( $this->any() )
- ->method( 'getGroup' )
- ->will( $this->returnValue( $group ) );
- $module->expects( $this->any() )
- ->method( 'getDependencies' )
- ->will( $this->returnValue( $dependencies ) );
- $context = $this->getMockBuilder( ResourceLoaderContext::class )
->disableOriginalConstructor()
+ ->setMethods( [ 'getTitleInfo', 'getGroup', 'getDependencies' ] )
->getMock();
- $this->assertEquals( $expected, $module->isKnownEmpty( $context ) );
+ $module->method( 'getTitleInfo' )
+ ->willReturn( $this->prepareTitleInfo( $titleInfo ) );
+ $module->method( 'getGroup' )
+ ->willReturn( $group );
+ $module->method( 'getDependencies' )
+ ->willReturn( $dependencies );
+ $context = $this->createMock( ResourceLoaderContext::class );
+ $this->assertSame( $expected, $module->isKnownEmpty( $context ) );
}
public static function provideIsKnownEmpty() {
- return [
- // No valid pages
- [ [], 'test1', [], true ],
- // 'site' module with a non-empty page
- [
- [ 'MediaWiki:Common.js' => [ 'page_len' => 1234 ] ],
- 'site',
- [],
- false,
- ],
- // 'site' module without existing pages but dependencies
- [
- [],
- 'site',
- [ 'mobile.css' ],
- false,
- ],
- // 'site' module which is empty but has dependencies
- [
- [ 'MediaWiki:Common.js' => [ 'page_len' => 0 ] ],
- 'site',
- [ 'mobile.css' ],
- false,
- ],
- // 'site' module with an empty page
- [
- [ 'MediaWiki:Foo.js' => [ 'page_len' => 0 ] ],
- 'site',
- [],
- false,
- ],
- // 'user' module with a non-empty page
- [
- [ 'User:Example/common.js' => [ 'page_len' => 25 ] ],
- 'user',
- [],
- false,
- ],
- // 'user' module with an empty page
- [
- [ 'User:Example/foo.js' => [ 'page_len' => 0 ] ],
- 'user',
- [],
- true,
- ],
+ yield 'nothing' => [
+ [],
+ null,
+ [],
+ // No pages exist, considered empty.
+ true,
+ ];
+
+ yield 'an empty page exists (no group)' => [
+ [ 'Project:Example/foo.js' => [ 'page_len' => 0 ] ],
+ null,
+ [],
+ // There is an existing page, so we should let the module be queued.
+ // Its emptiness might be temporary, hence considered non-empty (T70488).
+ false,
+ ];
+ yield 'an empty page exists (site group)' => [
+ [ 'MediaWiki:Foo.js' => [ 'page_len' => 0 ] ],
+ 'site',
+ [],
+ // There is an existing page, hence considered non-empty.
+ false,
+ ];
+ yield 'an empty page exists (user group)' => [
+ [ 'User:Example/foo.js' => [ 'page_len' => 0 ] ],
+ 'user',
+ [],
+ // There is an existing page, but it is empty.
+ // For user-specific modules, don't bother loading a known-empty module.
+ // Given user-specific HTML output, this will vary and re-appear if/when
+ // the page becomes non-empty again.
+ true,
+ ];
+
+ yield 'no pages but having dependencies (no group)' => [
+ [],
+ null,
+ [ 'another-module' ],
+ false,
+ ];
+ yield 'no pages but having dependencies (site group)' => [
+ [],
+ 'site',
+ [ 'another-module' ],
+ false,
+ ];
+ yield 'no pages but having dependencies (user group)' => [
+ [],
+ 'user',
+ [ 'another-module' ],
+ false,
+ ];
+
+ yield 'a non-empty page exists (user group)' => [
+ [ 'User:Example/foo.js' => [ 'page_len' => 25 ] ],
+ 'user',
+ [],
+ false,
+ ];
+ yield 'a non-empty page exists (site group)' => [
+ [ 'MediaWiki:Foo.js' => [ 'page_len' => 25 ] ],
+ 'site',
+ [],
+ false,
];
}
- /**
- * @covers ResourceLoaderWikiModule::getTitleInfo
- */
public function testGetTitleInfo() {
$pages = [
'MediaWiki:Common.css' => [ 'type' => 'styles' ],
] );
$expected = $titleInfo;
- $module = $this->getMockBuilder( TestResourceLoaderWikiModule::class )
- ->setMethods( [ 'getPages' ] )
+ $module = $this->getMockBuilder( ResourceLoaderWikiModule::class )
+ ->setMethods( [ 'getPages', 'getTitleInfo' ] )
->getMock();
$module->method( 'getPages' )->willReturn( $pages );
- // Can't mock static methods
- $module::$returnFetchTitleInfo = $titleInfo;
+ $module->method( 'getTitleInfo' )->willReturn( $titleInfo );
$context = $this->getMockBuilder( ResourceLoaderContext::class )
->disableOriginalConstructor()
->getMock();
$module = TestingAccessWrapper::newFromObject( $module );
- $this->assertEquals( $expected, $module->getTitleInfo( $context ), 'Title info' );
+ $this->assertSame( $expected, $module->getTitleInfo( $context ), 'Title info' );
}
- /**
- * @covers ResourceLoaderWikiModule::getTitleInfo
- * @covers ResourceLoaderWikiModule::setTitleInfo
- * @covers ResourceLoaderWikiModule::preloadTitleInfo
- */
public function testGetPreloadedTitleInfo() {
$pages = [
'MediaWiki:Common.css' => [ 'type' => 'styles' ],
);
TestResourceLoaderWikiModule::preloadTitleInfo(
$context,
- wfGetDB( DB_REPLICA ),
+ $this->createMock( IDatabase::class ),
[ 'testmodule' ]
);
$module = TestingAccessWrapper::newFromObject( $module );
- $this->assertEquals( $expected, $module->getTitleInfo( $context ), 'Title info' );
+ $this->assertSame( $expected, $module->getTitleInfo( $context ), 'Title info' );
}
- /**
- * @covers ResourceLoaderWikiModule::preloadTitleInfo
- */
public function testGetPreloadedBadTitle() {
// Set up
TestResourceLoaderWikiModule::$returnFetchTitleInfo = [];
// Act
TestResourceLoaderWikiModule::preloadTitleInfo(
$context,
- wfGetDB( DB_REPLICA ),
+ $this->createMock( IDatabase::class ),
[ 'testmodule' ]
);
$this->assertSame( [], $module->getTitleInfo( $context ), 'Title info' );
}
- /**
- * @covers ResourceLoaderWikiModule::preloadTitleInfo
- */
public function testGetPreloadedTitleInfoEmpty() {
$context = new ResourceLoaderContext( new EmptyResourceLoader(), new FauxRequest() );
- // Covers early return
+ // This covers the early return case
$this->assertSame(
null,
ResourceLoaderWikiModule::preloadTitleInfo(
$context,
- wfGetDB( DB_REPLICA ),
+ $this->createMock( IDatabase::class ),
[]
)
);
}
public static function provideGetContent() {
- return [
- 'Bad title' => [ null, '[x]' ],
- 'Dead redirect' => [ null, [
- 'text' => 'Dead redirect',
- 'title' => 'Dead_redirect',
- 'redirect' => 1,
- ] ],
- 'Bad content model' => [ null, [
- 'text' => 'MediaWiki:Wikitext',
- 'ns' => NS_MEDIAWIKI,
- 'title' => 'Wikitext',
- ] ],
- 'No JS content found' => [ null, [
- 'text' => 'MediaWiki:Script.js',
- 'ns' => NS_MEDIAWIKI,
- 'title' => 'Script.js',
- ] ],
- 'No CSS content found' => [ null, [
- 'text' => 'MediaWiki:Styles.css',
- 'ns' => NS_MEDIAWIKI,
- 'title' => 'Script.css',
- ] ],
- ];
+ yield 'Bad title' => [ null, '[x]' ];
+ yield 'Dead redirect' => [ null, [
+ 'text' => 'Dead redirect',
+ 'title' => 'Dead_redirect',
+ 'redirect' => 1,
+ ] ];
+ yield 'Bad content model' => [ null, [
+ 'text' => 'MediaWiki:Wikitext',
+ 'ns' => NS_MEDIAWIKI,
+ 'title' => 'Wikitext',
+ ] ];
+ yield 'No JS content found' => [ null, [
+ 'text' => 'MediaWiki:Script.js',
+ 'ns' => NS_MEDIAWIKI,
+ 'title' => 'Script.js',
+ ] ];
+ yield 'No CSS content found' => [ null, [
+ 'text' => 'MediaWiki:Styles.css',
+ 'ns' => NS_MEDIAWIKI,
+ 'title' => 'Script.css',
+ ] ];
}
/**
- * @covers ResourceLoaderWikiModule::getContent
* @dataProvider provideGetContent
*/
public function testGetContent( $expected, $title ) {
$context = $this->getResourceLoaderContext( [], new EmptyResourceLoader );
$module = $this->getMockBuilder( ResourceLoaderWikiModule::class )
->setMethods( [ 'getContentObj' ] )->getMock();
- $module->expects( $this->any() )
- ->method( 'getContentObj' )->willReturn( null );
+ $module->method( 'getContentObj' )
+ ->willReturn( null );
if ( is_array( $title ) ) {
$title += [ 'ns' => NS_MAIN, 'id' => 1, 'len' => 1, 'redirect' => 0 ];
}
$module = TestingAccessWrapper::newFromObject( $module );
- $this->assertEquals(
+ $this->assertSame(
$expected,
$module->getContent( $titleText, $context )
);
}
- /**
- * @covers ResourceLoaderWikiModule::getContent
- * @covers ResourceLoaderWikiModule::getContentObj
- * @covers ResourceLoaderWikiModule::shouldEmbedModule
- */
public function testContentOverrides() {
$pages = [
'MediaWiki:Common.css' => [ 'type' => 'style' ],
];
- $module = $this->getMockBuilder( TestResourceLoaderWikiModule::class )
+ $module = $this->getMockBuilder( ResourceLoaderWikiModule::class )
->setMethods( [ 'getPages' ] )
->getMock();
$module->method( 'getPages' )->willReturn( $pages );
} );
$this->assertTrue( $module->shouldEmbedModule( $context ) );
- $this->assertEquals( [
+ $this->assertSame( [
'all' => [
"/*\nMediaWiki:Common.css\n*/\n.override{}"
]
$this->assertFalse( $module->shouldEmbedModule( $context ) );
}
- /**
- * @covers ResourceLoaderWikiModule::getContent
- * @covers ResourceLoaderWikiModule::getContentObj
- */
public function testGetContentForRedirects() {
// Set up context and module object
$context = new DerivativeResourceLoaderContext(
$module = $this->getMockBuilder( ResourceLoaderWikiModule::class )
->setMethods( [ 'getPages' ] )
->getMock();
- $module->expects( $this->any() )
- ->method( 'getPages' )
- ->will( $this->returnValue( [
+ $module->method( 'getPages' )
+ ->willReturn( [
'MediaWiki:Redirect.js' => [ 'type' => 'script' ]
- ] ) );
+ ] );
$context->setContentOverrideCallback( function ( Title $title ) {
if ( $title->getPrefixedText() === 'MediaWiki:Redirect.js' ) {
$handler = new JavaScriptContentHandler();
1 // redirect
);
- $this->assertEquals(
+ $this->assertSame(
"/*\nMediaWiki:Redirect.js\n*/\ntarget;\n",
$module->getScript( $context ),
'Redirect resolved by getContent'
);
}
- function tearDown() {
+ public function tearDown() {
Title::clearCaches();
parent::tearDown();
}