/**
* 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
'lookbehind_not_letter' => '(?<![a-zA-Z])',
'chars_within_selector' => '[^\}]*?',
'noflip_annotation' => '\/\*\s*@noflip\s*\*\/',
-
'noflip_single' => null,
'noflip_class' => null,
'comment' => '/\/\*[^*]*\*+([^\/*][^*]*\*+)*\//',
'bg_horizontal_percentage' => null,
'bg_horizontal_percentage_x' => null,
);
-
+
/**
* Build patterns we can't define above because they depend on other patterns.
*/
// 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']})";
$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";
$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
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 );
}
$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
'$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
*/
$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
*/
$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.
*/
$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;
*/
'$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
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
class CSSJanus_Tokenizer {
private $regex, $token;
private $originals;
-
+
/**
* Constructor
* @param $regex string Regular expression whose matches to replace by a token.
$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.
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
+}
<?php
class CSSMin {
-
/* Constants */
-
+
/**
* Maximum file size to still qualify for in-line embedding as a data-URI
- *
+ *
* 24,576 is used because Internet Explorer has a 32,768 byte limit for data URIs, which when base64 encoded will
* result in a 1/3 increase in size.
*/
const EMBED_SIZE_LIMIT = 24576;
-
+
/* Static Methods */
-
+
/**
* Gets a list of local file paths which are referenced in a CSS style sheet
- *
+ *
* @param $source string CSS data to remap
* @param $path string File path where the source was read from (optional)
* @return array List of local file references
public static function getLocalFileReferences( $source, $path = null ) {
$pattern = '/url\([\'"]?(?<file>[^\?\)\:]*)\??[^\)]*[\'"]?\)/';
$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 = '/((?<embed>\s*\/\*\s*\@embed\s*\*\/)(?<rule>[^\;\}]*))?url\([\'"]?(?<file>[^\?\)\:]*)\??[^\)]*[\'"]?\)(?<extra>[^;]*)[\;]?/';
$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
// 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
// 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(
)
);
}
-}
\ No newline at end of file
+}
}
// 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 ) {
$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
*/
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,
__METHOD__,
array( 'IGNORE' )
);
+
if ( $success ) {
if ( $dbw->affectedRows() == 0 ) {
// Blob was already present, fetch it
// Update msg_resource_links
$rows = array();
$mod = ResourceLoader::getModule( $module );
+
foreach ( $mod->getMessages() as $key ) {
$rows[] = array(
'mrl_resource' => $module,
);
}
}
+
return $blob;
}
-
+
/**
* Update all message blobs for a given module.
* @param $module string Module name
*/
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;
}
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(
), __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
'mr_timestamp' => $update['timestamp'] ),
__METHOD__
);
-
+
// Only requeue conflicted updates.
// If update() returned false, don't retry, for
// fear of getting into an infinite loop
}
}
} 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?
$dbw->delete( 'msg_resource', '*', __METHOD__ );
$dbw->delete( 'msg_resource_links', '*', __METHOD__ );
}
-
+
/**
* Create an update queue for updateMessage()
* @param $key string Message key
*/
private static function getUpdatesForMessage( $key, $prevUpdates = null ) {
$dbw = wfGetDb( DB_MASTER );
+
if ( is_null( $prevUpdates ) ) {
// Fetch all blobs referencing $key
$res = $dbw->select(
);
} 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,
$key, $row->mr_lang )
);
}
+
return $updates;
}
-
+
/**
* Reencode a message blob with the updated value for a message
* @param $blob string Message blob (JSON object)
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.
array( 'mr_resource' => $modules, 'mr_lang' => $lang ),
__METHOD__
);
+
foreach ( $res as $row ) {
$module = ResourceLoader::getModule( $row->mr_resource );
if ( !$module ) {
$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
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 );
}
}
* 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
*
*/
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 ) {
} 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
*/
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
self::$modules[$name] = $object;
$object->setName( $name );
}
-
+
/**
* Gets a map of all modules and their options
*
public static function getModules() {
return self::$modules;
}
-
+
/**
* Get the ResourceLoaderModule object for a given module name
* @param $name string Module name
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
*
public static function getModuleRegistrations( ResourceLoaderContext $context ) {
$scripts = '';
$registrations = array();
+
foreach ( self::$modules as $name => $module ) {
// Support module loader scripts
if ( ( $loader = $module->getLoaderScript() ) !== false ) {
}
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.
*/
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
*
// 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;
$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 ) ) !== ''
}
$styles = $context->getDebug() ? $styles : self::filter( 'minify-css', $styles );
}
+
// Messages
$messages = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
+
// Output
if ( $context->getOnly() === 'styles' ) {
echo $styles;
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() ) {
}
// FIXME: Temp hack
-require_once "$IP/resources/Resources.php";
\ No newline at end of file
+require_once "$IP/resources/Resources.php";
* 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;
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
$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;
}
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
+}
* 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
*/
* 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()
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.
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
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
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.
* @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.
*
* @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.
*
* @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
* 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();
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' ... )
* ),
* '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' ... ),
}
}
}
-
+
/**
* Add script files to this module. In order to be valid, a module
* must contain at least one script file.
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
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
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
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.
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.
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,
array( $skin => $scripts )
);
}
-
+
/**
* Add loader scripts. These scripts are loaded on every page and are
* responsible for registering this module using
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 );
'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
if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
return $this->modifiedTime[$context->getHash()];
}
-
+
$files = array_merge(
$this->scripts,
$this->styles,
$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 );
), __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()
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()
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()
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()
}
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()
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()
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
*/
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.
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(
}
$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.
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.
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
*/
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()
* 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 ''; }
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 );
), -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
// 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
+}
* @author Trevor Parscal
*
*/
-
+
require ( dirname( __FILE__ ) . '/includes/WebStart.php' );
wfProfileIn( 'load.php' );
wfLogProfilingData();
// Shut down the database
-wfGetLBFactory()->shutdown();
\ No newline at end of file
+wfGetLBFactory()->shutdown();
<?php
class ResourceLoaderFileModuleTest extends PHPUnit_Framework_TestCase {
-
/* Provider Methods */
-
+
public function provide() {
-
+
}
-
+
/* Test Methods */
-
+
public function test() {
-
+
}
-}
\ No newline at end of file
+}
<?php
class ResourceLoaderTest extends PHPUnit_Framework_TestCase {
-
/* Provider Methods */
-
+
public function provide() {
-
+
}
-
+
/* Test Methods */
-
+
public function test() {
-
+
}
-}
\ No newline at end of file
+}