From ca32f08966f1b51fcb19460f0996bb0c4048e6fe Mon Sep 17 00:00:00 2001 From: Pierre Schmitz Date: Sat, 3 Dec 2011 13:29:22 +0100 Subject: Update to MediaWiki 1.18.0 * also update ArchLinux skin to chagnes in MonoBook * Use only css to hide our menu bar when printing --- includes/resourceloader/ResourceLoader.php | 297 ++++++++++++++------- includes/resourceloader/ResourceLoaderContext.php | 50 +++- .../resourceloader/ResourceLoaderFileModule.php | 270 +++++++++++-------- .../ResourceLoaderFilePageModule.php | 11 + includes/resourceloader/ResourceLoaderModule.php | 189 ++++++++++++- .../ResourceLoaderNoscriptModule.php | 52 ++++ .../resourceloader/ResourceLoaderSiteModule.php | 4 +- .../resourceloader/ResourceLoaderStartUpModule.php | 143 ++++++---- .../ResourceLoaderUserGroupsModule.php | 59 ++++ .../resourceloader/ResourceLoaderUserModule.php | 12 +- .../ResourceLoaderUserOptionsModule.php | 29 +- .../ResourceLoaderUserTokensModule.php | 63 +++++ .../resourceloader/ResourceLoaderWikiModule.php | 44 +-- 13 files changed, 937 insertions(+), 286 deletions(-) create mode 100644 includes/resourceloader/ResourceLoaderFilePageModule.php create mode 100644 includes/resourceloader/ResourceLoaderNoscriptModule.php create mode 100644 includes/resourceloader/ResourceLoaderUserGroupsModule.php create mode 100644 includes/resourceloader/ResourceLoaderUserTokensModule.php (limited to 'includes/resourceloader') diff --git a/includes/resourceloader/ResourceLoader.php b/includes/resourceloader/ResourceLoader.php index 191bc9f0..2a2e2981 100644 --- a/includes/resourceloader/ResourceLoader.php +++ b/includes/resourceloader/ResourceLoader.php @@ -29,7 +29,7 @@ class ResourceLoader { /* Protected Static Members */ - protected static $filterCacheVersion = 2; + protected static $filterCacheVersion = 4; /** Array: List of module name/ResourceLoaderModule object pairs */ protected $modules = array(); @@ -40,15 +40,15 @@ class ResourceLoader { /** * Loads information stored in the database about modules. - * - * This method grabs modules dependencies from the database and updates modules + * + * This method grabs modules dependencies from the database and updates modules * objects. - * - * This is not inside the module code because it is much faster to - * request all of the information at once than it is to have each module + * + * This is not inside the module code because it is much faster to + * request all of the information at once than it is to have each module * requests its own information. This sacrifice of modularity yields a substantial * performance improvement. - * + * * @param $modules Array: List of module names to preload information for * @param $context ResourceLoaderContext: Context to load the information within */ @@ -59,11 +59,11 @@ class ResourceLoader { $dbr = wfGetDB( DB_SLAVE ); $skin = $context->getSkin(); $lang = $context->getLanguage(); - + // Get file dependency information $res = $dbr->select( 'module_deps', array( 'md_module', 'md_deps' ), array( 'md_module' => $modules, - 'md_skin' => $context->getSkin() + 'md_skin' => $skin ), __METHOD__ ); @@ -80,7 +80,7 @@ class ResourceLoader { foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) { $this->getModule( $name )->setFileDependencies( $skin, array() ); } - + // Get message blob mtimes. Only do this for modules with messages $modulesWithMessages = array(); foreach ( $modules as $name ) { @@ -96,11 +96,11 @@ class ResourceLoader { ), __METHOD__ ); foreach ( $res as $row ) { - $this->getModule( $row->mr_resource )->setMsgBlobMtime( $lang, + $this->getModule( $row->mr_resource )->setMsgBlobMtime( $lang, wfTimestamp( TS_UNIX, $row->mr_timestamp ) ); unset( $modulesWithoutMessages[$row->mr_resource] ); } - } + } foreach ( array_keys( $modulesWithoutMessages ) as $name ) { $this->getModule( $name )->setMsgBlobMtime( $lang, 0 ); } @@ -108,14 +108,14 @@ class ResourceLoader { /** * Runs JavaScript or CSS data through a filter, caching the filtered result for future calls. - * + * * Available filters are: * - minify-js \see JavaScriptMinifier::minify * - minify-css \see CSSMin::minify - * - * If $data is empty, only contains whitespace or the filter was unknown, + * + * If $data is empty, only contains whitespace or the filter was unknown, * $data is returned unmodified. - * + * * @param $filter String: Name of filter to run * @param $data String: Text to filter, such as JavaScript or CSS text * @return String: Filtered data, or a comment containing an error message @@ -124,10 +124,10 @@ class ResourceLoader { global $wgResourceLoaderMinifierStatementsOnOwnLine, $wgResourceLoaderMinifierMaxLineLength; wfProfileIn( __METHOD__ ); - // For empty/whitespace-only data or for unknown filters, don't perform + // For empty/whitespace-only data or for unknown filters, don't perform // any caching or processing - if ( trim( $data ) === '' - || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) + if ( trim( $data ) === '' + || !in_array( $filter, array( 'minify-js', 'minify-css' ) ) ) { wfProfileOut( __METHOD__ ); return $data; @@ -135,7 +135,7 @@ class ResourceLoader { // Try for cache hit // Use CACHE_ANYTHING since filtering is very slow compared to DB queries - $key = wfMemcKey( 'resourceloader', 'filter', $filter, md5( $data ) ); + $key = wfMemcKey( 'resourceloader', 'filter', $filter, self::$filterCacheVersion, md5( $data ) ); $cache = wfGetCache( CACHE_ANYTHING ); $cacheEntry = $cache->get( $key ); if ( is_string( $cacheEntry ) ) { @@ -143,6 +143,7 @@ class ResourceLoader { return $cacheEntry; } + $result = ''; // Run the filter - we've already verified one of these will work try { switch ( $filter ) { @@ -151,9 +152,11 @@ class ResourceLoader { $wgResourceLoaderMinifierStatementsOnOwnLine, $wgResourceLoaderMinifierMaxLineLength ); + $result .= "\n\n/* cache key: $key */\n"; break; case 'minify-css': $result = CSSMin::minify( $data ); + $result .= "\n\n/* cache key: $key */\n"; break; } @@ -165,7 +168,7 @@ class ResourceLoader { } wfProfileOut( __METHOD__ ); - + return $result; } @@ -176,24 +179,24 @@ class ResourceLoader { */ public function __construct() { global $IP, $wgResourceModules; - + wfProfileIn( __METHOD__ ); - + // Register core modules $this->register( include( "$IP/resources/Resources.php" ) ); // Register extension modules wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) ); $this->register( $wgResourceModules ); - + wfProfileOut( __METHOD__ ); } /** * Registers a module with the ResourceLoader system. - * + * * @param $name Mixed: Name of module as a string or List of name/object pairs as an array - * @param $info Module info array. For backwards compatibility with 1.17alpha, - * this may also be a ResourceLoaderModule object. Optional when using + * @param $info Module info array. For backwards compatibility with 1.17alpha, + * this may also be a ResourceLoaderModule object. Optional when using * multiple-registration calling style. * @throws MWException: If a duplicate module registration is attempted * @throws MWException: If a module name contains illegal characters (pipes or commas) @@ -217,14 +220,14 @@ class ResourceLoader { if ( isset( $this->moduleInfos[$name] ) ) { // A module has already been registered by this name throw new MWException( - 'ResourceLoader duplicate registration error. ' . + 'ResourceLoader duplicate registration error. ' . 'Another module has already been registered as ' . $name ); } - + // Check $name for illegal characters - if ( preg_match( '/[|,]/', $name ) ) { - throw new MWException( "ResourceLoader module name '$name' is invalid. Names may not contain pipes (|) or commas (,)" ); + if ( preg_match( '/[|,!]/', $name ) ) { + throw new MWException( "ResourceLoader module name '$name' is invalid. Names may not contain pipes (|), commas (,) or exclamation marks (!)" ); } // Attach module @@ -232,7 +235,7 @@ class ResourceLoader { // Old calling convention // Validate the input if ( !( $info instanceof ResourceLoaderModule ) ) { - throw new MWException( 'ResourceLoader invalid module error. ' . + throw new MWException( 'ResourceLoader invalid module error. ' . 'Instances of ResourceLoaderModule expected.' ); } @@ -260,7 +263,7 @@ class ResourceLoader { * Get the ResourceLoaderModule object for a given module name. * * @param $name String: Module name - * @return Mixed: ResourceLoaderModule if module has been registered, null otherwise + * @return ResourceLoaderModule if module has been registered, null otherwise */ public function getModule( $name ) { if ( !isset( $this->modules[$name] ) ) { @@ -295,7 +298,7 @@ class ResourceLoader { */ public function respond( ResourceLoaderContext $context ) { global $wgResourceLoaderMaxage, $wgCacheEpoch; - + // Buffer output to catch warnings. Normally we'd use ob_clean() on the // top-level output buffer to clear warnings, but that breaks when ob_gzhandler // is used: ob_clean() will clear the GZIP header in that case and it won't come @@ -319,13 +322,13 @@ class ResourceLoader { } } - // If a version wasn't specified we need a shorter expiry time for updates + // If a version wasn't specified we need a shorter expiry time for updates // to propagate to clients quickly if ( is_null( $context->getVersion() ) ) { $maxage = $wgResourceLoaderMaxage['unversioned']['client']; $smaxage = $wgResourceLoaderMaxage['unversioned']['server']; } - // If a version was specified we can use a longer expiry time since changing + // If a version was specified we can use a longer expiry time since changing // version numbers causes cache misses else { $maxage = $wgResourceLoaderMaxage['versioned']['client']; @@ -343,7 +346,7 @@ class ResourceLoader { wfProfileIn( __METHOD__.'-getModifiedTime' ); $private = false; - // To send Last-Modified and support If-Modified-Since, we need to detect + // To send Last-Modified and support If-Modified-Since, we need to detect // the last modified time $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch ); foreach ( $modules as $module ) { @@ -387,7 +390,8 @@ class ResourceLoader { // Some clients send "timestamp;length=123". Strip the part after the first ';' // so we get a valid timestamp. $ims = $context->getRequest()->getHeader( 'If-Modified-Since' ); - if ( $ims !== false ) { + // Never send 304s in debug mode + if ( $ims !== false && !$context->getDebug() ) { $imsTS = strtok( $ims, ';' ); if ( $mtime <= wfTimestamp( TS_UNIX, $imsTS ) ) { // There's another bug in ob_gzhandler (see also the comment at @@ -406,17 +410,17 @@ class ResourceLoader { for ( $i = 0; $i < ob_get_level(); $i++ ) { ob_end_clean(); } - + header( 'HTTP/1.0 304 Not Modified' ); header( 'Status: 304 Not Modified' ); wfProfileOut( __METHOD__ ); return; } } - + // Generate a response $response = $this->makeModuleResponse( $context, $modules, $missing ); - + // Prepend comments indicating exceptions $response = $exceptions . $response; @@ -435,21 +439,21 @@ class ResourceLoader { /** * Generates code for a response - * + * * @param $context ResourceLoaderContext: Context in which to generate a response * @param $modules Array: List of module objects keyed by module name * @param $missing Array: List of unavailable modules (optional) * @return String: Response data */ - public function makeModuleResponse( ResourceLoaderContext $context, - array $modules, $missing = array() ) + public function makeModuleResponse( ResourceLoaderContext $context, + array $modules, $missing = array() ) { $out = ''; $exceptions = ''; if ( $modules === array() && $missing === array() ) { return '/* No modules requested. Max made me put this here */'; } - + wfProfileIn( __METHOD__ ); // Pre-fetch blobs if ( $context->shouldIncludeMessages() ) { @@ -467,18 +471,33 @@ class ResourceLoader { foreach ( $modules as $name => $module ) { wfProfileIn( __METHOD__ . '-' . $name ); try { - // Scripts $scripts = ''; if ( $context->shouldIncludeScripts() ) { - // bug 27054: Append semicolon to prevent weird bugs - // caused by files not terminating their statements right - $scripts .= $module->getScript( $context ) . ";\n"; + // If we are in debug mode, we'll want to return an array of URLs if possible + // However, we can't do this if the module doesn't support it + // We also can't do this if there is an only= parameter, because we have to give + // the module a way to return a load.php URL without causing an infinite loop + if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) { + $scripts = $module->getScriptURLsForDebug( $context ); + } else { + $scripts = $module->getScript( $context ); + if ( is_string( $scripts ) ) { + // bug 27054: Append semicolon to prevent weird bugs + // caused by files not terminating their statements right + $scripts .= ";\n"; + } + } } - // Styles $styles = array(); if ( $context->shouldIncludeStyles() ) { - $styles = $module->getStyles( $context ); + // If we are in debug mode, we'll want to return an array of URLs + // See comment near shouldIncludeScripts() for more details + if ( $context->getDebug() && !$context->getOnly() && $module->supportsURLLoading() ) { + $styles = $module->getStyleURLsForDebug( $context ); + } else { + $styles = $module->getStyles( $context ); + } } // Messages @@ -487,7 +506,11 @@ class ResourceLoader { // Append output switch ( $context->getOnly() ) { case 'scripts': - $out .= $scripts; + if ( is_string( $scripts ) ) { + $out .= $scripts; + } elseif ( is_array( $scripts ) ) { + $out .= self::makeLoaderImplementScript( $name, $scripts, array(), array() ); + } break; case 'styles': $out .= self::makeCombinedStyles( $styles ); @@ -496,11 +519,13 @@ class ResourceLoader { $out .= self::makeMessageSetScript( new XmlJsCode( $messagesBlob ) ); break; default: - // Minify CSS before embedding in mediaWiki.loader.implement call + // Minify CSS before embedding in mw.loader.implement call // (unless in debug mode) if ( !$context->getDebug() ) { foreach ( $styles as $media => $style ) { - $styles[$media] = $this->filter( 'minify-css', $style ); + if ( is_string( $style ) ) { + $styles[$media] = $this->filter( 'minify-css', $style ); + } } } $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, @@ -521,10 +546,10 @@ class ResourceLoader { // Update module states if ( $context->shouldIncludeScripts() ) { // Set the state of modules loaded as only scripts to ready - if ( count( $modules ) && $context->getOnly() === 'scripts' - && !isset( $modules['startup'] ) ) + if ( count( $modules ) && $context->getOnly() === 'scripts' + && !isset( $modules['startup'] ) ) { - $out .= self::makeLoaderStateScript( + $out .= self::makeLoaderStateScript( array_fill_keys( array_keys( $modules ), 'ready' ) ); } // Set the state of modules which were requested but unavailable as missing @@ -540,7 +565,7 @@ class ResourceLoader { $out = $this->filter( 'minify-js', $out ); } } - + wfProfileOut( __METHOD__ ); return $exceptions . $out; } @@ -548,26 +573,30 @@ class ResourceLoader { /* Static Methods */ /** - * Returns JS code to call to mediaWiki.loader.implement for a module with + * Returns JS code to call to mw.loader.implement for a module with * given properties. * * @param $name Module name - * @param $scripts Array: List of JavaScript code snippets to be executed after the - * module is loaded - * @param $styles Array: List of CSS strings keyed by media type - * @param $messages Mixed: List of messages associated with this module. May either be an + * @param $scripts Mixed: List of URLs to JavaScript files or String of JavaScript code + * @param $styles Mixed: List of CSS strings keyed by media type, or list of lists of URLs to + * CSS files keyed by media type + * @param $messages Mixed: List of messages associated with this module. May either be an * associative array mapping message key to value, or a JSON-encoded message blob containing * the same data, wrapped in an XmlJsCode object. + * + * @return string */ public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) { - if ( is_array( $scripts ) ) { - $scripts = implode( $scripts, "\n" ); + if ( is_string( $scripts ) ) { + $scripts = new XmlJsCode( "function( $ ) {{$scripts}}" ); + } elseif ( !is_array( $scripts ) ) { + throw new MWException( 'Invalid scripts error. Array of URLs or string of code expected.' ); } - return Xml::encodeJsCall( - 'mediaWiki.loader.implement', + return Xml::encodeJsCall( + 'mw.loader.implement', array( $name, - new XmlJsCode( "function( $, mw ) {{$scripts}}" ), + $scripts, (object)$styles, (object)$messages ) ); @@ -578,16 +607,20 @@ class ResourceLoader { * * @param $messages Mixed: Either an associative array mapping message key to value, or a * JSON-encoded message blob containing the same data, wrapped in an XmlJsCode object. + * + * @return string */ public static function makeMessageSetScript( $messages ) { - return Xml::encodeJsCall( 'mediaWiki.messages.set', array( (object)$messages ) ); + return Xml::encodeJsCall( 'mw.messages.set', array( (object)$messages ) ); } /** - * Combines an associative array mapping media type to CSS into a + * Combines an associative array mapping media type to CSS into a * single stylesheet with @media blocks. * * @param $styles Array: List of CSS strings keyed by media type + * + * @return string */ public static function makeCombinedStyles( array $styles ) { $out = ''; @@ -595,10 +628,10 @@ class ResourceLoader { // Transform the media type based on request params and config // The way that this relies on $wgRequest to propagate request params is slightly evil $media = OutputPage::transformCssMedia( $media ); - + if ( $media === null ) { // Skip - } else if ( $media === '' || $media == 'all' ) { + } elseif ( $media === '' || $media == 'all' ) { // Don't output invalid or frivolous @media statements $out .= "$style\n"; } else { @@ -609,7 +642,7 @@ class ResourceLoader { } /** - * Returns a JS call to mediaWiki.loader.state, which sets the state of a + * Returns a JS call to mw.loader.state, which sets the state of a * module or modules to a given value. Has two calling conventions: * * - ResourceLoader::makeLoaderStateScript( $name, $state ): @@ -617,36 +650,43 @@ class ResourceLoader { * * - ResourceLoader::makeLoaderStateScript( array( $name => $state, ... ) ): * Set the state of modules with the given names to the given states + * + * @param $name string + * @param $state + * + * @return string */ public static function makeLoaderStateScript( $name, $state = null ) { if ( is_array( $name ) ) { - return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name ) ); + return Xml::encodeJsCall( 'mw.loader.state', array( $name ) ); } else { - return Xml::encodeJsCall( 'mediaWiki.loader.state', array( $name, $state ) ); + return Xml::encodeJsCall( 'mw.loader.state', array( $name, $state ) ); } } /** * Returns JS code which calls the script given by $script. The script will - * be called with local variables name, version, dependencies and group, - * which will have values corresponding to $name, $version, $dependencies - * and $group as supplied. + * be called with local variables name, version, dependencies and group, + * which will have values corresponding to $name, $version, $dependencies + * and $group as supplied. * * @param $name String: Module name * @param $version Integer: Module version number as a timestamp * @param $dependencies Array: List of module names on which this module depends * @param $group String: Group which the module is in. * @param $script String: JavaScript code + * + * @return string */ public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $script ) { $script = str_replace( "\n", "\n\t", trim( $script ) ); - return Xml::encodeJsCall( + return Xml::encodeJsCall( "( function( name, version, dependencies, group ) {\n\t$script\n} )", array( $name, $version, $dependencies, $group ) ); } /** - * Returns JS code which calls mediaWiki.loader.register with the given + * Returns JS code which calls mw.loader.register with the given * parameters. Has three calling conventions: * * - ResourceLoader::makeLoaderRegisterScript( $name, $version, $dependencies, $group ): @@ -666,43 +706,49 @@ class ResourceLoader { * @param $version Integer: Module version number as a timestamp * @param $dependencies Array: List of module names on which this module depends * @param $group String: group which the module is in. + * + * @return string */ - public static function makeLoaderRegisterScript( $name, $version = null, - $dependencies = null, $group = null ) + public static function makeLoaderRegisterScript( $name, $version = null, + $dependencies = null, $group = null ) { if ( is_array( $name ) ) { - return Xml::encodeJsCall( 'mediaWiki.loader.register', array( $name ) ); + return Xml::encodeJsCall( 'mw.loader.register', array( $name ) ); } else { $version = (int) $version > 1 ? (int) $version : 1; - return Xml::encodeJsCall( 'mediaWiki.loader.register', + return Xml::encodeJsCall( 'mw.loader.register', array( $name, $version, $dependencies, $group ) ); } } /** - * Returns JS code which runs given JS code if the client-side framework is + * Returns JS code which runs given JS code if the client-side framework is * present. * * @param $script String: JavaScript code + * + * @return string */ public static function makeLoaderConditionalScript( $script ) { $script = str_replace( "\n", "\n\t", trim( $script ) ); - return "if ( window.mediaWiki ) {\n\t$script\n}\n"; + return "if(window.mw){\n\t$script\n}\n"; } /** - * Returns JS code which will set the MediaWiki configuration array to + * Returns JS code which will set the MediaWiki configuration array to * the given value. * * @param $configuration Array: List of configuration values keyed by variable name + * + * @return string */ public static function makeConfigSetScript( array $configuration ) { - return Xml::encodeJsCall( 'mediaWiki.config.set', array( $configuration ) ); + return Xml::encodeJsCall( 'mw.config.set', array( $configuration ) ); } - + /** * Convert an array of module names to a packed query string. - * + * * For example, array( 'foo.bar', 'foo.baz', 'bar.baz', 'bar.quux' ) * becomes 'foo.bar,baz|bar.baz,quux' * @param $modules array of module names (strings) @@ -716,15 +762,16 @@ class ResourceLoader { $suffix = $pos === false ? $module : substr( $module, $pos + 1 ); $groups[$prefix][] = $suffix; } - + $arr = array(); foreach ( $groups as $prefix => $suffixes ) { $p = $prefix === '' ? '' : $prefix . '.'; $arr[] = $p . implode( ',', $suffixes ); } - return implode( '|', $arr ); + $str = implode( '|', $arr ); + return $str; } - + /** * Determine whether debug mode was requested * Order of priority is 1) request param, 2) cookie, 3) $wg setting @@ -733,9 +780,71 @@ class ResourceLoader { public static function inDebugMode() { global $wgRequest, $wgResourceLoaderDebug; static $retval = null; - if ( !is_null( $retval ) ) + if ( !is_null( $retval ) ) { return $retval; + } return $retval = $wgRequest->getFuzzyBool( 'debug', $wgRequest->getCookie( 'resourceLoaderDebug', '', $wgResourceLoaderDebug ) ); } + + /** + * Build a load.php URL + * @param $modules array of module names (strings) + * @param $lang string Language code + * @param $skin string Skin name + * @param $user string|null User name. If null, the &user= parameter is omitted + * @param $version string|null Versioning timestamp + * @param $debug bool Whether the request should be in debug mode + * @param $only string|null &only= parameter + * @param $printable bool Printable mode + * @param $handheld bool Handheld mode + * @param $extraQuery array Extra query parameters to add + * @return string URL to load.php. May be protocol-relative (if $wgLoadScript is procol-relative) + */ + public static function makeLoaderURL( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null, + $printable = false, $handheld = false, $extraQuery = array() ) { + global $wgLoadScript; + $query = self::makeLoaderQuery( $modules, $lang, $skin, $user, $version, $debug, + $only, $printable, $handheld, $extraQuery + ); + + // Prevent the IE6 extension check from being triggered (bug 28840) + // by appending a character that's invalid in Windows extensions ('*') + return wfExpandUrl( wfAppendQuery( $wgLoadScript, $query ) . '&*', PROTO_RELATIVE ); + } + + /** + * Build a query array (array representation of query string) for load.php. Helper + * function for makeLoaderURL(). + * @return array + */ + public static function makeLoaderQuery( $modules, $lang, $skin, $user = null, $version = null, $debug = false, $only = null, + $printable = false, $handheld = false, $extraQuery = array() ) { + $query = array( + 'modules' => self::makePackedModulesString( $modules ), + 'lang' => $lang, + 'skin' => $skin, + 'debug' => $debug ? 'true' : 'false', + ); + if ( $user !== null ) { + $query['user'] = $user; + } + if ( $version !== null ) { + $query['version'] = $version; + } + if ( $only !== null ) { + $query['only'] = $only; + } + if ( $printable ) { + $query['printable'] = 1; + } + if ( $handheld ) { + $query['handheld'] = 1; + } + $query += $extraQuery; + + // Make queries uniform in order + ksort( $query ); + return $query; + } } diff --git a/includes/resourceloader/ResourceLoaderContext.php b/includes/resourceloader/ResourceLoaderContext.php index bf059b46..326b7c4a 100644 --- a/includes/resourceloader/ResourceLoaderContext.php +++ b/includes/resourceloader/ResourceLoaderContext.php @@ -73,6 +73,8 @@ class ResourceLoaderContext { */ public static function expandModuleNames( $modules ) { $retval = array(); + // For backwards compatibility with an earlier hack, replace ! with . + $modules = str_replace( '!', '.', $modules ); $exploded = explode( '|', $modules ); foreach ( $exploded as $group ) { if ( strpos( $group, ',' ) === false ) { @@ -98,18 +100,30 @@ class ResourceLoaderContext { return $retval; } + /** + * @return ResourceLoader + */ public function getResourceLoader() { return $this->resourceLoader; } - + + /** + * @return WebRequest + */ public function getRequest() { return $this->request; } + /** + * @return array + */ public function getModules() { return $this->modules; } + /** + * @return string + */ public function getLanguage() { if ( $this->language === null ) { global $wgLang; @@ -121,49 +135,79 @@ class ResourceLoaderContext { return $this->language; } + /** + * @return string + */ public function getDirection() { if ( $this->direction === null ) { $this->direction = $this->request->getVal( 'dir' ); if ( !$this->direction ) { - global $wgContLang; - $this->direction = $wgContLang->getDir(); + # directionality based on user language (see bug 6100) + $this->direction = Language::factory( $this->language )->getDir(); } } return $this->direction; } + /** + * @return string + */ public function getSkin() { return $this->skin; } + /** + * @return string + */ public function getUser() { return $this->user; } + /** + * @return bool + */ public function getDebug() { return $this->debug; } + /** + * @return String + */ public function getOnly() { return $this->only; } + /** + * @return String + */ public function getVersion() { return $this->version; } + /** + * @return bool + */ public function shouldIncludeScripts() { return is_null( $this->only ) || $this->only === 'scripts'; } + /** + * @return bool + */ public function shouldIncludeStyles() { return is_null( $this->only ) || $this->only === 'styles'; } + /** + * @return bool + */ public function shouldIncludeMessages() { return is_null( $this->only ) || $this->only === 'messages'; } + /** + * @return string + */ public function getHash() { if ( !isset( $this->hash ) ) { $this->hash = implode( '|', array( diff --git a/includes/resourceloader/ResourceLoaderFileModule.php b/includes/resourceloader/ResourceLoaderFileModule.php index 1c37eb07..f38c60ae 100644 --- a/includes/resourceloader/ResourceLoaderFileModule.php +++ b/includes/resourceloader/ResourceLoaderFileModule.php @@ -78,6 +78,8 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected $messages = array(); /** String: Name of group to load this module in */ protected $group; + /** String: Position on the page to load this module at */ + protected $position = 'bottom'; /** Boolean: Link to raw files in debug mode */ protected $debugRaw = true; /** @@ -95,15 +97,16 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Constructs a new module from an options array. - * + * * @param $options Array: List of options; if not given or empty, an empty module will be * constructed * @param $localBasePath String: Base path to prepend to all local paths in $options. Defaults * to $IP * @param $remoteBasePath String: Base path to prepend to all remote paths in $options. Defaults * to $wgScriptPath - * - * @example $options + * + * Below is a description for the $options array: + * @code * array( * // Base path to prepend to all local paths in $options. Defaults to $IP * 'localBasePath' => [base path], @@ -137,14 +140,21 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * 'messages' => [array of message key strings], * // Group which this module should be loaded together with * 'group' => [group name string], + * // Position on the page to load this module at + * 'position' => ['bottom' (default) or 'top'] * ) + * @endcode */ - public function __construct( $options = array(), $localBasePath = null, - $remoteBasePath = null ) + public function __construct( $options = array(), $localBasePath = null, + $remoteBasePath = null ) { - global $IP, $wgScriptPath; + global $IP, $wgScriptPath, $wgResourceBasePath; $this->localBasePath = $localBasePath === null ? $IP : $localBasePath; - $this->remoteBasePath = $remoteBasePath === null ? $wgScriptPath : $remoteBasePath; + if ( $remoteBasePath !== null ) { + $this->remoteBasePath = $remoteBasePath; + } else { + $this->remoteBasePath = $wgResourceBasePath === null ? $wgScriptPath : $wgResourceBasePath; + } if ( isset( $options['remoteExtPath'] ) ) { global $wgExtensionAssetsPath; @@ -166,14 +176,14 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { case 'skinStyles': if ( !is_array( $option ) ) { throw new MWException( - "Invalid collated file path list error. " . + "Invalid collated file path list error. " . "'$option' given, array expected." ); } foreach ( $option as $key => $value ) { if ( !is_string( $key ) ) { throw new MWException( - "Invalid collated file path list key error. " . + "Invalid collated file path list key error. " . "'$key' given, string expected." ); } @@ -187,6 +197,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { break; // Single strings case 'group': + case 'position': case 'localBasePath': case 'remoteBasePath': $this->{$member} = (string) $option; @@ -197,39 +208,37 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { break; } } - // Make sure the remote base path is a complete valid url - $this->remoteBasePath = wfExpandUrl( $this->remoteBasePath ); + // Make sure the remote base path is a complete valid URL, + // but possibly protocol-relative to avoid cache pollution + $this->remoteBasePath = wfExpandUrl( $this->remoteBasePath, PROTO_RELATIVE ); } /** * Gets all scripts for a given context concatenated together. - * + * * @param $context ResourceLoaderContext: Context in which to generate script * @return String: JavaScript code for $context */ public function getScript( ResourceLoaderContext $context ) { - $files = array_merge( - $this->scripts, - self::tryForKey( $this->languageScripts, $context->getLanguage() ), - self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ) - ); - if ( $context->getDebug() ) { - $files = array_merge( $files, $this->debugScripts ); - if ( $this->debugRaw ) { - $script = ''; - foreach ( $files as $file ) { - $path = $this->getRemotePath( $file ); - $script .= "\n\t" . Xml::encodeJsCall( 'mediaWiki.loader.load', array( $path ) ); - } - return $script; - } - } + $files = $this->getScriptFiles( $context ); return $this->readScriptFiles( $files ); } + + public function getScriptURLsForDebug( ResourceLoaderContext $context ) { + $urls = array(); + foreach ( $this->getScriptFiles( $context ) as $file ) { + $urls[] = $this->getRemotePath( $file ); + } + return $urls; + } + + public function supportsURLLoading() { + return $this->debugRaw; + } /** * Gets loader script. - * + * * @return String: JavaScript code to be added to startup module */ public function getLoaderScript() { @@ -241,29 +250,19 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Gets all styles for a given context concatenated together. - * + * * @param $context ResourceLoaderContext: Context in which to generate styles * @return String: CSS code for $context */ public function getStyles( ResourceLoaderContext $context ) { - // Merge general styles and skin specific styles, retaining media type collation - $styles = $this->readStyleFiles( $this->styles, $this->getFlip( $context ) ); - $skinStyles = $this->readStyleFiles( - self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), + $styles = $this->readStyleFiles( + $this->getStyleFiles( $context ), $this->getFlip( $context ) ); - - foreach ( $skinStyles as $media => $style ) { - if ( isset( $styles[$media] ) ) { - $styles[$media] .= $style; - } else { - $styles[$media] = $style; - } - } // Collect referenced files $this->localFileRefs = array_unique( $this->localFileRefs ); // If the list has been modified since last time we cached it, update the cache - if ( $this->localFileRefs !== $this->getFileDependencies( $context->getSkin() ) ) { + if ( $this->localFileRefs !== $this->getFileDependencies( $context->getSkin() ) && !wfReadOnly() ) { $dbw = wfGetDB( DB_MASTER ); $dbw->replace( 'module_deps', array( array( 'md_module', 'md_skin' ) ), array( @@ -276,9 +275,20 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { return $styles; } + public function getStyleURLsForDebug( ResourceLoaderContext $context ) { + $urls = array(); + foreach ( $this->getStyleFiles( $context ) as $mediaType => $list ) { + $urls[$mediaType] = array(); + foreach ( $list as $file ) { + $urls[$mediaType][] = $this->getRemotePath( $file ); + } + } + return $urls; + } + /** * Gets list of message keys used by this module. - * + * * @return Array: List of message keys */ public function getMessages() { @@ -287,16 +297,23 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Gets the name of the group this module should be loaded in. - * + * * @return String: Group name */ public function getGroup() { return $this->group; } + /** + * @return string + */ + public function getPosition() { + return $this->position; + } + /** * Gets list of names of modules this module depends on. - * + * * @return Array: List of module names */ public function getDependencies() { @@ -305,14 +322,14 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Get the last modified timestamp of this module. - * - * Last modified timestamps are calculated from the highest last modified - * timestamp of this module's constituent files as well as the files it - * depends on. This function is context-sensitive, only performing - * calculations on files relevant to the given language, skin and debug + * + * Last modified timestamps are calculated from the highest last modified + * timestamp of this module's constituent files as well as the files it + * depends on. This function is context-sensitive, only performing + * calculations on files relevant to the given language, skin and debug * mode. - * - * @param $context ResourceLoaderContext: Context in which to calculate + * + * @param $context ResourceLoaderContext: Context in which to calculate * the modified time * @return Integer: UNIX timestamp * @see ResourceLoaderModule::getFileDependencies @@ -322,23 +339,23 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { return $this->modifiedTime[$context->getHash()]; } wfProfileIn( __METHOD__ ); - + $files = array(); - + // Flatten style files into $files $styles = self::collateFilePathListByOption( $this->styles, 'media', 'all' ); foreach ( $styles as $styleFiles ) { $files = array_merge( $files, $styleFiles ); } $skinFiles = self::tryForKey( - self::collateFilePathListByOption( $this->skinStyles, 'media', 'all' ), - $context->getSkin(), + self::collateFilePathListByOption( $this->skinStyles, 'media', 'all' ), + $context->getSkin(), 'default' ); foreach ( $skinFiles as $styleFiles ) { $files = array_merge( $files, $styleFiles ); } - + // Final merge, this should result in a master list of dependent files $files = array_merge( $files, @@ -351,39 +368,47 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $files = array_map( array( $this, 'getLocalPath' ), $files ); // File deps need to be treated separately because they're already prefixed $files = array_merge( $files, $this->getFileDependencies( $context->getSkin() ) ); - - // If a module is nothing but a list of dependencies, we need to avoid + + // If a module is nothing but a list of dependencies, we need to avoid // giving max() an empty array if ( count( $files ) === 0 ) { wfProfileOut( __METHOD__ ); return $this->modifiedTime[$context->getHash()] = 1; } - + wfProfileIn( __METHOD__.'-filemtime' ); - $filesMtime = max( array_map( array( __CLASS__, 'safeFilemtime' ), $files ) ); + $filesMtime = max( array_map( 'filemtime', $files ) ); wfProfileOut( __METHOD__.'-filemtime' ); - $this->modifiedTime[$context->getHash()] = max( - $filesMtime, + $this->modifiedTime[$context->getHash()] = max( + $filesMtime, $this->getMsgBlobMtime( $context->getLanguage() ) ); wfProfileOut( __METHOD__ ); return $this->modifiedTime[$context->getHash()]; } - /* Protected Members */ + /* Protected Methods */ + /** + * @param $path string + * @return string + */ protected function getLocalPath( $path ) { return "{$this->localBasePath}/$path"; } - + + /** + * @param $path string + * @return string + */ protected function getRemotePath( $path ) { return "{$this->remoteBasePath}/$path"; } /** * Collates file paths by option (where provided). - * - * @param $list Array: List of file paths in any combination of index/path + * + * @param $list Array: List of file paths in any combination of index/path * or path/options pairs * @param $option String: option name * @param $default Mixed: default value if the option isn't set @@ -398,7 +423,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $collatedFiles[$default] = array(); } $collatedFiles[$default][] = $value; - } else if ( is_array( $value ) ) { + } elseif ( is_array( $value ) ) { // File name as the key, options array as the value $optionValue = isset( $value[$option] ) ? $value[$option] : $default; if ( !isset( $collatedFiles[$optionValue] ) ) { @@ -412,43 +437,76 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Gets a list of element that match a key, optionally using a fallback key. - * + * * @param $list Array: List of lists to select from * @param $key String: Key to look for in $map * @param $fallback String: Key to look for in $list if $key doesn't exist - * @return Array: List of elements from $map which matched $key or $fallback, + * @return Array: List of elements from $map which matched $key or $fallback, * or an empty list in case of no match */ protected static function tryForKey( array $list, $key, $fallback = null ) { if ( isset( $list[$key] ) && is_array( $list[$key] ) ) { return $list[$key]; - } else if ( is_string( $fallback ) - && isset( $list[$fallback] ) - && is_array( $list[$fallback] ) ) + } elseif ( is_string( $fallback ) + && isset( $list[$fallback] ) + && is_array( $list[$fallback] ) ) { return $list[$fallback]; } return array(); } + /** + * Gets a list of file paths for all scripts in this module, in order of propper execution. + * + * @param $context ResourceLoaderContext: Context + * @return Array: List of file paths + */ + protected function getScriptFiles( ResourceLoaderContext $context ) { + $files = array_merge( + $this->scripts, + self::tryForKey( $this->languageScripts, $context->getLanguage() ), + self::tryForKey( $this->skinScripts, $context->getSkin(), 'default' ) + ); + if ( $context->getDebug() ) { + $files = array_merge( $files, $this->debugScripts ); + } + return $files; + } + + /** + * Gets a list of file paths for all styles in this module, in order of propper inclusion. + * + * @param $context ResourceLoaderContext: Context + * @return Array: List of file paths + */ + protected function getStyleFiles( ResourceLoaderContext $context ) { + return array_merge_recursive( + self::collateFilePathListByOption( $this->styles, 'media', 'all' ), + self::collateFilePathListByOption( + self::tryForKey( $this->skinStyles, $context->getSkin(), 'default' ), 'media', 'all' + ) + ); + } + /** * Gets the contents of a list of JavaScript files. - * + * * @param $scripts Array: List of file paths to scripts to read, remap and concetenate * @return String: Concatenated and remapped JavaScript data from $scripts */ protected function readScriptFiles( array $scripts ) { + global $wgResourceLoaderValidateStaticJS; if ( empty( $scripts ) ) { return ''; } - global $wgResourceLoaderValidateStaticJS; $js = ''; foreach ( array_unique( $scripts ) as $fileName ) { $localPath = $this->getLocalPath( $fileName ); - if ( !file_exists( $localPath ) ) { + $contents = file_get_contents( $localPath ); + if ( $contents === false ) { throw new MWException( __METHOD__.": script file not found: \"$localPath\"" ); } - $contents = file_get_contents( $localPath ); if ( $wgResourceLoaderValidateStaticJS ) { // Static files don't really need to be checked as often; unlike // on-wiki module they shouldn't change unexpectedly without @@ -462,16 +520,19 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Gets the contents of a list of CSS files. - * - * @param $styles Array: List of file paths to styles to read, remap and concetenate - * @return Array: List of concatenated and remapped CSS data from $styles, + * + * @param $styles Array: List of media type/list of file paths pairs, to read, remap and + * concetenate + * + * @param $flip bool + * + * @return Array: List of concatenated and remapped CSS data from $styles, * keyed by media type */ protected function readStyleFiles( array $styles, $flip ) { if ( empty( $styles ) ) { return array(); } - $styles = self::collateFilePathListByOption( $styles, 'media', 'all' ); foreach ( $styles as $media => $files ) { $uniqueFiles = array_unique( $files ); $styles[$media] = implode( @@ -488,49 +549,38 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { /** * Reads a style file. - * + * * This method can be used as a callback for array_map() - * - * @param $path String: File path of style file to read + * + * @param $path String: File path of script file to read + * @param $flip bool + * * @return String: CSS data in script file - * @throws MWException if the file doesn't exist */ - protected function readStyleFile( $path, $flip ) { + protected function readStyleFile( $path, $flip ) { $localPath = $this->getLocalPath( $path ); - if ( !file_exists( $localPath ) ) { + $style = file_get_contents( $localPath ); + if ( $style === false ) { throw new MWException( __METHOD__.": style file not found: \"$localPath\"" ); } - $style = file_get_contents( $localPath ); if ( $flip ) { $style = CSSJanus::transform( $style, true, false ); } - $dir = $this->getLocalPath( dirname( $path ) ); - $remoteDir = $this->getRemotePath( dirname( $path ) ); + $dirname = dirname( $path ); + if ( $dirname == '.' ) { + // If $path doesn't have a directory component, don't prepend a dot + $dirname = ''; + } + $dir = $this->getLocalPath( $dirname ); + $remoteDir = $this->getRemotePath( $dirname ); // Get and register local file references - $this->localFileRefs = array_merge( - $this->localFileRefs, + $this->localFileRefs = array_merge( + $this->localFileRefs, CSSMin::getLocalFileReferences( $style, $dir ) ); return CSSMin::remap( $style, $dir, $remoteDir, true ); } - - /** - * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist - * but returns 1 instead. - * @param $filename string File name - * @return int UNIX timestamp, or 1 if the file doesn't exist - */ - protected static function safeFilemtime( $filename ) { - if ( file_exists( $filename ) ) { - return filemtime( $filename ); - } else { - // We only ever map this function on an array if we're gonna call max() after, - // so return our standard minimum timestamps here. This is 1, not 0, because - // wfTimestamp(0) == NOW - return 1; - } - } /** * Get whether CSS for this module should be flipped diff --git a/includes/resourceloader/ResourceLoaderFilePageModule.php b/includes/resourceloader/ResourceLoaderFilePageModule.php new file mode 100644 index 00000000..fc9aef1b --- /dev/null +++ b/includes/resourceloader/ResourceLoaderFilePageModule.php @@ -0,0 +1,11 @@ + array( 'type' => 'style' ), + ); + } +} diff --git a/includes/resourceloader/ResourceLoaderModule.php b/includes/resourceloader/ResourceLoaderModule.php index 77d230c9..ae1be5af 100644 --- a/includes/resourceloader/ResourceLoaderModule.php +++ b/includes/resourceloader/ResourceLoaderModule.php @@ -24,6 +24,34 @@ * Abstraction for resource loader modules, with name registration and maxage functionality. */ abstract class ResourceLoaderModule { + + # Type of resource + const TYPE_SCRIPTS = 'scripts'; + const TYPE_STYLES = 'styles'; + const TYPE_MESSAGES = 'messages'; + const TYPE_COMBINED = 'combined'; + + # sitewide core module like a skin file or jQuery component + const ORIGIN_CORE_SITEWIDE = 1; + + # per-user module generated by the software + const ORIGIN_CORE_INDIVIDUAL = 2; + + # sitewide module generated from user-editable files, like MediaWiki:Common.js, or + # modules accessible to multiple users, such as those generated by the Gadgets extension. + const ORIGIN_USER_SITEWIDE = 3; + + # per-user module generated from user-editable files, like User:Me/vector.js + const ORIGIN_USER_INDIVIDUAL = 4; + + # an access constant; make sure this is kept as the largest number in this group + const ORIGIN_ALL = 10; + + # script and style modules form a hierarchy of trustworthiness, with core modules like + # skins and jQuery as most trustworthy, and user scripts as least trustworthy. We can + # limit the types of scripts and styles we allow to load on, say, sensitive special + # pages like Special:UserLogin and Special:Preferences + protected $origin = self::ORIGIN_CORE_SITEWIDE; /* Protected Members */ @@ -57,10 +85,34 @@ abstract class ResourceLoaderModule { } /** - * Get whether CSS for this module should be flipped + * Get this module's origin. This is set when the module is registered + * with ResourceLoader::register() + * + * @return Int ResourceLoaderModule class constant, the subclass default + * if not set manuall + */ + public function getOrigin() { + return $this->origin; + } + + /** + * Set this module's origin. This is called by ResourceLodaer::register() + * when registering the module. Other code should not call this. + * + * @param $origin Int origin + */ + public function setOrigin( $origin ) { + $this->origin = $origin; + } + + /** + * @param $context ResourceLoaderContext + * @return bool */ public function getFlip( $context ) { - return $context->getDirection() === 'rtl'; + global $wgContLang; + + return $wgContLang->getDir() !== $context->getDirection(); } /** @@ -74,6 +126,44 @@ abstract class ResourceLoaderModule { // Stub, override expected return ''; } + + /** + * Get the URL or URLs to load for this module's JS in debug mode. + * The default behavior is to return a load.php?only=scripts URL for + * the module, but file-based modules will want to override this to + * load the files directly. + * + * This function is called only when 1) we're in debug mode, 2) there + * is no only= parameter and 3) supportsURLLoading() returns true. + * #2 is important to prevent an infinite loop, therefore this function + * MUST return either an only= URL or a non-load.php URL. + * + * @param $context ResourceLoaderContext: Context object + * @return Array of URLs + */ + public function getScriptURLsForDebug( ResourceLoaderContext $context ) { + global $wgLoadScript; // TODO factor out to ResourceLoader static method and deduplicate from makeResourceLoaderLink() + $query = array( + 'modules' => $this->getName(), + 'only' => 'scripts', + 'skin' => $context->getSkin(), + 'user' => $context->getUser(), + 'debug' => 'true', + 'version' => $context->getVersion() + ); + ksort( $query ); + return array( wfAppendQuery( $wgLoadScript, $query ) . '&*' ); + } + + /** + * Whether this module supports URL loading. If this function returns false, + * getScript() will be used even in cases (debug mode, no only param) where + * getScriptURLsForDebug() would normally be used instead. + * @return bool + */ + public function supportsURLLoading() { + return true; + } /** * Get all CSS for this module for a given skin. @@ -83,7 +173,30 @@ abstract class ResourceLoaderModule { */ public function getStyles( ResourceLoaderContext $context ) { // Stub, override expected - return ''; + return array(); + } + + /** + * Get the URL or URLs to load for this module's CSS in debug mode. + * The default behavior is to return a load.php?only=styles URL for + * the module, but file-based modules will want to override this to + * load the files directly. See also getScriptURLsForDebug() + * + * @param $context ResourceLoaderContext: Context object + * @return Array: array( mediaType => array( URL1, URL2, ... ), ... ) + */ + public function getStyleURLsForDebug( ResourceLoaderContext $context ) { + global $wgLoadScript; // TODO factor out to ResourceLoader static method and deduplicate from makeResourceLoaderLink() + $query = array( + 'modules' => $this->getName(), + 'only' => 'styles', + 'skin' => $context->getSkin(), + 'user' => $context->getUser(), + 'debug' => 'true', + 'version' => $context->getVersion() + ); + ksort( $query ); + return array( 'all' => array( wfAppendQuery( $wgLoadScript, $query ) . '&*' ) ); } /** @@ -107,6 +220,17 @@ abstract class ResourceLoaderModule { // Stub, override expected return null; } + + /** + * Where on the HTML page should this module's JS be loaded? + * 'top': in the + * 'bottom': at the bottom of the + * + * @return string + */ + public function getPosition() { + return 'bottom'; + } /** * Get the loader JS for this module, if set. @@ -179,7 +303,7 @@ abstract class ResourceLoaderModule { * Get the last modification timestamp of the message blob for this * module in a given language. * @param $lang String: Language code - * @return Integer: UNIX timestamp, or 0 if no blob found + * @return Integer: UNIX timestamp, or 0 if the module doesn't have messages */ public function getMsgBlobMtime( $lang ) { if ( !isset( $this->msgBlobMtime[$lang] ) ) { @@ -192,7 +316,12 @@ abstract class ResourceLoaderModule { 'mr_lang' => $lang ), __METHOD__ ); - $this->msgBlobMtime[$lang] = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0; + // If no blob was found, but the module does have messages, that means we need + // to regenerate it. Return NOW + if ( $msgBlobMtime === false ) { + $msgBlobMtime = wfTimestampNow(); + } + $this->msgBlobMtime[$lang] = wfTimestamp( TS_UNIX, $msgBlobMtime ); } return $this->msgBlobMtime[$lang]; } @@ -236,4 +365,54 @@ abstract class ResourceLoaderModule { public function isKnownEmpty( ResourceLoaderContext $context ) { return false; } + + + /** @var JSParser lazy-initialized; use self::javaScriptParser() */ + private static $jsParser; + private static $parseCacheVersion = 1; + + /** + * Validate a given script file; if valid returns the original source. + * If invalid, returns replacement JS source that throws an exception. + * + * @param string $fileName + * @param string $contents + * @return string JS with the original, or a replacement error + */ + protected function validateScriptFile( $fileName, $contents ) { + global $wgResourceLoaderValidateJS; + if ( $wgResourceLoaderValidateJS ) { + // Try for cache hit + // Use CACHE_ANYTHING since filtering is very slow compared to DB queries + $key = wfMemcKey( 'resourceloader', 'jsparse', self::$parseCacheVersion, md5( $contents ) ); + $cache = wfGetCache( CACHE_ANYTHING ); + $cacheEntry = $cache->get( $key ); + if ( is_string( $cacheEntry ) ) { + return $cacheEntry; + } + + $parser = self::javaScriptParser(); + try { + $parser->parse( $contents, $fileName, 1 ); + $result = $contents; + } catch (Exception $e) { + // We'll save this to cache to avoid having to validate broken JS over and over... + $err = $e->getMessage(); + $result = "throw new Error(" . Xml::encodeJsVar("JavaScript parse error: $err") . ");"; + } + + $cache->set( $key, $result ); + return $result; + } else { + return $contents; + } + } + + protected static function javaScriptParser() { + if ( !self::$jsParser ) { + self::$jsParser = new JSParser(); + } + return self::$jsParser; + } + } diff --git a/includes/resourceloader/ResourceLoaderNoscriptModule.php b/includes/resourceloader/ResourceLoaderNoscriptModule.php new file mode 100644 index 00000000..28f629a2 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderNoscriptModule.php @@ -0,0 +1,52 @@ + array( 'type' => 'style' ) ); + } + + /* Methods */ + + /** + * Gets group name + * + * @return String: Name of group + */ + public function getGroup() { + return 'noscript'; + } +} diff --git a/includes/resourceloader/ResourceLoaderSiteModule.php b/includes/resourceloader/ResourceLoaderSiteModule.php index 977d16bb..2527a0a3 100644 --- a/includes/resourceloader/ResourceLoaderSiteModule.php +++ b/includes/resourceloader/ResourceLoaderSiteModule.php @@ -29,7 +29,9 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule { /** * Gets list of pages used by this module - * + * + * @param $context ResourceLoaderContext + * * @return Array: List of pages */ protected function getPages( ResourceLoaderContext $context ) { diff --git a/includes/resourceloader/ResourceLoaderStartUpModule.php b/includes/resourceloader/ResourceLoaderStartUpModule.php index f3117378..43f1dbd2 100644 --- a/includes/resourceloader/ResourceLoaderStartUpModule.php +++ b/includes/resourceloader/ResourceLoaderStartUpModule.php @@ -21,20 +21,24 @@ */ class ResourceLoaderStartUpModule extends ResourceLoaderModule { - + /* Protected Members */ protected $modifiedTime = array(); /* Protected Methods */ - + + /** + * @param $context ResourceLoaderContext + * @return array + */ protected function getConfig( $context ) { - global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension, - $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, - $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion, - $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest, + global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension, + $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, + $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion, + $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest, $wgSitename, $wgFileExtensions, $wgExtensionAssetsPath, - $wgResourceLoaderMaxQueryLength; + $wgCookiePrefix, $wgResourceLoaderMaxQueryLength, $wgLegacyJavaScriptGlobals; // Pre-process information $separatorTransTable = $wgContLang->separatorTransformTable(); @@ -50,7 +54,21 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { implode( "\t", $digitTransTable ), ); $mainPage = Title::newMainPage(); - + + /** + * Namespace related preparation + * - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces. + * - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive. + */ + $namespaceIds = $wgContLang->getNamespaceIds(); + $caseSensitiveNamespaces = array(); + foreach( MWNamespace::getCanonicalNamespaces() as $index => $name ) { + $namespaceIds[$wgContLang->lc( $name )] = $index; + if ( !MWNamespace::isCapitalized( $index ) ) { + $caseSensitiveNamespaces[] = $index; + } + } + // Build list of variables $vars = array( 'wgLoadScript' => $wgLoadScript, @@ -70,29 +88,37 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgVersion' => $wgVersion, 'wgEnableAPI' => $wgEnableAPI, 'wgEnableWriteAPI' => $wgEnableWriteAPI, + 'wgDefaultDateFormat' => $wgContLang->getDefaultDateFormat(), + 'wgMonthNames' => $wgContLang->getMonthNamesArray(), + 'wgMonthNamesShort' => $wgContLang->getMonthAbbreviationsArray(), 'wgSeparatorTransformTable' => $compactSeparatorTransTable, 'wgDigitTransformTable' => $compactDigitTransTable, 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null, 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(), - 'wgNamespaceIds' => $wgContLang->getNamespaceIds(), + 'wgNamespaceIds' => $namespaceIds, 'wgSiteName' => $wgSitename, 'wgFileExtensions' => array_values( $wgFileExtensions ), 'wgDBname' => $wgDBname, + // This sucks, it is only needed on Special:Upload, but I could + // not find a way to add vars only for a certain module + 'wgFileCanRotate' => BitmapHandler::canRotate(), + 'wgAvailableSkins' => Skin::getSkinNames(), 'wgExtensionAssetsPath' => $wgExtensionAssetsPath, + // MediaWiki sets cookies to have this prefix by default + 'wgCookiePrefix' => $wgCookiePrefix, 'wgResourceLoaderMaxQueryLength' => $wgResourceLoaderMaxQueryLength, + 'wgLegacyJavaScriptGlobals' => $wgLegacyJavaScriptGlobals, + 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces, ); - if ( $wgContLang->hasVariants() ) { - $vars['wgUserVariant'] = $wgContLang->getPreferredVariant(); - } if ( $wgUseAjax && $wgEnableMWSuggest ) { $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate(); } - + wfRunHooks( 'ResourceLoaderGetConfigVars', array( &$vars ) ); - + return $vars; } - + /** * Gets registration code for all modules * @@ -102,7 +128,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { public static function getModuleRegistrations( ResourceLoaderContext $context ) { global $wgCacheEpoch; wfProfileIn( __METHOD__ ); - + $out = ''; $registrations = array(); $resourceLoader = $context->getResourceLoader(); @@ -113,8 +139,8 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { if ( $loader !== false ) { $deps = $module->getDependencies(); $group = $module->getGroup(); - $version = wfTimestamp( TS_ISO_8601_BASIC, - round( $module->getModifiedTime( $context ), -2 ) ); + $version = wfTimestamp( TS_ISO_8601_BASIC, + $module->getModifiedTime( $context ) ); $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $loader ); } // Automatically register module @@ -123,19 +149,19 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { // seem to do that, and custom implementations might forget. Coerce it to TS_UNIX $moduleMtime = wfTimestamp( TS_UNIX, $module->getModifiedTime( $context ) ); $mtime = max( $moduleMtime, wfTimestamp( TS_UNIX, $wgCacheEpoch ) ); - // Modules without dependencies or a group pass two arguments (name, timestamp) to - // mediaWiki.loader.register() + // Modules without dependencies or a group pass two arguments (name, timestamp) to + // mw.loader.register() if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) { $registrations[] = array( $name, $mtime ); } - // Modules with dependencies but no group pass three arguments - // (name, timestamp, dependencies) to mediaWiki.loader.register() - else if ( $module->getGroup() === null ) { + // Modules with dependencies but no group pass three arguments + // (name, timestamp, dependencies) to mw.loader.register() + elseif ( $module->getGroup() === null ) { $registrations[] = array( $name, $mtime, $module->getDependencies() ); } - // Modules with dependencies pass four arguments (name, timestamp, dependencies, group) - // to mediaWiki.loader.register() + // Modules with dependencies pass four arguments (name, timestamp, dependencies, group) + // to mw.loader.register() else { $registrations[] = array( $name, $mtime, $module->getDependencies(), $module->getGroup() ); @@ -143,52 +169,74 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { } } $out .= ResourceLoader::makeLoaderRegisterScript( $registrations ); - + wfProfileOut( __METHOD__ ); return $out; } /* Methods */ + /** + * @param $context ResourceLoaderContext + * @return string + */ public function getScript( ResourceLoaderContext $context ) { - global $IP, $wgLoadScript; + global $IP, $wgLoadScript, $wgLegacyJavaScriptGlobals; $out = file_get_contents( "$IP/resources/startup.js" ); if ( $context->getOnly() === 'scripts' ) { - // Build load query for jquery and mediawiki modules + + // The core modules: + $modules = array( 'jquery', 'mediawiki' ); + wfRunHooks( 'ResourceLoaderGetStartupModules', array( &$modules ) ); + + // Get the latest version + $version = 0; + foreach ( $modules as $moduleName ) { + $version = max( $version, + $context->getResourceLoader()->getModule( $moduleName )->getModifiedTime( $context ) + ); + } + // Build load query for StartupModules $query = array( - 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ), + 'modules' => ResourceLoader::makePackedModulesString( $modules ), 'only' => 'scripts', 'lang' => $context->getLanguage(), 'skin' => $context->getSkin(), 'debug' => $context->getDebug() ? 'true' : 'false', - 'version' => wfTimestamp( TS_ISO_8601_BASIC, round( max( - $context->getResourceLoader()->getModule( 'jquery' )->getModifiedTime( $context ), - $context->getResourceLoader()->getModule( 'mediawiki' )->getModifiedTime( $context ) - ), -2 ) ) + 'version' => wfTimestamp( TS_ISO_8601_BASIC, $version ) ); // Ensure uniform query order ksort( $query ); - + // Startup function $configuration = $this->getConfig( $context ); $registrations = self::getModuleRegistrations( $context ); - $out .= "var startUp = function() {\n" . - "\t$registrations\n" . - "\t" . Xml::encodeJsCall( 'mediaWiki.config.set', array( $configuration ) ) . + $out .= "var startUp = function() {\n" . + "\tmw.config = new " . Xml::encodeJsCall( 'mw.Map', array( $wgLegacyJavaScriptGlobals ) ) . "\n" . + "\t$registrations\n" . + "\t" . Xml::encodeJsCall( 'mw.config.set', array( $configuration ) ) . "};\n"; - + // Conditional script injection $scriptTag = Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ); - $out .= "if ( isCompatible() ) {\n" . - "\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) . - "}\n" . + $out .= "if ( isCompatible() ) {\n" . + "\t" . Xml::encodeJsCall( 'document.write', array( $scriptTag ) ) . + "}\n" . "delete isCompatible;"; } return $out; } + public function supportsURLLoading() { + return false; + } + + /** + * @param $context ResourceLoaderContext + * @return array|mixed + */ public function getModifiedTime( ResourceLoaderContext $context ) { global $IP, $wgCacheEpoch; @@ -204,7 +252,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" ); // ATTENTION!: Because of the line above, this is not going to cause - // infinite recursion - think carefully before making changes to this + // infinite recursion - think carefully before making changes to this // code! $time = wfTimestamp( TS_UNIX, $wgCacheEpoch ); foreach ( $loader->getModuleNames() as $name ) { @@ -214,14 +262,11 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { return $this->modifiedTime[$hash] = $time; } - public function getFlip( $context ) { - global $wgContLang; - - return $wgContLang->getDir() !== $context->getDirection(); - } - /* Methods */ - + + /** + * @return string + */ public function getGroup() { return 'startup'; } diff --git a/includes/resourceloader/ResourceLoaderUserGroupsModule.php b/includes/resourceloader/ResourceLoaderUserGroupsModule.php new file mode 100644 index 00000000..733dfa04 --- /dev/null +++ b/includes/resourceloader/ResourceLoaderUserGroupsModule.php @@ -0,0 +1,59 @@ +getUser() ) { + $user = User::newFromName( $context->getUser() ); + if ( $user instanceof User ) { + $pages = array(); + foreach( $user->getEffectiveGroups() as $group ) { + if ( in_array( $group, array( '*', 'user' ) ) ) { + continue; + } + $pages["MediaWiki:Group-$group.js"] = array( 'type' => 'script' ); + $pages["MediaWiki:Group-$group.css"] = array( 'type' => 'style' ); + } + return $pages; + } + } + return array(); + } + + /* Methods */ + + /** + * @return string + */ + public function getGroup() { + return 'user'; + } +} diff --git a/includes/resourceloader/ResourceLoaderUserModule.php b/includes/resourceloader/ResourceLoaderUserModule.php index c7186653..892e8462 100644 --- a/includes/resourceloader/ResourceLoaderUserModule.php +++ b/includes/resourceloader/ResourceLoaderUserModule.php @@ -26,7 +26,12 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule { /* Protected Methods */ + protected $origin = self::ORIGIN_USER_INDIVIDUAL; + /** + * @param $context ResourceLoaderContext + * @return array + */ protected function getPages( ResourceLoaderContext $context ) { if ( $context->getUser() ) { $username = $context->getUser(); @@ -41,9 +46,12 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule { } return array(); } - + /* Methods */ - + + /** + * @return string + */ public function getGroup() { return 'user'; } diff --git a/includes/resourceloader/ResourceLoaderUserOptionsModule.php b/includes/resourceloader/ResourceLoaderUserOptionsModule.php index 61c76940..8f28eb8d 100644 --- a/includes/resourceloader/ResourceLoaderUserOptionsModule.php +++ b/includes/resourceloader/ResourceLoaderUserOptionsModule.php @@ -29,8 +29,14 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { protected $modifiedTime = array(); + protected $origin = self::ORIGIN_CORE_INDIVIDUAL; + /* Methods */ + /** + * @param $context ResourceLoaderContext + * @return array|int|Mixed + */ public function getModifiedTime( ResourceLoaderContext $context ) { $hash = $context->getHash(); if ( isset( $this->modifiedTime[$hash] ) ) { @@ -64,11 +70,19 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { } } + /** + * @param $context ResourceLoaderContext + * @return string + */ public function getScript( ResourceLoaderContext $context ) { - return Xml::encodeJsCall( 'mediaWiki.user.options.set', + return Xml::encodeJsCall( 'mw.user.options.set', array( $this->contextUserOptions( $context ) ) ); } + /** + * @param $context ResourceLoaderContext + * @return array + */ public function getStyles( ResourceLoaderContext $context ) { global $wgAllowUserCssPrefs; @@ -80,6 +94,10 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { if ( $options['underline'] < 2 ) { $rules[] = "a { text-decoration: " . ( $options['underline'] ? 'underline' : 'none' ) . "; }"; + } else { + # The scripts of these languages are very hard to read with underlines + $rules[] = 'a:lang(ar), a:lang(ckb), a:lang(fa),a:lang(kk-arab), ' . + 'a:lang(mzn), a:lang(ps), a:lang(ur) { text-decoration: none; }'; } if ( $options['highlightbroken'] ) { $rules[] = "a.new, #quickbar a.new { color: #ba0000; }\n"; @@ -109,12 +127,9 @@ class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { return array(); } - public function getFlip( $context ) { - global $wgContLang; - - return $wgContLang->getDir() !== $context->getDirection(); - } - + /** + * @return string + */ public function getGroup() { return 'private'; } diff --git a/includes/resourceloader/ResourceLoaderUserTokensModule.php b/includes/resourceloader/ResourceLoaderUserTokensModule.php new file mode 100644 index 00000000..9403534c --- /dev/null +++ b/includes/resourceloader/ResourceLoaderUserTokensModule.php @@ -0,0 +1,63 @@ + $wgUser->edittoken(), + 'watchToken' => ApiQueryInfo::getWatchToken(null, null), + ); + } + + /** + * @param $context ResourceLoaderContext + * @return string + */ + public function getScript( ResourceLoaderContext $context ) { + return Xml::encodeJsCall( 'mw.user.tokens.set', + array( $this->contextUserTokens( $context ) ) ); + } + + /** + * @return string + */ + public function getGroup() { + return 'private'; + } +} diff --git a/includes/resourceloader/ResourceLoaderWikiModule.php b/includes/resourceloader/ResourceLoaderWikiModule.php index 93e66eb0..bad61cb9 100644 --- a/includes/resourceloader/ResourceLoaderWikiModule.php +++ b/includes/resourceloader/ResourceLoaderWikiModule.php @@ -32,6 +32,9 @@ defined( 'MEDIAWIKI' ) || die( 1 ); abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { /* Protected Members */ + + # Origin is user-supplied code + protected $origin = self::ORIGIN_USER_SITEWIDE; // In-object cache for title mtimes protected $titleMtimes = array(); @@ -41,11 +44,15 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { abstract protected function getPages( ResourceLoaderContext $context ); /* Protected Methods */ - + + /** + * @param $title Title + * @return null|string + */ protected function getContent( $title ) { if ( $title->getNamespace() === NS_MEDIAWIKI ) { - $dbkey = $title->getDBkey(); - return wfEmptyMsg( $dbkey ) ? '' : wfMsgExt( $dbkey, 'content' ); + $message = wfMessage( $title->getDBkey() )->inContentLanguage(); + return $message->exists() ? $message->plain() : ''; } if ( !$title->isCssJsSubpage() ) { return null; @@ -59,6 +66,10 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { /* Methods */ + /** + * @param $context ResourceLoaderContext + * @return string + */ public function getScript( ResourceLoaderContext $context ) { $scripts = ''; foreach ( $this->getPages( $context ) as $titleText => $options ) { @@ -66,11 +77,12 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { continue; } $title = Title::newFromText( $titleText ); - if ( !$title ) { + if ( !$title || $title->isRedirect() ) { continue; } $script = $this->getContent( $title ); if ( strval( $script ) !== '' ) { + $script = $this->validateScriptFile( $titleText, $script ); if ( strpos( $titleText, '*/' ) === false ) { $scripts .= "/* $titleText */\n"; } @@ -80,6 +92,10 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { return $scripts; } + /** + * @param $context ResourceLoaderContext + * @return array + */ public function getStyles( ResourceLoaderContext $context ) { global $wgScriptPath; @@ -89,7 +105,7 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { continue; } $title = Title::newFromText( $titleText ); - if ( !$title ) { + if ( !$title || $title->isRedirect() ) { continue; } $media = isset( $options['media'] ) ? $options['media'] : 'all'; @@ -112,6 +128,10 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { return $styles; } + /** + * @param $context ResourceLoaderContext + * @return int|mixed + */ public function getModifiedTime( ResourceLoaderContext $context ) { $modifiedTime = 1; // wfTimestamp() interprets 0 as "now" $mtimes = $this->getTitleMtimes( $context ); @@ -120,21 +140,15 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { } return $modifiedTime; } - - public function isKnownEmpty( ResourceLoaderContext $context ) { - return count( $this->getTitleMtimes( $context ) ) == 0; - } - + /** * @param $context ResourceLoaderContext * @return bool */ - public function getFlip( $context ) { - global $wgContLang; - - return $wgContLang->getDir() !== $context->getDirection(); + public function isKnownEmpty( ResourceLoaderContext $context ) { + return count( $this->getTitleMtimes( $context ) ) == 0; } - + /** * Get the modification times of all titles that would be loaded for * a given context. -- cgit v1.2.3-54-g00ecf