X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FResourceLoaderModule.php;h=bda0795ad7b205c41a44200a874da170d49b8019;hb=acb2d77b4c761b35e8cdd12cafa4968ba0f0daf7;hp=cbe827eddc7b6704e00ed572cc29f7fa661fb923;hpb=b9137a9cf2ed562f7fc11c84e7f2ae4c9e33e9a2;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/ResourceLoaderModule.php b/includes/ResourceLoaderModule.php index cbe827eddc..bda0795ad7 100644 --- a/includes/ResourceLoaderModule.php +++ b/includes/ResourceLoaderModule.php @@ -20,13 +20,21 @@ * @author Roan Kattouw */ +defined( 'MEDIAWIKI' ) || die( 1 ); + /** * Abstraction for resource loader modules, with name registration and maxage functionality. */ abstract class ResourceLoaderModule { + /* Protected Members */ protected $name = null; + + // In-object cache for file dependencies + protected $fileDeps = array(); + // In-object cache for message blob mtime + protected $msgBlobMtime = array(); /* Methods */ @@ -50,30 +58,6 @@ abstract class ResourceLoaderModule { $this->name = $name; } - /** - * The maximum number of seconds to cache this module for in the - * client-side (browser) cache. Override this only if you have a good - * reason not to use $wgResourceLoaderClientMaxage. - * - * @return Integer: cache maxage in seconds - */ - public function getClientMaxage() { - global $wgResourceLoaderClientMaxage; - return $wgResourceLoaderClientMaxage; - } - - /** - * The maximum number of seconds to cache this module for in the - * server-side (Squid / proxy) cache. Override this only if you have a - * good reason not to use $wgResourceLoaderServerMaxage. - * - * @return Integer: cache maxage in seconds - */ - public function getServerMaxage() { - global $wgResourceLoaderServerMaxage; - return $wgResourceLoaderServerMaxage; - } - /** * Get whether CSS for this module should be flipped */ @@ -115,6 +99,16 @@ abstract class ResourceLoaderModule { // Stub, override expected return array(); } + + /** + * Get the group this module is in. + * + * @return string of group name + */ + public function getGroup() { + // Stub, override expected + return null; + } /** * Get the loader JS for this module, if set. @@ -123,7 +117,7 @@ abstract class ResourceLoaderModule { */ public function getLoaderScript() { // Stub, override expected - return ''; + return false; } /** @@ -145,7 +139,72 @@ abstract class ResourceLoaderModule { // Stub, override expected return array(); } + + /** + * Get the files this module depends on indirectly for a given skin. + * Currently these are only image files referenced by the module's CSS. + * + * @param $skin String: skin name + * @return array of files + */ + public function getFileDependencies( $skin ) { + // Try in-object cache first + if ( isset( $this->fileDeps[$skin] ) ) { + return $this->fileDeps[$skin]; + } + $dbr = wfGetDB( DB_SLAVE ); + $deps = $dbr->selectField( 'module_deps', 'md_deps', array( + 'md_module' => $this->getName(), + 'md_skin' => $skin, + ), __METHOD__ + ); + if ( !is_null( $deps ) ) { + return $this->fileDeps[$skin] = (array) FormatJson::decode( $deps, true ); + } + return $this->fileDeps[$skin] = array(); + } + + /** + * Set preloaded file dependency information. Used so we can load this + * information for all modules at once. + * @param $skin string Skin name + * @param $deps array Array of file names + */ + public function setFileDependencies( $skin, $deps ) { + $this->fileDeps[$skin] = $deps; + } + + /** + * Get the last modification timestamp of the message blob for this + * module in a given language. + * @param $lang string Language code + * @return int UNIX timestamp, or 0 if no blob found + */ + public function getMsgBlobMtime( $lang ) { + if ( !count( $this->getMessages() ) ) + return 0; + + $dbr = wfGetDB( DB_SLAVE ); + $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array( + 'mr_resource' => $this->getName(), + 'mr_lang' => $lang + ), __METHOD__ + ); + $this->msgBlobMtime[$lang] = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0; + return $this->msgBlobMtime[$lang]; + } + + /** + * Set a preloaded message blob last modification timestamp. Used so we + * can load this information for all modules at once. + * @param $lang string Language code + * @param $mtime int UNIX timestamp or 0 if there is no such blob + */ + public function setMsgBlobMtime( $lang, $mtime ) { + $this->msgBlobMtime[$lang] = $mtime; + } + /* Abstract Methods */ /** @@ -158,7 +217,10 @@ abstract class ResourceLoaderModule { * @param $context ResourceLoaderContext object * @return int UNIX timestamp */ - public abstract function getModifiedTime( ResourceLoaderContext $context ); + public function getModifiedTime( ResourceLoaderContext $context ) { + // 0 would mean now + return 1; + } } /** @@ -170,6 +232,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected $scripts = array(); protected $styles = array(); protected $messages = array(); + protected $group; protected $dependencies = array(); protected $debugScripts = array(); protected $languageScripts = array(); @@ -194,7 +257,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * array( * // Required module options (mutually exclusive) * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ), - * + * * // Optional module options * 'languageScripts' => array( * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... ) @@ -214,6 +277,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * ... * ), * 'messages' => array( 'message1', 'message2' ... ), + * 'group' => 'stuff', * ) */ public function __construct( $options = array() ) { @@ -228,6 +292,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { case 'messages': $this->messages = (array)$value; break; + case 'group': + $this->group = (string)$value; + break; case 'dependencies': $this->dependencies = (array)$value; break; @@ -277,7 +344,16 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { public function addMessages( $messages ) { $this->messages = array_merge( $this->messages, (array)$messages ); } - + + /** + * Sets the group of this module. + * + * @param $group string group name + */ + public function setGroup( $group ) { + $this->group = $group; + } + /** * Add dependencies. Dependency information is taken into account when * loading a module on the client side. When adding a module on the @@ -404,7 +480,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { // Only store if modified if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) { $encFiles = FormatJson::encode( $files ); - $dbw = wfGetDb( DB_MASTER ); + $dbw = wfGetDB( DB_MASTER ); $dbw->replace( 'module_deps', array( array( 'md_module', 'md_skin' ) ), array( 'md_module' => $this->getName(), @@ -427,6 +503,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { return $this->messages; } + public function getGroup() { + return $this->group; + } + public function getDependencies() { return $this->dependencies; } @@ -454,6 +534,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { if ( isset( $this->modifiedTime[$context->getHash()] ) ) { return $this->modifiedTime[$context->getHash()]; } + wfProfileIn( __METHOD__ ); // Sort of nasty way we can get a flat list of files depended on by all styles $styles = array(); @@ -479,19 +560,11 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $this->getFileDependencies( $context->getSkin() ) ); + wfProfileIn( __METHOD__.'-filemtime' ); $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__, 'remapFilename' ), $files ) ) ); - - // Get the mtime of the message blob - // TODO: This timestamp is queried a lot and queried separately for each module. Maybe it should be put in memcached? - $dbr = wfGetDb( DB_SLAVE ); - $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array( - 'mr_resource' => $this->getName(), - 'mr_lang' => $context->getLanguage() - ), __METHOD__ - ); - $msgBlobMtime = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0; - - $this->modifiedTime[$context->getHash()] = max( $filesMtime, $msgBlobMtime ); + wfProfileOut( __METHOD__.'-filemtime' ); + $this->modifiedTime[$context->getHash()] = max( $filesMtime, $this->getMsgBlobMtime( $context->getLanguage() ) ); + wfProfileOut( __METHOD__ ); return $this->modifiedTime[$context->getHash()]; } @@ -579,43 +652,6 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { return $retval; } - /** - * Get the files this module depends on indirectly for a given skin. - * Currently these are only image files referenced by the module's CSS. - * - * @param $skin String: skin name - * @return array of files - */ - protected function getFileDependencies( $skin ) { - // Try in-object cache first - if ( isset( $this->fileDeps[$skin] ) ) { - return $this->fileDeps[$skin]; - } - - // Now try memcached - global $wgMemc; - - $key = wfMemcKey( 'resourceloader', 'module_deps', $this->getName(), $skin ); - $deps = $wgMemc->get( $key ); - - if ( !$deps ) { - $dbr = wfGetDb( DB_SLAVE ); - $deps = $dbr->selectField( 'module_deps', 'md_deps', array( - 'md_module' => $this->getName(), - 'md_skin' => $skin, - ), __METHOD__ - ); - if ( !$deps ) { - $deps = '[]'; // Empty array so we can do negative caching - } - $wgMemc->set( $key, $deps ); - } - - $this->fileDeps = FormatJson::decode( $deps, true ); - - return $this->fileDeps; - } - /** * Get the contents of a set of files and concatenate them, with * newlines in between. Each file is used only once. @@ -624,7 +660,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * @return String: concatenated contents of $files */ protected static function concatScripts( $files ) { - return implode( "\n", array_map( 'file_get_contents', array_map( array( __CLASS__, 'remapFilename' ), array_unique( (array) $files ) ) ) ); + return implode( "\n", + array_map( + 'file_get_contents', + array_map( + array( __CLASS__, 'remapFilename' ), + array_unique( (array) $files ) ) ) ); } protected static function organizeFilesByOption( $files, $option, $default ) { @@ -659,7 +700,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $styles = self::organizeFilesByOption( $styles, 'media', 'all' ); foreach ( $styles as $media => $files ) { $styles[$media] = - implode( "\n", array_map( array( __CLASS__, 'remapStyle' ), array_unique( (array) $files ) ) ); + implode( "\n", + array_map( + array( __CLASS__, 'remapStyle' ), + array_unique( (array) $files ) ) ); } return $styles; } @@ -684,8 +728,13 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * @return string Remapped CSS */ protected static function remapStyle( $file ) { - global $wgUseDataURLs; - return CSSMin::remap( file_get_contents( self::remapFilename( $file ) ), dirname( $file ), $wgUseDataURLs ); + global $wgScriptPath; + return CSSMin::remap( + file_get_contents( self::remapFilename( $file ) ), + dirname( $file ), + $wgScriptPath . '/' . dirname( $file ), + true + ); } } @@ -758,20 +807,25 @@ abstract class ResourceLoaderWikiModule extends ResourceLoaderModule { if ( isset( $this->modifiedTime[$hash] ) ) { return $this->modifiedTime[$hash]; } + $titles = array(); foreach ( $this->getPages( $context ) as $page => $options ) { - $titles[] = Title::makeTitle( $options['ns'], $page ); + $titles[$options['ns']][$page] = true; } - // Do batch existence check - // TODO: This would work better if page_touched were loaded by this as well - $lb = new LinkBatch( $titles ); - $lb->execute(); + $modifiedTime = 1; // wfTimestamp() interprets 0 as "now" - foreach ( $titles as $title ) { - if ( $title->exists() ) { - $modifiedTime = max( $modifiedTime, wfTimestamp( TS_UNIX, $title->getTouched() ) ); + + if ( $titles ) { + $dbr = wfGetDB( DB_SLAVE ); + $latest = $dbr->selectField( 'page', 'MAX(page_touched)', + $dbr->makeWhereFrom2d( $titles, 'page_namespace', 'page_title' ), + __METHOD__ ); + + if ( $latest ) { + $modifiedTime = wfTimestamp( TS_UNIX, $latest ); } } + return $this->modifiedTime[$hash] = $modifiedTime; } } @@ -798,6 +852,12 @@ class ResourceLoaderSiteModule extends ResourceLoaderWikiModule { } return $pages; } + + /* Methods */ + + public function getGroup() { + return 'site'; + } } /** @@ -821,12 +881,18 @@ class ResourceLoaderUserModule extends ResourceLoaderWikiModule { } return array(); } + + /* Methods */ + + public function getGroup() { + return 'user'; + } } /** * Module for user preference customizations */ -class ResourceLoaderUserPreferencesModule extends ResourceLoaderModule { +class ResourceLoaderUserOptionsModule extends ResourceLoaderModule { /* Protected Members */ @@ -839,40 +905,68 @@ class ResourceLoaderUserPreferencesModule extends ResourceLoaderModule { if ( isset( $this->modifiedTime[$hash] ) ) { return $this->modifiedTime[$hash]; } - if ( $context->getUser() ) { - $user = User::newFromName( $context->getUser() ); - return $this->modifiedTime[$hash] = $user->getTouched(); + + global $wgUser; + + if ( $context->getUser() === $wgUser->getName() ) { + return $this->modifiedTime[$hash] = $wgUser->getTouched(); } else { - return 0; + return 1; + } + } + + /** + * Fetch the context's user options, or if it doesn't match current user, + * the default options. + * + * @param ResourceLoaderContext $context + * @return array + */ + protected function contextUserOptions( ResourceLoaderContext $context ) { + global $wgUser; + + // Verify identity -- this is a private module + if ( $context->getUser() === $wgUser->getName() ) { + return $wgUser->getOptions(); + } else { + return User::getDefaultOptions(); } } + public function getScript( ResourceLoaderContext $context ) { + $encOptions = FormatJson::encode( $this->contextUserOptions( $context ) ); + return "mediaWiki.user.options.set( $encOptions );"; + } + public function getStyles( ResourceLoaderContext $context ) { global $wgAllowUserCssPrefs; + if ( $wgAllowUserCssPrefs ) { - $user = User::newFromName( $context->getUser() ); + $options = $this->contextUserOptions( $context ); + + // Build CSS rules $rules = array(); - if ( ( $underline = $user->getOption( 'underline' ) ) < 2 ) { - $rules[] = "a { text-decoration: " . ( $underline ? 'underline' : 'none' ) . "; }"; + if ( $options['underline'] < 2 ) { + $rules[] = "a { text-decoration: " . ( $options['underline'] ? 'underline' : 'none' ) . "; }"; } - if ( $user->getOption( 'highlightbroken' ) ) { + if ( $options['highlightbroken'] ) { $rules[] = "a.new, #quickbar a.new { color: #CC2200; }\n"; } else { $rules[] = "a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; }"; $rules[] = "a.new:after, #quickbar a.new:after { content: '?'; color: #CC2200; }"; $rules[] = "a.stub:after, #quickbar a.stub:after { content: '!'; color: #772233; }"; } - if ( $user->getOption( 'justify' ) ) { + if ( $options['justify'] ) { $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n"; } - if ( !$user->getOption( 'showtoc' ) ) { + if ( !$options['showtoc'] ) { $rules[] = "#toc { display: none; }\n"; } - if ( !$user->getOption( 'editsection' ) ) { + if ( !$options['editsection'] ) { $rules[] = ".editsection { display: none; }\n"; } - if ( ( $fontstyle = $user->getOption( 'editfont' ) ) !== 'default' ) { - $rules[] = "textarea { font-family: $fontstyle; }\n"; + if ( $options['editfont'] !== 'default' ) { + $rules[] = "textarea { font-family: {$options['editfont']}; }\n"; } return array( 'all' => implode( "\n", $rules ) ); } @@ -884,6 +978,10 @@ class ResourceLoaderUserPreferencesModule extends ResourceLoaderModule { return $wgContLang->getDir() !== $context->getDirection(); } + + public function getGroup() { + return 'private'; + } } class ResourceLoaderStartUpModule extends ResourceLoaderModule { @@ -894,9 +992,11 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { /* Protected Methods */ protected function getConfig( $context ) { - global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension, $wgArticlePath, $wgScriptPath, $wgServer, - $wgContLang, $wgBreakFrames, $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgAjaxWatch, $wgVersion, - $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest, $wgSitename, $wgFileExtensions; + global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension, + $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgBreakFrames, + $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion, + $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest, + $wgSitename, $wgFileExtensions; // Pre-process information $separatorTransTable = $wgContLang->separatorTransformTable(); @@ -940,32 +1040,71 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'wgNamespaceIds' => $wgContLang->getNamespaceIds(), 'wgSiteName' => $wgSitename, 'wgFileExtensions' => $wgFileExtensions, + 'wgDBname' => $wgDBname, ); if ( $wgContLang->hasVariants() ) { $vars['wgUserVariant'] = $wgContLang->getPreferredVariant(); } if ( $wgUseAjax && $wgEnableMWSuggest ) { $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate(); - $vars['wgDBname'] = $wgDBname; } return $vars; } + /** + * Gets registration code for all modules + * + * @param $context ResourceLoaderContext object + * @return String: JavaScript code for registering all modules with the client loader + */ + public static function getModuleRegistrations( ResourceLoaderContext $context ) { + wfProfileIn( __METHOD__ ); + + $out = ''; + $registrations = array(); + foreach ( $context->getResourceLoader()->getModules() as $name => $module ) { + // Support module loader scripts + if ( ( $loader = $module->getLoaderScript() ) !== false ) { + $deps = $module->getDependencies(); + $group = $module->getGroup(); + $version = wfTimestamp( TS_ISO_8601_BASIC, round( $module->getModifiedTime( $context ), -2 ) ); + $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $loader ); + } + // Automatically register module + else { + // Modules without dependencies or a group pass two arguments (name, timestamp) to + // mediaWiki.loader.register() + if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) { + $registrations[] = array( $name, $module->getModifiedTime( $context ) ); + } + // Modules with dependencies but no group pass three arguments (name, timestamp, dependencies) + // to mediaWiki.loader.register() + else if ( $module->getGroup() === null ) { + $registrations[] = array( + $name, $module->getModifiedTime( $context ), $module->getDependencies() ); + } + // Modules with dependencies pass four arguments (name, timestamp, dependencies, group) + // to mediaWiki.loader.register() + else { + $registrations[] = array( + $name, $module->getModifiedTime( $context ), $module->getDependencies(), $module->getGroup() ); + } + } + } + $out .= ResourceLoader::makeLoaderRegisterScript( $registrations ); + + wfProfileOut( __METHOD__ ); + return $out; + } + /* Methods */ public function getScript( ResourceLoaderContext $context ) { - global $IP, $wgStylePath, $wgLoadScript; - - $scripts = file_get_contents( "$IP/resources/startup.js" ); + global $IP, $wgLoadScript; + $out = file_get_contents( "$IP/resources/startup.js" ); if ( $context->getOnly() === 'scripts' ) { - // Get all module registrations - $registration = ResourceLoader::getModuleRegistrations( $context ); - // Build configuration - $config = FormatJson::encode( $this->getConfig( $context ) ); - // Add a well-known start-up function - $scripts .= "window.startUp = function() { $registration mediaWiki.config.set( $config ); };"; // Build load query for jquery and mediawiki modules $query = array( 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ), @@ -973,22 +1112,25 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { 'lang' => $context->getLanguage(), 'skin' => $context->getSkin(), 'debug' => $context->getDebug() ? 'true' : 'false', - 'version' => wfTimestamp( TS_ISO_8601, round( max( - ResourceLoader::getModule( 'jquery' )->getModifiedTime( $context ), - ResourceLoader::getModule( 'mediawiki' )->getModifiedTime( $context ) + 'version' => wfTimestamp( TS_ISO_8601_BASIC, round( max( + $context->getResourceLoader()->getModule( 'jquery' )->getModifiedTime( $context ), + $context->getResourceLoader()->getModule( 'mediawiki' )->getModifiedTime( $context ) ), -2 ) ) ); - // Uniform query order + // Ensure uniform query order ksort( $query ); - // Build HTML code for loading jquery and mediawiki modules - $loadScript = Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ); - // Add code to add jquery and mediawiki loading code; only if the current client is compatible - $scripts .= "if ( isCompatible() ) { document.write( '$loadScript' ); }"; - // Delete the compatible function - it's not needed anymore - $scripts .= "delete window['isCompatible'];"; + + // Startup function + $configuration = FormatJson::encode( $this->getConfig( $context ) ); + $registrations = self::getModuleRegistrations( $context ); + $out .= "var startUp = function() {\n\t$registrations\n\tmediaWiki.config.set( $configuration );\n};"; + + // Conditional script injection + $scriptTag = Xml::escapeJsString( Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) ); + $out .= "if ( isCompatible() ) {\n\tdocument.write( '$scriptTag' );\n}\ndelete isCompatible;"; } - return $scripts; + return $out; } public function getModifiedTime( ResourceLoaderContext $context ) { @@ -999,18 +1141,14 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { return $this->modifiedTime[$hash]; } $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 code! - $this->modifiedTime[$hash] = ResourceLoader::getHighestModifiedTime( $context ); - return $this->modifiedTime[$hash]; - } - - public function getClientMaxage() { - return 300; // 5 minutes - } - - public function getServerMaxage() { - return 300; // 5 minutes + $time = 1; // wfTimestamp() treats 0 as 'now', so that's not a suitable choice + foreach ( $context->getResourceLoader()->getModules() as $module ) { + $time = max( $time, $module->getModifiedTime( $context ) ); + } + return $this->modifiedTime[$hash] = $time; } public function getFlip( $context ) { @@ -1018,4 +1156,10 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { return $wgContLang->getDir() !== $context->getDirection(); } + + /* Methods */ + + public function getGroup() { + return 'startup'; + } }