From 3f501420ee6db340670dff009e8aeb5a3574031d Mon Sep 17 00:00:00 2001 From: Siebrand Mazeland Date: Sat, 4 Sep 2010 12:53:01 +0000 Subject: [PATCH] Stylize files added in r72349 --- includes/CSSJanus.php | 78 ++++---- includes/CSSMin.php | 32 ++-- includes/MessageBlobStore.php | 62 ++++--- includes/ResourceLoader.php | 69 ++++--- includes/ResourceLoaderContext.php | 36 ++-- includes/ResourceLoaderModule.php | 171 ++++++++++-------- load.php | 4 +- .../tests/ResourceLoaderFileModuleTest.php | 13 +- maintenance/tests/ResourceLoaderTest.php | 13 +- 9 files changed, 276 insertions(+), 202 deletions(-) diff --git a/includes/CSSJanus.php b/includes/CSSJanus.php index 86d52b0be1..08e8e0963d 100644 --- a/includes/CSSJanus.php +++ b/includes/CSSJanus.php @@ -20,10 +20,10 @@ /** * This is a PHP port of CSSJanus, a utility that transforms CSS style sheets * written for LTR to RTL. - * + * * The original Python version of CSSJanus is Copyright 2008 by Google Inc. and - * is distributed under the Apache license. - * + * is distributed under the Apache license. + * * Original code: http://code.google.com/p/cssjanus/source/browse/trunk/cssjanus.py * License of original code: http://code.google.com/p/cssjanus/source/browse/trunk/LICENSE * @author Roan Kattouw @@ -55,7 +55,6 @@ class CSSJanus { 'lookbehind_not_letter' => '(? '[^\}]*?', 'noflip_annotation' => '\/\*\s*@noflip\s*\*\/', - 'noflip_single' => null, 'noflip_class' => null, 'comment' => '/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//', @@ -74,7 +73,7 @@ class CSSJanus { 'bg_horizontal_percentage' => null, 'bg_horizontal_percentage_x' => null, ); - + /** * Build patterns we can't define above because they depend on other patterns. */ @@ -83,6 +82,7 @@ class CSSJanus { // Patterns have already been built return; } + $patterns =& self::$patterns; $patterns['escape'] = "(?:{$patterns['unicode']}|\\[^\r\n\f0-9a-f])"; $patterns['nmstart'] = "(?:[_a-z]|{$patterns['nonAscii']}|{$patterns['escape']})"; @@ -95,7 +95,6 @@ class CSSJanus { $patterns['lookahead_not_open_brace'] = "(?!({$patterns['nmchar']}|\r?\n|\s|#|\:|\.|\,|\+|>)*?{)"; $patterns['lookahead_not_closing_paren'] = "(?!{$patterns['url_chars']}?{$patterns['valid_after_uri_chars']}\))"; $patterns['lookahead_for_closing_paren'] = "(?={$patterns['url_chars']}?{$patterns['valid_after_uri_chars']}\))"; - $patterns['noflip_single'] = "/({$patterns['noflip_annotation']}{$patterns['lookahead_not_open_brace']}[^;}]+;?)/i"; $patterns['noflip_class'] = "/({$patterns['noflip_annotation']}{$patterns['chars_within_selector']}})/i"; $patterns['body_direction_ltr'] = "/({$patterns['body_selector']}{$patterns['chars_within_selector']}{$patterns['direction']})ltr/i"; @@ -115,7 +114,7 @@ class CSSJanus { $patterns['bg_horizontal_percentage'] = "/(background(?:-position)?\s*:\s*[^%]*?)({$patterns['num']})(%\s*(?:{$patterns['quantity']}|{$patterns['ident']}))/"; $patterns['bg_horizontal_percentage_x'] = "/(background-position-x\s*:\s*)({$patterns['num']})(%)/"; } - + /** * Transform an LTR stylesheet to RTL * @param string $css Stylesheet to transform @@ -126,28 +125,29 @@ class CSSJanus { public static function transform( $css, $swapLtrRtlInURL = false, $swapLeftRightInURL = false ) { // We wrap tokens in ` , not ~ like the original implementation does. // This was done because ` is not a legal character in CSS and can only - // occur in URLs, where we escape it to %60 before inserting our tokens. + // occur in URLs, where we escape it to %60 before inserting our tokens. $css = str_replace( '`', '%60', $css ); - + self::buildPatterns(); - + // Tokenize single line rules with /* @noflip */ $noFlipSingle = new CSSJanus_Tokenizer( self::$patterns['noflip_single'], '`NOFLIP_SINGLE`' ); $css = $noFlipSingle->tokenize( $css ); - + // Tokenize class rules with /* @noflip */ $noFlipClass = new CSSJanus_Tokenizer( self::$patterns['noflip_class'], '`NOFLIP_CLASS`' ); $css = $noFlipClass->tokenize( $css ); - + // Tokenize comments $comments = new CSSJanus_Tokenizer( self::$patterns['comment'], '`C`' ); $css = $comments->tokenize( $css ); - + // LTR->RTL fixes start here $css = self::fixBodyDirection( $css ); if ( $swapLtrRtlInURL ) { $css = self::fixLtrRtlInURL( $css ); } + if ( $swapLeftRightInURL ) { $css = self::fixLeftRightInURL( $css ); } @@ -155,18 +155,19 @@ class CSSJanus { $css = self::fixCursorProperties( $css ); $css = self::fixFourPartNotation( $css ); $css = self::fixBackgroundPosition( $css ); - + // Detokenize stuff we tokenized before $css = $comments->detokenize( $css ); $css = $noFlipClass->detokenize( $css ); $css = $noFlipSingle->detokenize( $css ); + return $css; } - + /** * Replace direction: ltr; with direction: rtl; and vice versa, but *only* * those inside a body { .. } selector. - * + * * Unlike the original implementation, this function doesn't suffer from * the bug causing "body\n{\ndirection: ltr;\n}" to be missed. * See http://code.google.com/p/cssjanus/issues/detail?id=15 @@ -176,9 +177,10 @@ class CSSJanus { '$1' . self::$patterns['tmpToken'], $css ); $css = preg_replace( self::$patterns['body_direction_rtl'], '$1ltr', $css ); $css = str_replace( self::$patterns['tmpToken'], 'rtl', $css ); + return $css; } - + /** * Replace 'ltr' with 'rtl' and vice versa in background URLs */ @@ -186,9 +188,10 @@ class CSSJanus { $css = preg_replace( self::$patterns['ltr_in_url'], self::$patterns['tmpToken'], $css ); $css = preg_replace( self::$patterns['rtl_in_url'], 'ltr', $css ); $css = str_replace( self::$patterns['tmpToken'], 'rtl', $css ); + return $css; } - + /** * Replace 'left' with 'right' and vice versa in background URLs */ @@ -196,9 +199,10 @@ class CSSJanus { $css = preg_replace( self::$patterns['left_in_url'], self::$patterns['tmpToken'], $css ); $css = preg_replace( self::$patterns['right_in_url'], 'left', $css ); $css = str_replace( self::$patterns['tmpToken'], 'right', $css ); + return $css; } - + /** * Flip rules like left: , padding-right: , etc. */ @@ -206,9 +210,10 @@ class CSSJanus { $css = preg_replace( self::$patterns['left'], self::$patterns['tmpToken'], $css ); $css = preg_replace( self::$patterns['right'], 'left', $css ); $css = str_replace( self::$patterns['tmpToken'], 'right', $css ); + return $css; } - + /** * Flip East and West in rules like cursor: nw-resize; */ @@ -217,13 +222,14 @@ class CSSJanus { '$1' . self::$patterns['tmpToken'], $css ); $css = preg_replace( self::$patterns['cursor_west'], '$1e-resize', $css ); $css = str_replace( self::$patterns['tmpToken'], 'w-resize', $css ); + return $css; } - + /** * Swap the second and fourth parts in four-part notation rules like * padding: 1px 2px 3px 4px; - * + * * Unlike the original implementation, this function doesn't suffer from * the bug where whitespace is not preserved when flipping four-part rules * and four-part color rules with multiple whitespace characters between @@ -233,27 +239,29 @@ class CSSJanus { private static function fixFourPartNotation( $css ) { $css = preg_replace( self::$patterns['four_notation_quantity'], '$1$2$7$4$5$6$3', $css ); $css = preg_replace( self::$patterns['four_notation_color'], '$1$2$3$8$5$6$7$4', $css ); + return $css; } - + /** - * Flip horizontal background percentages. + * Flip horizontal background percentages. */ private static function fixBackgroundPosition( $css ) { $css = preg_replace_callback( self::$patterns['bg_horizontal_percentage'], array( 'self', 'calculateNewBackgroundPosition' ), $css ); $css = preg_replace_callback( self::$patterns['bg_horizontal_percentage_x'], array( 'self', 'calculateNewBackgroundPosition' ), $css ); + return $css; } - + /** - * Callback for calculateNewBackgroundPosition() + * Callback for calculateNewBackgroundPosition() */ private static function calculateNewBackgroundPosition( $matches ) { return $matches[1] . ( 100 - $matches[2] ) . $matches[3]; } -} +} /** * Utility class used by CSSJanus that tokenizes and untokenizes things we want @@ -263,7 +271,7 @@ class CSSJanus { class CSSJanus_Tokenizer { private $regex, $token; private $originals; - + /** * Constructor * @param $regex string Regular expression whose matches to replace by a token. @@ -274,22 +282,22 @@ class CSSJanus_Tokenizer { $this->token = $token; $this->originals = array(); } - + /** * Replace all occurrences of $regex in $str with a token and remember - * the original strings. + * the original strings. * @param $str string String to tokenize * @return string Tokenized string */ public function tokenize( $str ) { return preg_replace_callback( $this->regex, array( $this, 'tokenizeCallback' ), $str ); } - + private function tokenizeCallback( $matches ) { $this->originals[] = $matches[0]; return $this->token; } - + /** * Replace tokens with their originals. If multiple strings were tokenized, it's important they be * detokenized in exactly the SAME ORDER. @@ -303,11 +311,11 @@ class CSSJanus_Tokenizer { return preg_replace_callback( '/' . preg_quote( $this->token, '/' ) . '/', array( $this, 'detokenizeCallback' ), $str ); } - + private function detokenizeCallback( $matches ) { $retval = current( $this->originals ); next( $this->originals ); + return $retval; } - -} \ No newline at end of file +} diff --git a/includes/CSSMin.php b/includes/CSSMin.php index 3272bbe0f4..ab49445e35 100644 --- a/includes/CSSMin.php +++ b/includes/CSSMin.php @@ -1,22 +1,21 @@ [^\?\)\:]*)\??[^\)]*[\'"]?\)/'; $files = array(); + if ( preg_match_all( $pattern, $source, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER ) ) { foreach ( $matches as $match ) { $file = ( isset( $path ) ? rtrim( $path, '/' ) . '/' : '' ) . "{$match['file'][0]}"; + // Only proceed if we can access the file if ( file_exists( $file ) ) { $files[] = $file; } } } + return $files; } - + /** * Remaps CSS URL paths and automatically embeds data URIs for URL rules preceded by an /* @embed * / comment - * + * * @param $source string CSS data to remap * @param $path string File path where the source was read from * @return string Remapped CSS data */ public static function remap( $source, $path ) { global $wgUseDataURLs; + $pattern = '/((?\s*\/\*\s*\@embed\s*\*\/)(?[^\;\}]*))?url\([\'"]?(?[^\?\)\:]*)\??[^\)]*[\'"]?\)(?[^;]*)[\;]?/'; $offset = 0; + while ( preg_match( $pattern, $source, $match, PREG_OFFSET_CAPTURE, $offset ) ) { // Shortcuts $embed = $match['embed'][0]; $rule = $match['rule'][0]; $extra = $match['extra'][0]; $file = "{$path}/{$match['file'][0]}"; + // Only proceed if we can access the file if ( file_exists( $file ) ) { // Add version parameter as a time-stamp in ISO 8601 format, using Z for the timezone, meaning GMT $url = "{$file}?" . gmdate( 'Y-m-d\TH:i:s\Z', round( filemtime( $file ), -2 ) ); + // Detect when URLs were preceeded with embed tags, and also verify file size is below the limit if ( $wgUseDataURLs && $match['embed'][1] > 0 && filesize( $file ) < self::EMBED_SIZE_LIMIT ) { // If we ever get to PHP 5.3, we should use the Fileinfo extension instead of mime_content_type @@ -71,6 +77,7 @@ class CSSMin { // Build a CSS property with a remapped and versioned URL $replacement = "{$embed}{$rule}url({$url}){$extra};"; } + // Perform replacement on the source $source = substr_replace( $source, $replacement, $match[0][1], strlen( $match[0][0] ) ); // Move the offset to the end of the replacement in the source @@ -80,14 +87,15 @@ class CSSMin { // Move the offset to the end of the match, leaving it alone $offset = $match[0][1] + strlen( $match[0][0] ); } + return $source; } - + /** * Removes whitespace from CSS data - * + * * @param $source string CSS data to minify - * @return string Minified CSS data + * @return string Minified CSS data */ public static function minify( $css ) { return trim( @@ -98,4 +106,4 @@ class CSSMin { ) ); } -} \ No newline at end of file +} diff --git a/includes/MessageBlobStore.php b/includes/MessageBlobStore.php index 8fc72d04ba..51f3f72d6a 100644 --- a/includes/MessageBlobStore.php +++ b/includes/MessageBlobStore.php @@ -42,7 +42,7 @@ class MessageBlobStore { } // Try getting from the DB first $blobs = self::getFromDB( $modules, $lang ); - + // Generate blobs for any missing modules and store them in the DB $missing = array_diff( $modules, array_keys( $blobs ) ); foreach ( $missing as $module ) { @@ -51,9 +51,10 @@ class MessageBlobStore { $blobs[$module] = $blob; } } + return $blobs; } - + /** * Generate and insert a new message blob. If the blob was already * present, it is not regenerated; instead, the preexisting blob @@ -64,10 +65,11 @@ class MessageBlobStore { */ public static function insertMessageBlob( $module, $lang ) { $blob = self::generateMessageBlob( $module, $lang ); + if ( !$blob ) { return false; } - + $dbw = wfGetDb( DB_MASTER ); $success = $dbw->insert( 'msg_resource', array( 'mr_lang' => $lang, @@ -78,6 +80,7 @@ class MessageBlobStore { __METHOD__, array( 'IGNORE' ) ); + if ( $success ) { if ( $dbw->affectedRows() == 0 ) { // Blob was already present, fetch it @@ -91,6 +94,7 @@ class MessageBlobStore { // Update msg_resource_links $rows = array(); $mod = ResourceLoader::getModule( $module ); + foreach ( $mod->getMessages() as $key ) { $rows[] = array( 'mrl_resource' => $module, @@ -102,9 +106,10 @@ class MessageBlobStore { ); } } + return $blob; } - + /** * Update all message blobs for a given module. * @param $module string Module name @@ -113,22 +118,24 @@ class MessageBlobStore { */ public static function updateModule( $module, $lang = null ) { $retval = null; - + // Find all existing blobs for this module $dbw = wfGetDb( DB_MASTER ); $res = $dbw->select( 'msg_resource', array( 'mr_lang', 'mr_blob' ), array( 'mr_resource' => $module ), __METHOD__ ); - + // Build the new msg_resource rows $newRows = array(); $now = $dbw->timestamp(); // Save the last-processed old and new blobs for later $oldBlob = $newBlob = null; + foreach ( $res as $row ) { $oldBlob = $row->mr_blob; $newBlob = self::generateMessageBlob( $module, $row->mr_lang ); + if ( $row->mr_lang === $lang ) { $retval = $newBlob; } @@ -144,13 +151,13 @@ class MessageBlobStore { array( array( 'mr_resource', 'mr_lang' ) ), $newRows, __METHOD__ ); - + // Figure out which messages were added and removed $oldMessages = array_keys( FormatJson::decode( $oldBlob, true ) ); $newMessages = array_keys( FormatJson::decode( $newBlob, true ) ); $added = array_diff( $newMessages, $oldMessages ); $removed = array_diff( $oldMessages, $newMessages ); - + // Delete removed messages, insert added ones if ( $removed ) { $dbw->delete( 'msg_resource_links', array( @@ -159,35 +166,39 @@ class MessageBlobStore { ), __METHOD__ ); } + $newLinksRows = array(); + foreach ( $added as $message ) { $newLinksRows[] = array( 'mrl_resource' => $module, 'mrl_message' => $message ); } + if ( $newLinksRows ) { $dbw->insert( 'msg_resource_links', $newLinksRows, __METHOD__, array( 'IGNORE' ) // just in case ); } - + return $retval; } - + /** * Update a single message in all message blobs it occurs in. * @param $key string Message key */ public static function updateMessage( $key ) { $dbw = wfGetDb( DB_MASTER ); - + // Keep running until the updates queue is empty. // Due to update conflicts, the queue might not be emptied // in one iteration. $updates = null; do { $updates = self::getUpdatesForMessage( $key, $updates ); + foreach ( $updates as $key => $update ) { // Update the row on the condition that it // didn't change since we fetched it by putting @@ -200,7 +211,7 @@ class MessageBlobStore { 'mr_timestamp' => $update['timestamp'] ), __METHOD__ ); - + // Only requeue conflicted updates. // If update() returned false, don't retry, for // fear of getting into an infinite loop @@ -210,11 +221,11 @@ class MessageBlobStore { } } } while ( count( $updates ) ); - + // No need to update msg_resource_links because we didn't add // or remove any messages, we just changed their contents. } - + public static function clear() { // TODO: Give this some more thought // TODO: Is TRUNCATE better? @@ -222,7 +233,7 @@ class MessageBlobStore { $dbw->delete( 'msg_resource', '*', __METHOD__ ); $dbw->delete( 'msg_resource_links', '*', __METHOD__ ); } - + /** * Create an update queue for updateMessage() * @param $key string Message key @@ -231,6 +242,7 @@ class MessageBlobStore { */ private static function getUpdatesForMessage( $key, $prevUpdates = null ) { $dbw = wfGetDb( DB_MASTER ); + if ( is_null( $prevUpdates ) ) { // Fetch all blobs referencing $key $res = $dbw->select( @@ -241,23 +253,25 @@ class MessageBlobStore { ); } else { // Refetch the blobs referenced by $prevUpdates - + // Reorganize the (resource, lang) pairs in the format // expected by makeWhereFrom2d() $twoD = array(); + foreach ( $prevUpdates as $update ) { $twoD[$update['resource']][$update['lang']] = true; } - + $res = $dbw->select( 'msg_resource', array( 'mr_resource', 'mr_lang', 'mr_blob', 'mr_timestamp' ), $dbw->makeWhereFrom2d( $twoD, 'mr_resource', 'mr_lang' ), __METHOD__ ); } - + // Build the new updates queue $updates = array(); + foreach ( $res as $row ) { $updates[] = array( 'resource' => $row->mr_resource, @@ -267,9 +281,10 @@ class MessageBlobStore { $key, $row->mr_lang ) ); } + return $updates; } - + /** * Reencode a message blob with the updated value for a message * @param $blob string Message blob (JSON object) @@ -280,9 +295,10 @@ class MessageBlobStore { private static function reencodeBlob( $blob, $key, $lang ) { $decoded = FormatJson::decode( $blob, true ); $decoded[$key] = wfMsgExt( $key, array( 'language' => $lang ) ); + return FormatJson::encode( $decoded ); } - + /** * Get the message blobs for a set of modules from the database. * Modules whose blobs are not in the database are silently dropped. @@ -297,6 +313,7 @@ class MessageBlobStore { array( 'mr_resource' => $modules, 'mr_lang' => $lang ), __METHOD__ ); + foreach ( $res as $row ) { $module = ResourceLoader::getModule( $row->mr_resource ); if ( !$module ) { @@ -309,9 +326,10 @@ class MessageBlobStore { $retval[$row->mr_resource] = $row->mr_blob; } } + return $retval; } - + /** * Generate the message blob for a given module in a given language. * @param $module string Module name @@ -321,9 +339,11 @@ class MessageBlobStore { private static function generateMessageBlob( $module, $lang ) { $mod = ResourceLoader::getModule( $module ); $messages = array(); + foreach ( $mod->getMessages() as $key ) { $messages[$key] = wfMsgExt( $key, array( 'language' => $lang ) ); } + return FormatJson::encode( $messages ); } } diff --git a/includes/ResourceLoader.php b/includes/ResourceLoader.php index f79114af8b..d568d2845e 100644 --- a/includes/ResourceLoader.php +++ b/includes/ResourceLoader.php @@ -61,14 +61,13 @@ * ResourceLoader::respond( $wgRequest, $wgServer . $wgScriptPath . '/load.php' ); */ class ResourceLoader { - /* Protected Static Members */ - + // @var array list of module name/ResourceLoaderModule object pairs protected static $modules = array(); - + /* Protected Static Methods */ - + /** * Runs text through a filter, caching the filtered result for future calls * @@ -79,18 +78,20 @@ class ResourceLoader { */ protected static function filter( $filter, $data ) { global $wgMemc; + // For empty or whitespace-only things, don't do any processing if ( trim( $data ) === '' ) { return $data; } - + // Try memcached $key = wfMemcKey( 'resourceloader', 'filter', $filter, md5( $data ) ); $cached = $wgMemc->get( $key ); + if ( $cached !== false && $cached !== null ) { return $cached; } - + // Run the filter try { switch ( $filter ) { @@ -110,24 +111,25 @@ class ResourceLoader { } catch ( Exception $exception ) { throw new MWException( 'Filter threw an exception: ' . $exception->getMessage() ); } - + // Save to memcached $wgMemc->set( $key, $result ); + return $result; } - + /* Static Methods */ - + /** * Registers a module with the ResourceLoader system. * * Note that registering the same object under multiple names is not supported and may silently fail in all * kinds of interesting ways. - * + * * @param {mixed} $name string of name of module or array of name/object pairs * @param {ResourceLoaderModule} $object module object (optional when using multiple-registration calling style) * @return {boolean} false if there were any errors, in which case one or more modules were not registered - * + * * @todo We need much more clever error reporting, not just in detailing what happened, but in bringing errors to * the client in a way that they can easily see them if they want to, such as by using FireBug */ @@ -137,8 +139,10 @@ class ResourceLoader { foreach ( $name as $key => $value ) { self::register( $key, $value ); } + return; } + // Disallow duplicate registrations if ( isset( self::$modules[$name] ) ) { // A module has already been registered by this name @@ -148,7 +152,7 @@ class ResourceLoader { self::$modules[$name] = $object; $object->setName( $name ); } - + /** * Gets a map of all modules and their options * @@ -157,7 +161,7 @@ class ResourceLoader { public static function getModules() { return self::$modules; } - + /** * Get the ResourceLoaderModule object for a given module name * @param $name string Module name @@ -166,7 +170,7 @@ class ResourceLoader { public static function getModule( $name ) { return isset( self::$modules[$name] ) ? self::$modules[$name] : null; } - + /** * Gets registration code for all modules, except pre-registered ones listed in self::$preRegisteredModules * @@ -180,6 +184,7 @@ class ResourceLoader { public static function getModuleRegistrations( ResourceLoaderContext $context ) { $scripts = ''; $registrations = array(); + foreach ( self::$modules as $name => $module ) { // Support module loader scripts if ( ( $loader = $module->getLoaderScript() ) !== false ) { @@ -199,7 +204,7 @@ class ResourceLoader { } return $scripts . "mediaWiki.loader.register( " . FormatJson::encode( $registrations ) . " );"; } - + /** * Get the highest modification time of all modules, based on a given combination of language code, * skin name and debug mode flag. @@ -210,14 +215,16 @@ class ResourceLoader { */ public static function getHighestModifiedTime( ResourceLoaderContext $context ) { $time = 1; // wfTimestamp() treats 0 as 'now', so that's not a suitable choice + foreach ( self::$modules as $module ) { $time = max( $time, $module->getModifiedTime( $context ) ); } + return $time; } - + /* Methods */ - + /* * Outputs a response to a resource load-request, including a content-type header * @@ -237,6 +244,7 @@ class ResourceLoader { // Split requested modules into two groups, modules and missing $modules = array(); $missing = array(); + foreach ( $context->getModules() as $name ) { if ( isset( self::$modules[$name] ) ) { $modules[] = $name; @@ -244,52 +252,58 @@ class ResourceLoader { $missing[] = $name; } } - + // Calculate the mtime and caching maxages for this request. We need this, 304 or no 304 $mtime = 1; $maxage = PHP_INT_MAX; $smaxage = PHP_INT_MAX; + foreach ( $modules as $name ) { $mtime = max( $mtime, self::$modules[$name]->getModifiedTime( $context ) ); $maxage = min( $maxage, self::$modules[$name]->getClientMaxage() ); $smaxage = min( $smaxage, self::$modules[$name]->getServerMaxage() ); } - + // Output headers if ( $context->getOnly() === 'styles' ) { header( 'Content-Type: text/css' ); } else { header( 'Content-Type: text/javascript' ); } + header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) ); $expires = wfTimestamp( TS_RFC2822, min( $maxage, $smaxage ) + time() ); header( "Cache-Control: public, maxage=$maxage, s-maxage=$smaxage" ); header( "Expires: $expires" ); - + // Check if there's an If-Modified-Since header and respond with a 304 Not Modified if possible $ims = $context->getRequest()->getHeader( 'If-Modified-Since' ); + if ( $ims !== false && wfTimestamp( TS_UNIX, $ims ) == $mtime ) { header( 'HTTP/1.0 304 Not Modified' ); header( 'Status: 304 Not Modified' ); return; } - + // Use output buffering ob_start(); - + // Pre-fetch blobs $blobs = $context->shouldIncludeMessages() ? MessageBlobStore::get( $modules, $context->getLanguage() ) : array(); - + // Generate output foreach ( $modules as $name ) { // Scripts $scripts = ''; + if ( $context->shouldIncludeScripts() ) { $scripts .= self::$modules[$name]->getScript( $context ); } + // Styles $styles = ''; + if ( $context->shouldIncludeStyles() && ( $styles .= self::$modules[$name]->getStyle( $context ) ) !== '' @@ -299,8 +313,10 @@ class ResourceLoader { } $styles = $context->getDebug() ? $styles : self::filter( 'minify-css', $styles ); } + // Messages $messages = isset( $blobs[$name] ) ? $blobs[$name] : '{}'; + // Output if ( $context->getOnly() === 'styles' ) { echo $styles; @@ -313,21 +329,26 @@ class ResourceLoader { echo "mediaWiki.loader.implement( '$name', function() {{$scripts}},\n'$styles',\n$messages );\n"; } } + // Update the status of script-only modules if ( $context->getOnly() === 'scripts' && !in_array( 'startup', $modules ) ) { $statuses = array(); + foreach ( $modules as $name ) { $statuses[$name] = 'ready'; } + $statuses = FormatJson::encode( $statuses ); echo "mediaWiki.loader.state( $statuses );"; } + // Register missing modules if ( $context->shouldIncludeScripts() ) { foreach ( $missing as $name ) { echo "mediaWiki.loader.register( '$name', null, 'missing' );\n"; } } + // Output the appropriate header if ( $context->getOnly() !== 'styles' ) { if ( $context->getDebug() ) { @@ -340,4 +361,4 @@ class ResourceLoader { } // FIXME: Temp hack -require_once "$IP/resources/Resources.php"; \ No newline at end of file +require_once "$IP/resources/Resources.php"; diff --git a/includes/ResourceLoaderContext.php b/includes/ResourceLoaderContext.php index c6a277fc26..3052b5fbf9 100644 --- a/includes/ResourceLoaderContext.php +++ b/includes/ResourceLoaderContext.php @@ -23,9 +23,8 @@ * Object passed around to modules which contains information about the state of a specific loader request */ class ResourceLoaderContext { - /* Protected Members */ - + protected $request; protected $server; protected $modules; @@ -35,12 +34,12 @@ class ResourceLoaderContext { protected $debug; protected $only; protected $hash; - + /* Methods */ - + public function __construct( WebRequest $request, $server ) { global $wgUser, $wgLang, $wgDefaultSkin; - + $this->request = $request; $this->server = $server; // Interperet request @@ -50,30 +49,33 @@ class ResourceLoaderContext { $this->skin = $request->getVal( 'skin' ); $this->debug = $request->getVal( 'debug' ) === 'true' || $request->getBool( 'debug' ); $this->only = $request->getVal( 'only' ); + // Fallback on system defaults if ( !$this->language ) { $this->language = $wgLang->getCode(); } + if ( !$this->direction ) { $this->direction = Language::factory( $this->language )->getDir(); } + if ( !$this->skin ) { $this->skin = $wgDefaultSkin; } } - + public function getRequest() { return $this->request; } - + public function getServer() { return $this->server; } - + public function getModules() { return $this->modules; } - + public function getLanguage() { return $this->language; } @@ -81,33 +83,33 @@ class ResourceLoaderContext { public function getDirection() { return $this->direction; } - + public function getSkin() { return $this->skin; } - + public function getDebug() { return $this->debug; } - + public function getOnly() { return $this->only; } - + public function shouldIncludeScripts() { return is_null( $this->only ) || $this->only === 'scripts'; } - + public function shouldIncludeStyles() { return is_null( $this->only ) || $this->only === 'styles'; } - + public function shouldIncludeMessages() { return is_null( $this->only ) || $this->only === 'messages'; } - + public function getHash() { return isset( $this->hash ) ? $this->hash : $this->hash = implode( '|', array( $this->language, $this->skin, $this->debug ) ); } -} \ No newline at end of file +} diff --git a/includes/ResourceLoaderModule.php b/includes/ResourceLoaderModule.php index b0b509d781..febaeb7ad6 100644 --- a/includes/ResourceLoaderModule.php +++ b/includes/ResourceLoaderModule.php @@ -14,7 +14,7 @@ * with this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * http://www.gnu.org/copyleft/gpl.html - * + * * @author Trevor Parscal * @author Roan Kattouw */ @@ -23,13 +23,12 @@ * Interface for resource loader modules, with name registration and maxage functionality. */ abstract class ResourceLoaderModule { - /* Protected Members */ - + protected $name = null; - + /* Methods */ - + /** * Get this module's name. This is set when the module is registered * with ResourceLoader::register() @@ -38,7 +37,7 @@ abstract class ResourceLoaderModule { public function getName() { return $this->name; } - + /** * Set this module's name. This is called by ResourceLodaer::register() * when registering the module. Other code should not call this. @@ -47,7 +46,7 @@ abstract class ResourceLoaderModule { public function setName( $name ) { $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 @@ -58,7 +57,7 @@ abstract class ResourceLoaderModule { 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 @@ -76,9 +75,9 @@ abstract class ResourceLoaderModule { public function getFlip( $context ) { return $context->getDirection() === 'rtl'; } - + /* Abstract Methods */ - + /** * Get all JS for this module for a given language and skin. * Includes all relevant JS except loader scripts. @@ -88,14 +87,14 @@ abstract class ResourceLoaderModule { * @return string JS */ public abstract function getScript( ResourceLoaderContext $context ); - + /** * Get all CSS for this module for a given skin. * @param $skin string Skin name * @return string CSS */ public abstract function getStyle( ResourceLoaderContext $context ); - + /** * Get the messages needed for this module. * @@ -103,13 +102,13 @@ abstract class ResourceLoaderModule { * @return array of message keys. Keys may occur more than once */ public abstract function getMessages(); - + /** * Get the loader JS for this module, if set. * @return mixed Loader JS (string) or false if no custom loader set */ public abstract function getLoaderScript(); - + /** * Get a list of modules this module depends on. * @@ -126,7 +125,7 @@ abstract class ResourceLoaderModule { * @return array Array of module names (strings) */ public abstract function getDependencies(); - + /** * Get this module's last modification timestamp for a given * combination of language, skin and debug mode flag. This is typically @@ -145,9 +144,8 @@ abstract class ResourceLoaderModule { * Module based on local JS/CSS files. This is the most common type of module. */ class ResourceLoaderFileModule extends ResourceLoaderModule { - /* Protected Members */ - + protected $scripts = array(); protected $styles = array(); protected $messages = array(); @@ -158,24 +156,24 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected $skinStyles = array(); protected $loaders = array(); protected $parameters = array(); - + // In-object cache for file dependencies protected $fileDeps = array(); // In-object cache for mtime protected $modifiedTime = array(); - + /* Methods */ - + /** * Construct a new module from an options array. * * @param $options array Options array. If empty, an empty module will be constructed - * + * * $options format: * 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' ... ) @@ -183,7 +181,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * ), * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ), * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ), - * + * * // Non-raw module options * 'dependencies' => 'module' | array( 'module1', 'module2' ... ) * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ), @@ -228,7 +226,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { } } } - + /** * Add script files to this module. In order to be valid, a module * must contain at least one script file. @@ -237,7 +235,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { public function addScripts( $scripts ) { $this->scripts = array_merge( $this->scripts, (array)$scripts ); } - + /** * Add style (CSS) files to this module. * @param $styles mixed Path to CSS file (string) or array of paths @@ -245,7 +243,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { public function addStyles( $styles ) { $this->styles = array_merge( $this->styles, (array)$styles ); } - + /** * Add messages to this module. * @param $messages mixed Message key (string) or array of message keys @@ -253,7 +251,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { public function addMessages( $messages ) { $this->messages = array_merge( $this->messages, (array)$messages ); } - + /** * Add dependencies. Dependency information is taken into account when * loading a module on the client side. When adding a module on the @@ -271,7 +269,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { public function addDependencies( $dependencies ) { $this->dependencies = array_merge( $this->dependencies, (array)$dependencies ); } - + /** * Add debug scripts to the module. These scripts are only included * in debug mode. @@ -280,7 +278,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { public function addDebugScripts( $scripts ) { $this->debugScripts = array_merge( $this->debugScripts, (array)$scripts ); } - + /** * Add language-specific scripts. These scripts are only included for * a given language. @@ -306,7 +304,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { array( $skin => $scripts ) ); } - + /** * Add skin-specific CSS. These CSS files are only included for a * given skin. If there are no skin-specific CSS files for a skin, @@ -320,7 +318,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { array( $skin => $scripts ) ); } - + /** * Add loader scripts. These scripts are loaded on every page and are * responsible for registering this module using @@ -337,23 +335,25 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { public function addLoaders( $scripts ) { $this->loaders = array_merge( $this->loaders, (array)$scripts ); } - + public function getScript( ResourceLoaderContext $context ) { $retval = $this->getPrimaryScript() . "\n" . $this->getLanguageScript( $context->getLanguage() ) . "\n" . $this->getSkinScript( $context->getSkin() ); + if ( $context->getDebug() ) { $retval .= $this->getDebugScript(); } + return $retval; } - + public function getStyle( ResourceLoaderContext $context ) { $style = $this->getPrimaryStyle() . "\n" . $this->getSkinStyle( $context->getSkin() ); - + // Extract and store the list of referenced files $files = CSSMin::getLocalFileReferences( $style ); - + // Only store if modified if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) { $encFiles = FormatJson::encode( $files ); @@ -365,30 +365,33 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { 'md_deps' => $encFiles, ) ); - + // Save into memcached global $wgMemc; + $key = wfMemcKey( 'resourceloader', 'module_deps', $this->getName(), $context->getSkin() ); $wgMemc->set( $key, $encFiles ); } + return $style; } - + public function getMessages() { return $this->messages; } - + public function getDependencies() { return $this->dependencies; } - + public function getLoaderScript() { if ( count( $this->loaders ) == 0 ) { return false; } + return self::concatScripts( $this->loaders ); } - + /** * Get the last modified timestamp of this module, which is calculated * as the highest last modified timestamp of its constituent files and @@ -405,7 +408,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { if ( isset( $this->modifiedTime[$context->getHash()] ) ) { return $this->modifiedTime[$context->getHash()]; } - + $files = array_merge( $this->scripts, $this->styles, @@ -417,8 +420,9 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { $this->loaders, $this->getFileDependencies( $context->getSkin() ) ); + $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 ); @@ -428,13 +432,13 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { ), __METHOD__ ); $msgBlobMtime = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0; - + $this->modifiedTime[$context->getHash()] = max( $filesMtime, $msgBlobMtime ); return $this->modifiedTime[$context->getHash()]; } - + /* Protected Members */ - + /** * Get the primary JS for this module. This is pulled from the * script files added through addScripts() @@ -443,7 +447,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected function getPrimaryScript() { return self::concatScripts( $this->scripts ); } - + /** * Get the primary CSS for this module. This is pulled from the CSS * files added through addStyles() @@ -452,7 +456,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected function getPrimaryStyle() { return self::concatStyles( $this->styles ); } - + /** * Get the debug JS for this module. This is pulled from the script * files added through addDebugScripts() @@ -461,7 +465,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected function getDebugScript() { return self::concatScripts( $this->debugScripts ); } - + /** * Get the language-specific JS for a given language. This is pulled * from the language-specific script files added through addLanguageScripts() @@ -473,7 +477,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { } return self::concatScripts( $this->languageScripts[$lang] ); } - + /** * Get the skin-specific JS for a given skin. This is pulled from the * skin-specific JS files added through addSkinScripts() @@ -482,7 +486,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected function getSkinScript( $skin ) { return self::concatScripts( self::getSkinFiles( $skin, $this->skinScripts ) ); } - + /** * Get the skin-specific CSS for a given skin. This is pulled from the * skin-specific CSS files added through addSkinStyles() @@ -491,7 +495,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected function getSkinStyle( $skin ) { return self::concatStyles( self::getSkinFiles( $skin, $this->skinStyles ) ); } - + /** * Helper function to get skin-specific data from an array. * @param $skin string Skin name @@ -500,14 +504,16 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { */ protected static function getSkinFiles( $skin, $map ) { $retval = array(); + if ( isset( $map[$skin] ) && $map[$skin] ) { $retval = $map[$skin]; } else if ( isset( $map['default'] ) ) { $retval = $map['default']; } + 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. @@ -519,12 +525,13 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { 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( @@ -537,10 +544,12 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { } $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. @@ -550,7 +559,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected static function concatScripts( $files ) { return implode( "\n", array_map( 'file_get_contents', array_map( array( __CLASS__, 'remapFilename' ), array_unique( (array) $files ) ) ) ); } - + /** * Get the contents of a set of CSS files, remap then and concatenate * them, with newlines in between. Each file is used only once. @@ -560,7 +569,7 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { protected static function concatStyles( $files ) { return implode( "\n", array_map( array( __CLASS__, 'remapStyle' ), array_unique( (array) $files ) ) ); } - + /** * Remap a relative to $IP. Used as a callback for array_map() * @param $file string File name @@ -568,9 +577,10 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { */ protected static function remapFilename( $file ) { global $IP; + return "$IP/$file"; } - + /** * Get the contents of a CSS file and run it through CSSMin::remap(). * This wrapper is needed so we can use array_map() in concatStyles() @@ -587,44 +597,45 @@ class ResourceLoaderFileModule extends ResourceLoaderModule { * TODO: Add Site CSS functionality too */ class ResourceLoaderSiteModule extends ResourceLoaderModule { - /* Protected Members */ - + // In-object cache for modified time protected $modifiedTime = null; - + /* Methods */ - + public function getScript( ResourceLoaderContext $context ) { return Skin::newFromKey( $context->getSkin() )->generateUserJs(); } - + public function getModifiedTime( ResourceLoaderContext $context ) { if ( isset( $this->modifiedTime[$context->getHash()] ) ) { return $this->modifiedTime[$context->getHash()]; } - + // HACK: We duplicate the message names from generateUserJs() // here and weird things (i.e. mtime moving backwards) can happen // when a MediaWiki:Something.js page is deleted $jsPages = array( Title::makeTitle( NS_MEDIAWIKI, 'Common.js' ), Title::makeTitle( NS_MEDIAWIKI, ucfirst( $context->getSkin() ) . '.js' ) ); - + // Do batch existence check // TODO: This would work better if page_touched were loaded by this as well $lb = new LinkBatch( $jsPages ); $lb->execute(); - + $this->modifiedTime = 1; // wfTimestamp() interprets 0 as "now" + foreach ( $jsPages as $jsPage ) { if ( $jsPage->exists() ) { $this->modifiedTime = max( $this->modifiedTime, wfTimestamp( TS_UNIX, $jsPage->getTouched() ) ); } } + return $this->modifiedTime; } - + public function getStyle( ResourceLoaderContext $context ) { return ''; } public function getMessages() { return array(); } public function getLoaderScript() { return ''; } @@ -633,17 +644,17 @@ class ResourceLoaderSiteModule extends ResourceLoaderModule { class ResourceLoaderStartUpModule extends ResourceLoaderModule { - /* Protected Members */ - + protected $modifiedTime = null; - + /* Methods */ - + public function getScript( ResourceLoaderContext $context ) { global $IP; - + $scripts = file_get_contents( "$IP/resources/startup.js" ); + if ( $context->getOnly() === 'scripts' ) { // Get all module registrations $registration = ResourceLoader::getModuleRegistrations( $context ); @@ -668,6 +679,7 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { ), -2 ) ) ) ); + // Build HTML code for loading jquery and mediawiki modules $loadScript = Html::linkedScript( $context->getServer() . "?$query" ); // Add code to add jquery and mediawiki loading code; only if the current client is compatible @@ -675,34 +687,39 @@ class ResourceLoaderStartUpModule extends ResourceLoaderModule { // Delete the compatible function - it's not needed anymore $scripts .= "delete window['isCompatible'];"; } + return $scripts; } - + public function getModifiedTime( ResourceLoaderContext $context ) { global $IP; + if ( !is_null( $this->modifiedTime ) ) { return $this->modifiedTime; } + // HACK getHighestModifiedTime() calls this function, so protect against infinite recursion $this->modifiedTime = filemtime( "$IP/resources/startup.js" ); $this->modifiedTime = ResourceLoader::getHighestModifiedTime( $context ); return $this->modifiedTime; } - + public function getClientMaxage() { return 300; // 5 minutes } - + public function getServerMaxage() { return 300; // 5 minutes } - + public function getStyle( ResourceLoaderContext $context ) { return ''; } + public function getFlip( $context ) { global $wgContLang; + return $wgContLang->getDir() !== $context->getDirection(); } public function getMessages() { return array(); } public function getLoaderScript() { return ''; } public function getDependencies() { return array(); } -} \ No newline at end of file +} diff --git a/load.php b/load.php index 9affd3d037..a17e56df65 100644 --- a/load.php +++ b/load.php @@ -22,7 +22,7 @@ * @author Trevor Parscal * */ - + require ( dirname( __FILE__ ) . '/includes/WebStart.php' ); wfProfileIn( 'load.php' ); @@ -51,4 +51,4 @@ wfProfileOut( 'load.php' ); wfLogProfilingData(); // Shut down the database -wfGetLBFactory()->shutdown(); \ No newline at end of file +wfGetLBFactory()->shutdown(); diff --git a/maintenance/tests/ResourceLoaderFileModuleTest.php b/maintenance/tests/ResourceLoaderFileModuleTest.php index 5f5447ce92..5ad7d9373d 100644 --- a/maintenance/tests/ResourceLoaderFileModuleTest.php +++ b/maintenance/tests/ResourceLoaderFileModuleTest.php @@ -1,16 +1,15 @@