r75021 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r75020‎ | r75021 | r75022 >
Date:18:21, 19 October 2010
Author:tparscal
Status:ok
Tags:
Comment:
Moved ResourceLoader classes to their own folder, preparing to also split ResourceLoaderModule.php into multiple files (it's getting a bit long now)
Modified paths:
  • /trunk/phase3/includes/AutoLoader.php (modified) (history)
  • /trunk/phase3/includes/ResourceLoader.php (deleted) (history)
  • /trunk/phase3/includes/ResourceLoaderContext.php (deleted) (history)
  • /trunk/phase3/includes/ResourceLoaderModule.php (deleted) (history)
  • /trunk/phase3/includes/resourceloader (added) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoader.php (added) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoaderContext.php (added) (history)
  • /trunk/phase3/includes/resourceloader/ResourceLoaderModule.php (added) (history)

Diff [purge]

Index: trunk/phase3/includes/ResourceLoaderContext.php
@@ -1,138 +0,0 @@
2 -<?php
3 -/**
4 - * This program is free software; you can redistribute it and/or modify
5 - * it under the terms of the GNU General Public License as published by
6 - * the Free Software Foundation; either version 2 of the License, or
7 - * (at your option) any later version.
8 - *
9 - * This program is distributed in the hope that it will be useful,
10 - * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 - * GNU General Public License for more details.
13 - *
14 - * You should have received a copy of the GNU General Public License along
15 - * with this program; if not, write to the Free Software Foundation, Inc.,
16 - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 - * http://www.gnu.org/copyleft/gpl.html
18 - *
19 - * @file
20 - * @author Trevor Parscal
21 - * @author Roan Kattouw
22 - */
23 -
24 -defined( 'MEDIAWIKI' ) || die( 1 );
25 -
26 -/**
27 - * Object passed around to modules which contains information about the state
28 - * of a specific loader request
29 - */
30 -class ResourceLoaderContext {
31 -
32 - /* Protected Members */
33 -
34 - protected $resourceLoader;
35 - protected $request;
36 - protected $modules;
37 - protected $language;
38 - protected $direction;
39 - protected $skin;
40 - protected $user;
41 - protected $debug;
42 - protected $only;
43 - protected $version;
44 - protected $hash;
45 -
46 - /* Methods */
47 -
48 - public function __construct( ResourceLoader $resourceLoader, WebRequest $request ) {
49 - global $wgLang, $wgDefaultSkin, $wgResourceLoaderDebug;
50 -
51 - $this->resourceLoader = $resourceLoader;
52 - $this->request = $request;
53 - // Interpret request
54 - $modules = $request->getVal( 'modules' );
55 - $this->modules = $modules ? explode( '|', $modules ) : array();
56 - $this->language = $request->getVal( 'lang' );
57 - $this->direction = $request->getVal( 'dir' );
58 - $this->skin = $request->getVal( 'skin' );
59 - $this->user = $request->getVal( 'user' );
60 - $this->debug = $request->getFuzzyBool( 'debug', $wgResourceLoaderDebug );
61 - $this->only = $request->getVal( 'only' );
62 - $this->version = $request->getVal( 'version' );
63 -
64 - // Fallback on system defaults
65 - if ( !$this->language ) {
66 - $this->language = $wgLang->getCode();
67 - }
68 -
69 - if ( !$this->direction ) {
70 - $this->direction = Language::factory( $this->language )->getDir();
71 - }
72 -
73 - if ( !$this->skin ) {
74 - $this->skin = $wgDefaultSkin;
75 - }
76 - }
77 -
78 - public function getResourceLoader() {
79 - return $this->resourceLoader;
80 - }
81 -
82 - public function getRequest() {
83 - return $this->request;
84 - }
85 -
86 - public function getModules() {
87 - return $this->modules;
88 - }
89 -
90 - public function getLanguage() {
91 - return $this->language;
92 - }
93 -
94 - public function getDirection() {
95 - return $this->direction;
96 - }
97 -
98 - public function getSkin() {
99 - return $this->skin;
100 - }
101 -
102 - public function getUser() {
103 - return $this->user;
104 - }
105 -
106 - public function getDebug() {
107 - return $this->debug;
108 - }
109 -
110 - public function getOnly() {
111 - return $this->only;
112 - }
113 -
114 - public function getVersion() {
115 - return $this->version;
116 - }
117 -
118 - public function shouldIncludeScripts() {
119 - return is_null( $this->only ) || $this->only === 'scripts';
120 - }
121 -
122 - public function shouldIncludeStyles() {
123 - return is_null( $this->only ) || $this->only === 'styles';
124 - }
125 -
126 - public function shouldIncludeMessages() {
127 - return is_null( $this->only ) || $this->only === 'messages';
128 - }
129 -
130 - public function getHash() {
131 - if ( isset( $this->hash ) ) {
132 - $this->hash = implode( '|', array(
133 - $this->language, $this->direction, $this->skin, $this->user,
134 - $this->debug, $this->only, $this->version
135 - ) );
136 - }
137 - return $this->hash;
138 - }
139 -}
Index: trunk/phase3/includes/ResourceLoader.php
@@ -1,465 +0,0 @@
2 -<?php
3 -/**
4 - * This program is free software; you can redistribute it and/or modify
5 - * it under the terms of the GNU General Public License as published by
6 - * the Free Software Foundation; either version 2 of the License, or
7 - * (at your option) any later version.
8 - *
9 - * This program is distributed in the hope that it will be useful,
10 - * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 - * GNU General Public License for more details.
13 - *
14 - * You should have received a copy of the GNU General Public License along
15 - * with this program; if not, write to the Free Software Foundation, Inc.,
16 - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 - * http://www.gnu.org/copyleft/gpl.html
18 - *
19 - * @file
20 - * @author Roan Kattouw
21 - * @author Trevor Parscal
22 - */
23 -
24 -defined( 'MEDIAWIKI' ) || die( 1 );
25 -
26 -/**
27 - * Dynamic JavaScript and CSS resource loading system
28 - */
29 -class ResourceLoader {
30 -
31 - /* Protected Static Members */
32 -
33 - // @var array list of module name/ResourceLoaderModule object pairs
34 - protected $modules = array();
35 -
36 - /* Protected Methods */
37 -
38 - /**
39 - * Loads information stored in the database about modules
40 - *
41 - * This is not inside the module code because it's so much more performant to request all of the information at once
42 - * than it is to have each module requests it's own information.
43 - *
44 - * @param $modules array list of module names to preload information for
45 - * @param $context ResourceLoaderContext context to load the information within
46 - */
47 - protected function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
48 - if ( !count( $modules ) ) {
49 - return; # or Database*::select() will explode
50 - }
51 - $dbr = wfGetDb( DB_SLAVE );
52 - $skin = $context->getSkin();
53 - $lang = $context->getLanguage();
54 -
55 - // Get file dependency information
56 - $res = $dbr->select( 'module_deps', array( 'md_module', 'md_deps' ), array(
57 - 'md_module' => $modules,
58 - 'md_skin' => $context->getSkin()
59 - ), __METHOD__
60 - );
61 -
62 - $modulesWithDeps = array();
63 - foreach ( $res as $row ) {
64 - $this->modules[$row->md_module]->setFileDependencies( $skin,
65 - FormatJson::decode( $row->md_deps, true )
66 - );
67 - $modulesWithDeps[] = $row->md_module;
68 - }
69 - // Register the absence of a dependencies row too
70 - foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
71 - $this->modules[$name]->setFileDependencies( $skin, array() );
72 - }
73 -
74 - // Get message blob mtimes. Only do this for modules with messages
75 - $modulesWithMessages = array();
76 - $modulesWithoutMessages = array();
77 - foreach ( $modules as $name ) {
78 - if ( count( $this->modules[$name]->getMessages() ) ) {
79 - $modulesWithMessages[] = $name;
80 - } else {
81 - $modulesWithoutMessages[] = $name;
82 - }
83 - }
84 - if ( count( $modulesWithMessages ) ) {
85 - $res = $dbr->select( 'msg_resource', array( 'mr_resource', 'mr_timestamp' ), array(
86 - 'mr_resource' => $modulesWithMessages,
87 - 'mr_lang' => $lang
88 - ), __METHOD__
89 - );
90 - foreach ( $res as $row ) {
91 - $this->modules[$row->mr_resource]->setMsgBlobMtime( $lang, $row->mr_timestamp );
92 - }
93 - }
94 - foreach ( $modulesWithoutMessages as $name ) {
95 - $this->modules[$name]->setMsgBlobMtime( $lang, 0 );
96 - }
97 - }
98 -
99 - /**
100 - * Runs text through a filter, caching the filtered result for future calls
101 - *
102 - * @param $filter String: name of filter to run
103 - * @param $data String: text to filter, such as JavaScript or CSS text
104 - * @param $file String: path to file being filtered, (optional: only required for CSS to resolve paths)
105 - * @return String: filtered data
106 - */
107 - protected function filter( $filter, $data ) {
108 - global $wgMemc;
109 - wfProfileIn( __METHOD__ );
110 -
111 - // For empty or whitespace-only things, don't do any processing
112 - if ( trim( $data ) === '' ) {
113 - wfProfileOut( __METHOD__ );
114 - return $data;
115 - }
116 -
117 - // Try memcached
118 - $key = wfMemcKey( 'resourceloader', 'filter', $filter, md5( $data ) );
119 - $cached = $wgMemc->get( $key );
120 -
121 - if ( $cached !== false && $cached !== null ) {
122 - wfProfileOut( __METHOD__ );
123 - return $cached;
124 - }
125 -
126 - // Run the filter
127 - try {
128 - switch ( $filter ) {
129 - case 'minify-js':
130 - $result = JSMin::minify( $data );
131 - break;
132 - case 'minify-css':
133 - $result = CSSMin::minify( $data );
134 - break;
135 - case 'flip-css':
136 - $result = CSSJanus::transform( $data, true, false );
137 - break;
138 - default:
139 - // Don't cache anything, just pass right through
140 - wfProfileOut( __METHOD__ );
141 - return $data;
142 - }
143 - } catch ( Exception $exception ) {
144 - throw new MWException( 'Filter threw an exception: ' . $exception->getMessage() );
145 - }
146 -
147 - // Save to memcached
148 - $wgMemc->set( $key, $result );
149 -
150 - wfProfileOut( __METHOD__ );
151 - return $result;
152 - }
153 -
154 - /* Methods */
155 -
156 - /**
157 - * Registers core modules and runs registration hooks
158 - */
159 - public function __construct() {
160 - global $IP;
161 -
162 - wfProfileIn( __METHOD__ );
163 -
164 - // Register core modules
165 - $this->register( include( "$IP/resources/Resources.php" ) );
166 - // Register extension modules
167 - wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
168 -
169 - wfProfileOut( __METHOD__ );
170 - }
171 -
172 - /**
173 - * Registers a module with the ResourceLoader system.
174 - *
175 - * Note that registering the same object under multiple names is not supported
176 - * and may silently fail in all kinds of interesting ways.
177 - *
178 - * @param $name Mixed: string of name of module or array of name/object pairs
179 - * @param $object ResourceLoaderModule: module object (optional when using
180 - * multiple-registration calling style)
181 - * @return Boolean: false if there were any errors, in which case one or more
182 - * modules were not registered
183 - *
184 - * @todo We need much more clever error reporting, not just in detailing what
185 - * happened, but in bringing errors to the client in a way that they can
186 - * easily see them if they want to, such as by using FireBug
187 - */
188 - public function register( $name, ResourceLoaderModule $object = null ) {
189 - wfProfileIn( __METHOD__ );
190 -
191 - // Allow multiple modules to be registered in one call
192 - if ( is_array( $name ) && !isset( $object ) ) {
193 - foreach ( $name as $key => $value ) {
194 - $this->register( $key, $value );
195 - }
196 -
197 - wfProfileOut( __METHOD__ );
198 - return;
199 - }
200 -
201 - // Disallow duplicate registrations
202 - if ( isset( $this->modules[$name] ) ) {
203 - // A module has already been registered by this name
204 - throw new MWException( 'Another module has already been registered as ' . $name );
205 - }
206 -
207 - // Validate the input (type hinting lets null through)
208 - if ( !( $object instanceof ResourceLoaderModule ) ) {
209 - throw new MWException( 'Invalid ResourceLoader module error. Instances of ResourceLoaderModule expected.' );
210 - }
211 -
212 - // Attach module
213 - $this->modules[$name] = $object;
214 - $object->setName( $name );
215 -
216 - wfProfileOut( __METHOD__ );
217 - }
218 -
219 - /**
220 - * Gets a map of all modules and their options
221 - *
222 - * @return Array: array( modulename => ResourceLoaderModule )
223 - */
224 - public function getModules() {
225 - return $this->modules;
226 - }
227 -
228 - /**
229 - * Get the ResourceLoaderModule object for a given module name
230 - *
231 - * @param $name String: module name
232 - * @return mixed ResourceLoaderModule or null if not registered
233 - */
234 - public function getModule( $name ) {
235 - return isset( $this->modules[$name] ) ? $this->modules[$name] : null;
236 - }
237 -
238 - /**
239 - * Outputs a response to a resource load-request, including a content-type header
240 - *
241 - * @param $context ResourceLoaderContext object
242 - */
243 - public function respond( ResourceLoaderContext $context ) {
244 - global $wgResourceLoaderMaxage, $wgCacheEpoch;
245 -
246 - wfProfileIn( __METHOD__ );
247 -
248 - // Split requested modules into two groups, modules and missing
249 - $modules = array();
250 - $missing = array();
251 -
252 - foreach ( $context->getModules() as $name ) {
253 - if ( isset( $this->modules[$name] ) ) {
254 - $modules[$name] = $this->modules[$name];
255 - } else {
256 - $missing[] = $name;
257 - }
258 - }
259 -
260 - // If a version wasn't specified we need a shorter expiry time for updates to
261 - // propagate to clients quickly
262 - if ( is_null( $context->getVersion() ) ) {
263 - $maxage = $wgResourceLoaderMaxage['unversioned']['client'];
264 - $smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
265 - }
266 - // If a version was specified we can use a longer expiry time since changing
267 - // version numbers causes cache misses
268 - else {
269 - $maxage = $wgResourceLoaderMaxage['versioned']['client'];
270 - $smaxage = $wgResourceLoaderMaxage['versioned']['server'];
271 - }
272 -
273 - // Preload information needed to the mtime calculation below
274 - $this->preloadModuleInfo( array_keys( $modules ), $context );
275 -
276 - // To send Last-Modified and support If-Modified-Since, we need to detect
277 - // the last modified time
278 - wfProfileIn( __METHOD__.'-getModifiedTime' );
279 - $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
280 - foreach ( $modules as $module ) {
281 - // Bypass squid cache if the request includes any private modules
282 - if ( $module->getGroup() === 'private' ) {
283 - $smaxage = 0;
284 - }
285 - // Calculate maximum modified time
286 - $mtime = max( $mtime, $module->getModifiedTime( $context ) );
287 - }
288 - wfProfileOut( __METHOD__.'-getModifiedTime' );
289 -
290 - header( 'Content-Type: ' . ( $context->getOnly() === 'styles' ? 'text/css' : 'text/javascript' ) );
291 - header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) );
292 - header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
293 - header( 'Expires: ' . wfTimestamp( TS_RFC2822, min( $maxage, $smaxage ) + time() ) );
294 -
295 - // If there's an If-Modified-Since header, respond with a 304 appropriately
296 - $ims = $context->getRequest()->getHeader( 'If-Modified-Since' );
297 - if ( $ims !== false && $mtime <= wfTimestamp( TS_UNIX, $ims ) ) {
298 - header( 'HTTP/1.0 304 Not Modified' );
299 - header( 'Status: 304 Not Modified' );
300 - wfProfileOut( __METHOD__ );
301 - return;
302 - }
303 -
304 - $response = $this->makeModuleResponse( $context, $modules, $missing );
305 - if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
306 - $response .= "/*\n$warnings\n*/";
307 - }
308 - // Clear any warnings from the buffer
309 - ob_clean();
310 - echo $response;
311 -
312 - wfProfileOut( __METHOD__ );
313 - }
314 -
315 - public function makeModuleResponse( ResourceLoaderContext $context, array $modules, $missing = null ) {
316 - // Pre-fetch blobs
317 - $blobs = $context->shouldIncludeMessages() ?
318 - MessageBlobStore::get( $this, $modules, $context->getLanguage() ) : array();
319 -
320 - // Generate output
321 - $out = '';
322 - foreach ( $modules as $name => $module ) {
323 - wfProfileIn( __METHOD__ . '-' . $name );
324 -
325 - // Scripts
326 - $scripts = '';
327 - if ( $context->shouldIncludeScripts() ) {
328 - $scripts .= $module->getScript( $context ) . "\n";
329 - }
330 -
331 - // Styles
332 - $styles = array();
333 - if ( $context->shouldIncludeStyles() && ( count( $styles = $module->getStyles( $context ) ) ) ) {
334 - // Flip CSS on a per-module basis
335 - if ( $this->modules[$name]->getFlip( $context ) ) {
336 - foreach ( $styles as $media => $style ) {
337 - $styles[$media] = $this->filter( 'flip-css', $style );
338 - }
339 - }
340 - }
341 -
342 - // Messages
343 - $messages = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
344 -
345 - // Append output
346 - switch ( $context->getOnly() ) {
347 - case 'scripts':
348 - $out .= $scripts;
349 - break;
350 - case 'styles':
351 - $out .= self::makeCombinedStyles( $styles );
352 - break;
353 - case 'messages':
354 - $out .= self::makeMessageSetScript( $messages );
355 - break;
356 - default:
357 - // Minify CSS before embedding in mediaWiki.loader.implement call (unless in debug mode)
358 - if ( !$context->getDebug() ) {
359 - foreach ( $styles as $media => $style ) {
360 - $styles[$media] = $this->filter( 'minify-css', $style );
361 - }
362 - }
363 - $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, $messages );
364 - break;
365 - }
366 -
367 - wfProfileOut( __METHOD__ . '-' . $name );
368 - }
369 -
370 - // Update module states
371 - if ( $context->shouldIncludeScripts() ) {
372 - // Set the state of modules loaded as only scripts to ready
373 - if ( count( $modules ) && $context->getOnly() === 'scripts' && !isset( $modules['startup'] ) ) {
374 - $out .= self::makeLoaderStateScript( array_fill_keys( array_keys( $modules ), 'ready' ) );
375 - }
376 - // Set the state of modules which were requested but unavailable as missing
377 - if ( is_array( $missing ) && count( $missing ) ) {
378 - $out .= self::makeLoaderStateScript( array_fill_keys( $missing, 'missing' ) );
379 - }
380 - }
381 -
382 - if ( $context->getDebug() ) {
383 - return $out;
384 - } else {
385 - if ( $context->getOnly() === 'styles' ) {
386 - return $this->filter( 'minify-css', $out );
387 - } else {
388 - return $this->filter( 'minify-js', $out );
389 - }
390 - }
391 - }
392 -
393 - /* Static Methods */
394 -
395 - public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
396 - if ( is_array( $scripts ) ) {
397 - $scripts = implode( $scripts, "\n" );
398 - }
399 - if ( is_array( $styles ) ) {
400 - $styles = count( $styles ) ? FormatJson::encode( $styles ) : 'null';
401 - }
402 - if ( is_array( $messages ) ) {
403 - $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null';
404 - }
405 - return "mediaWiki.loader.implement( '$name', function() {{$scripts}},\n$styles,\n$messages );\n";
406 - }
407 -
408 - public static function makeMessageSetScript( $messages ) {
409 - if ( is_array( $messages ) ) {
410 - $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null';
411 - }
412 - return "mediaWiki.msg.set( $messages );\n";
413 - }
414 -
415 - public static function makeCombinedStyles( array $styles ) {
416 - $out = '';
417 - foreach ( $styles as $media => $style ) {
418 - $out .= "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "\n}\n";
419 - }
420 - return $out;
421 - }
422 -
423 - public static function makeLoaderStateScript( $name, $state = null ) {
424 - if ( is_array( $name ) ) {
425 - $statuses = FormatJson::encode( $name );
426 - return "mediaWiki.loader.state( $statuses );\n";
427 - } else {
428 - $name = Xml::escapeJsString( $name );
429 - $state = Xml::escapeJsString( $state );
430 - return "mediaWiki.loader.state( '$name', '$state' );\n";
431 - }
432 - }
433 -
434 - public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $script ) {
435 - $name = Xml::escapeJsString( $name );
436 - $version = (int) $version > 1 ? (int) $version : 1;
437 - $dependencies = FormatJson::encode( $dependencies );
438 - $group = FormatJson::encode( $group );
439 - $script = str_replace( "\n", "\n\t", trim( $script ) );
440 - return "( function( name, version, dependencies, group ) {\n\t$script\n} )" .
441 - "( '$name', $version, $dependencies, $group );\n";
442 - }
443 -
444 - public static function makeLoaderRegisterScript( $name, $version = null, $dependencies = null, $group = null ) {
445 - if ( is_array( $name ) ) {
446 - $registrations = FormatJson::encode( $name );
447 - return "mediaWiki.loader.register( $registrations );\n";
448 - } else {
449 - $name = Xml::escapeJsString( $name );
450 - $version = (int) $version > 1 ? (int) $version : 1;
451 - $dependencies = FormatJson::encode( $dependencies );
452 - $group = FormatJson::encode( $group );
453 - return "mediaWiki.loader.register( '$name', $version, $dependencies, $group );\n";
454 - }
455 - }
456 -
457 - public static function makeLoaderConditionalScript( $script ) {
458 - $script = str_replace( "\n", "\n\t", trim( $script ) );
459 - return "if ( window.mediaWiki ) {\n\t$script\n}\n";
460 - }
461 -
462 - public static function makeConfigSetScript( array $configuration ) {
463 - $configuration = FormatJson::encode( $configuration );
464 - return "mediaWiki.config.set( $configuration );\n";
465 - }
466 -}
Index: trunk/phase3/includes/ResourceLoaderModule.php
@@ -1,1168 +0,0 @@
2 -<?php
3 -/**
4 - * This program is free software; you can redistribute it and/or modify
5 - * it under the terms of the GNU General Public License as published by
6 - * the Free Software Foundation; either version 2 of the License, or
7 - * (at your option) any later version.
8 - *
9 - * This program is distributed in the hope that it will be useful,
10 - * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 - * GNU General Public License for more details.
13 - *
14 - * You should have received a copy of the GNU General Public License along
15 - * with this program; if not, write to the Free Software Foundation, Inc.,
16 - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 - * http://www.gnu.org/copyleft/gpl.html
18 - *
19 - * @file
20 - * @author Trevor Parscal
21 - * @author Roan Kattouw
22 - */
23 -
24 -defined( 'MEDIAWIKI' ) || die( 1 );
25 -
26 -/**
27 - * Abstraction for resource loader modules, with name registration and maxage functionality.
28 - */
29 -abstract class ResourceLoaderModule {
30 -
31 - /* Protected Members */
32 -
33 - protected $name = null;
34 -
35 - // In-object cache for file dependencies
36 - protected $fileDeps = array();
37 - // In-object cache for message blob mtime
38 - protected $msgBlobMtime = array();
39 -
40 - /* Methods */
41 -
42 - /**
43 - * Get this module's name. This is set when the module is registered
44 - * with ResourceLoader::register()
45 - *
46 - * @return Mixed: name (string) or null if no name was set
47 - */
48 - public function getName() {
49 - return $this->name;
50 - }
51 -
52 - /**
53 - * Set this module's name. This is called by ResourceLodaer::register()
54 - * when registering the module. Other code should not call this.
55 - *
56 - * @param $name String: name
57 - */
58 - public function setName( $name ) {
59 - $this->name = $name;
60 - }
61 -
62 - /**
63 - * Get whether CSS for this module should be flipped
64 - */
65 - public function getFlip( $context ) {
66 - return $context->getDirection() === 'rtl';
67 - }
68 -
69 - /**
70 - * Get all JS for this module for a given language and skin.
71 - * Includes all relevant JS except loader scripts.
72 - *
73 - * @param $context ResourceLoaderContext object
74 - * @return String: JS
75 - */
76 - public function getScript( ResourceLoaderContext $context ) {
77 - // Stub, override expected
78 - return '';
79 - }
80 -
81 - /**
82 - * Get all CSS for this module for a given skin.
83 - *
84 - * @param $context ResourceLoaderContext object
85 - * @return array: strings of CSS keyed by media type
86 - */
87 - public function getStyles( ResourceLoaderContext $context ) {
88 - // Stub, override expected
89 - return '';
90 - }
91 -
92 - /**
93 - * Get the messages needed for this module.
94 - *
95 - * To get a JSON blob with messages, use MessageBlobStore::get()
96 - *
97 - * @return array of message keys. Keys may occur more than once
98 - */
99 - public function getMessages() {
100 - // Stub, override expected
101 - return array();
102 - }
103 -
104 - /**
105 - * Get the group this module is in.
106 - *
107 - * @return string of group name
108 - */
109 - public function getGroup() {
110 - // Stub, override expected
111 - return null;
112 - }
113 -
114 - /**
115 - * Get the loader JS for this module, if set.
116 - *
117 - * @return Mixed: loader JS (string) or false if no custom loader set
118 - */
119 - public function getLoaderScript() {
120 - // Stub, override expected
121 - return false;
122 - }
123 -
124 - /**
125 - * Get a list of modules this module depends on.
126 - *
127 - * Dependency information is taken into account when loading a module
128 - * on the client side. When adding a module on the server side,
129 - * dependency information is NOT taken into account and YOU are
130 - * responsible for adding dependent modules as well. If you don't do
131 - * this, the client side loader will send a second request back to the
132 - * server to fetch the missing modules, which kind of defeats the
133 - * purpose of the resource loader.
134 - *
135 - * To add dependencies dynamically on the client side, use a custom
136 - * loader script, see getLoaderScript()
137 - * @return Array of module names (strings)
138 - */
139 - public function getDependencies() {
140 - // Stub, override expected
141 - return array();
142 - }
143 -
144 - /**
145 - * Get the files this module depends on indirectly for a given skin.
146 - * Currently these are only image files referenced by the module's CSS.
147 - *
148 - * @param $skin String: skin name
149 - * @return array of files
150 - */
151 - public function getFileDependencies( $skin ) {
152 - // Try in-object cache first
153 - if ( isset( $this->fileDeps[$skin] ) ) {
154 - return $this->fileDeps[$skin];
155 - }
156 -
157 - $dbr = wfGetDB( DB_SLAVE );
158 - $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
159 - 'md_module' => $this->getName(),
160 - 'md_skin' => $skin,
161 - ), __METHOD__
162 - );
163 - if ( !is_null( $deps ) ) {
164 - return $this->fileDeps[$skin] = (array) FormatJson::decode( $deps, true );
165 - }
166 - return $this->fileDeps[$skin] = array();
167 - }
168 -
169 - /**
170 - * Set preloaded file dependency information. Used so we can load this
171 - * information for all modules at once.
172 - * @param $skin string Skin name
173 - * @param $deps array Array of file names
174 - */
175 - public function setFileDependencies( $skin, $deps ) {
176 - $this->fileDeps[$skin] = $deps;
177 - }
178 -
179 - /**
180 - * Get the last modification timestamp of the message blob for this
181 - * module in a given language.
182 - * @param $lang string Language code
183 - * @return int UNIX timestamp, or 0 if no blob found
184 - */
185 - public function getMsgBlobMtime( $lang ) {
186 - if ( !count( $this->getMessages() ) )
187 - return 0;
188 -
189 - $dbr = wfGetDB( DB_SLAVE );
190 - $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
191 - 'mr_resource' => $this->getName(),
192 - 'mr_lang' => $lang
193 - ), __METHOD__
194 - );
195 - $this->msgBlobMtime[$lang] = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0;
196 - return $this->msgBlobMtime[$lang];
197 - }
198 -
199 - /**
200 - * Set a preloaded message blob last modification timestamp. Used so we
201 - * can load this information for all modules at once.
202 - * @param $lang string Language code
203 - * @param $mtime int UNIX timestamp or 0 if there is no such blob
204 - */
205 - public function setMsgBlobMtime( $lang, $mtime ) {
206 - $this->msgBlobMtime[$lang] = $mtime;
207 - }
208 -
209 - /* Abstract Methods */
210 -
211 - /**
212 - * Get this module's last modification timestamp for a given
213 - * combination of language, skin and debug mode flag. This is typically
214 - * the highest of each of the relevant components' modification
215 - * timestamps. Whenever anything happens that changes the module's
216 - * contents for these parameters, the mtime should increase.
217 - *
218 - * @param $context ResourceLoaderContext object
219 - * @return int UNIX timestamp
220 - */
221 - public function getModifiedTime( ResourceLoaderContext $context ) {
222 - // 0 would mean now
223 - return 1;
224 - }
225 -}
226 -
227 -/**
228 - * Module based on local JS/CSS files. This is the most common type of module.
229 - */
230 -class ResourceLoaderFileModule extends ResourceLoaderModule {
231 - /* Protected Members */
232 -
233 - protected $scripts = array();
234 - protected $styles = array();
235 - protected $messages = array();
236 - protected $group;
237 - protected $dependencies = array();
238 - protected $debugScripts = array();
239 - protected $languageScripts = array();
240 - protected $skinScripts = array();
241 - protected $skinStyles = array();
242 - protected $loaders = array();
243 - protected $parameters = array();
244 -
245 - // In-object cache for file dependencies
246 - protected $fileDeps = array();
247 - // In-object cache for mtime
248 - protected $modifiedTime = array();
249 -
250 - /* Methods */
251 -
252 - /**
253 - * Construct a new module from an options array.
254 - *
255 - * @param $options array Options array. If empty, an empty module will be constructed
256 - *
257 - * $options format:
258 - * array(
259 - * // Required module options (mutually exclusive)
260 - * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ),
261 - *
262 - * // Optional module options
263 - * 'languageScripts' => array(
264 - * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... )
265 - * ...
266 - * ),
267 - * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ),
268 - * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ),
269 - *
270 - * // Non-raw module options
271 - * 'dependencies' => 'module' | array( 'module1', 'module2' ... )
272 - * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ),
273 - * 'styles' => 'dir/file.css' | array( 'dir/file1.css', 'dir/file2.css' ... ), |
274 - * array( 'dir/file1.css' => array( 'media' => 'print' ) ),
275 - * 'skinStyles' => array(
276 - * '[skin name]' => 'dir/skin.css' | array( 'dir/skin1.css', 'dir/skin2.css' ... ) |
277 - * array( 'dir/file1.css' => array( 'media' => 'print' )
278 - * ...
279 - * ),
280 - * 'messages' => array( 'message1', 'message2' ... ),
281 - * 'group' => 'stuff',
282 - * )
283 - *
284 - * @param $basePath String: base path to prepend to all paths in $options
285 - */
286 - public function __construct( $options = array(), $basePath = null ) {
287 - foreach ( $options as $option => $value ) {
288 - switch ( $option ) {
289 - case 'scripts':
290 - case 'debugScripts':
291 - case 'languageScripts':
292 - case 'skinScripts':
293 - case 'loaders':
294 - $this->{$option} = (array)$value;
295 - // Automatically prefix script paths
296 - if ( is_string( $basePath ) ) {
297 - foreach ( $this->{$option} as $key => $value ) {
298 - $this->{$option}[$key] = $basePath . $value;
299 - }
300 - }
301 - break;
302 - case 'styles':
303 - case 'skinStyles':
304 - $this->{$option} = (array)$value;
305 - // Automatically prefix style paths
306 - if ( is_string( $basePath ) ) {
307 - foreach ( $this->{$option} as $key => $value ) {
308 - if ( is_array( $value ) ) {
309 - $this->{$option}[$basePath . $key] = $value;
310 - unset( $this->{$option}[$key] );
311 - } else {
312 - $this->{$option}[$key] = $basePath . $value;
313 - }
314 - }
315 - }
316 - break;
317 - case 'dependencies':
318 - case 'messages':
319 - $this->{$option} = (array)$value;
320 - break;
321 - case 'group':
322 - $this->group = (string)$value;
323 - break;
324 - }
325 - }
326 - }
327 -
328 - /**
329 - * Add script files to this module. In order to be valid, a module
330 - * must contain at least one script file.
331 - *
332 - * @param $scripts Mixed: path to script file (string) or array of paths
333 - */
334 - public function addScripts( $scripts ) {
335 - $this->scripts = array_merge( $this->scripts, (array)$scripts );
336 - }
337 -
338 - /**
339 - * Add style (CSS) files to this module.
340 - *
341 - * @param $styles Mixed: path to CSS file (string) or array of paths
342 - */
343 - public function addStyles( $styles ) {
344 - $this->styles = array_merge( $this->styles, (array)$styles );
345 - }
346 -
347 - /**
348 - * Add messages to this module.
349 - *
350 - * @param $messages Mixed: message key (string) or array of message keys
351 - */
352 - public function addMessages( $messages ) {
353 - $this->messages = array_merge( $this->messages, (array)$messages );
354 - }
355 -
356 - /**
357 - * Sets the group of this module.
358 - *
359 - * @param $group string group name
360 - */
361 - public function setGroup( $group ) {
362 - $this->group = $group;
363 - }
364 -
365 - /**
366 - * Add dependencies. Dependency information is taken into account when
367 - * loading a module on the client side. When adding a module on the
368 - * server side, dependency information is NOT taken into account and
369 - * YOU are responsible for adding dependent modules as well. If you
370 - * don't do this, the client side loader will send a second request
371 - * back to the server to fetch the missing modules, which kind of
372 - * defeats the point of using the resource loader in the first place.
373 - *
374 - * To add dependencies dynamically on the client side, use a custom
375 - * loader (see addLoaders())
376 - *
377 - * @param $dependencies Mixed: module name (string) or array of module names
378 - */
379 - public function addDependencies( $dependencies ) {
380 - $this->dependencies = array_merge( $this->dependencies, (array)$dependencies );
381 - }
382 -
383 - /**
384 - * Add debug scripts to the module. These scripts are only included
385 - * in debug mode.
386 - *
387 - * @param $scripts Mixed: path to script file (string) or array of paths
388 - */
389 - public function addDebugScripts( $scripts ) {
390 - $this->debugScripts = array_merge( $this->debugScripts, (array)$scripts );
391 - }
392 -
393 - /**
394 - * Add language-specific scripts. These scripts are only included for
395 - * a given language.
396 - *
397 - * @param $lang String: language code
398 - * @param $scripts Mixed: path to script file (string) or array of paths
399 - */
400 - public function addLanguageScripts( $lang, $scripts ) {
401 - $this->languageScripts = array_merge_recursive(
402 - $this->languageScripts,
403 - array( $lang => $scripts )
404 - );
405 - }
406 -
407 - /**
408 - * Add skin-specific scripts. These scripts are only included for
409 - * a given skin.
410 - *
411 - * @param $skin String: skin name, or 'default'
412 - * @param $scripts Mixed: path to script file (string) or array of paths
413 - */
414 - public function addSkinScripts( $skin, $scripts ) {
415 - $this->skinScripts = array_merge_recursive(
416 - $this->skinScripts,
417 - array( $skin => $scripts )
418 - );
419 - }
420 -
421 - /**
422 - * Add skin-specific CSS. These CSS files are only included for a
423 - * given skin. If there are no skin-specific CSS files for a skin,
424 - * the files defined for 'default' will be used, if any.
425 - *
426 - * @param $skin String: skin name, or 'default'
427 - * @param $scripts Mixed: path to CSS file (string) or array of paths
428 - */
429 - public function addSkinStyles( $skin, $scripts ) {
430 - $this->skinStyles = array_merge_recursive(
431 - $this->skinStyles,
432 - array( $skin => $scripts )
433 - );
434 - }
435 -
436 - /**
437 - * Add loader scripts. These scripts are loaded on every page and are
438 - * responsible for registering this module using
439 - * mediaWiki.loader.register(). If there are no loader scripts defined,
440 - * the resource loader will register the module itself.
441 - *
442 - * Loader scripts are used to determine a module's dependencies
443 - * dynamically on the client side (e.g. based on browser type/version).
444 - * Note that loader scripts are included on every page, so they should
445 - * be lightweight and use mediaWiki.loader.register()'s callback
446 - * feature to defer dependency calculation.
447 - *
448 - * @param $scripts Mixed: path to script file (string) or array of paths
449 - */
450 - public function addLoaders( $scripts ) {
451 - $this->loaders = array_merge( $this->loaders, (array)$scripts );
452 - }
453 -
454 - public function getScript( ResourceLoaderContext $context ) {
455 - $retval = $this->getPrimaryScript() . "\n" .
456 - $this->getLanguageScript( $context->getLanguage() ) . "\n" .
457 - $this->getSkinScript( $context->getSkin() );
458 -
459 - if ( $context->getDebug() ) {
460 - $retval .= $this->getDebugScript();
461 - }
462 -
463 - return $retval;
464 - }
465 -
466 - public function getStyles( ResourceLoaderContext $context ) {
467 - $styles = array();
468 - foreach ( $this->getPrimaryStyles() as $media => $style ) {
469 - if ( !isset( $styles[$media] ) ) {
470 - $styles[$media] = '';
471 - }
472 - $styles[$media] .= $style;
473 - }
474 - foreach ( $this->getSkinStyles( $context->getSkin() ) as $media => $style ) {
475 - if ( !isset( $styles[$media] ) ) {
476 - $styles[$media] = '';
477 - }
478 - $styles[$media] .= $style;
479 - }
480 -
481 - // Collect referenced files
482 - $files = array();
483 - foreach ( $styles as $style ) {
484 - // Extract and store the list of referenced files
485 - $files = array_merge( $files, CSSMin::getLocalFileReferences( $style ) );
486 - }
487 -
488 - // Only store if modified
489 - if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) {
490 - $encFiles = FormatJson::encode( $files );
491 - $dbw = wfGetDB( DB_MASTER );
492 - $dbw->replace( 'module_deps',
493 - array( array( 'md_module', 'md_skin' ) ), array(
494 - 'md_module' => $this->getName(),
495 - 'md_skin' => $context->getSkin(),
496 - 'md_deps' => $encFiles,
497 - )
498 - );
499 - }
500 -
501 - return $styles;
502 - }
503 -
504 - public function getMessages() {
505 - return $this->messages;
506 - }
507 -
508 - public function getGroup() {
509 - return $this->group;
510 - }
511 -
512 - public function getDependencies() {
513 - return $this->dependencies;
514 - }
515 -
516 - public function getLoaderScript() {
517 - if ( count( $this->loaders ) == 0 ) {
518 - return false;
519 - }
520 -
521 - return self::concatScripts( $this->loaders );
522 - }
523 -
524 - /**
525 - * Get the last modified timestamp of this module, which is calculated
526 - * as the highest last modified timestamp of its constituent files and
527 - * the files it depends on (see getFileDependencies()). Only files
528 - * relevant to the given language and skin are taken into account, and
529 - * files only relevant in debug mode are not taken into account when
530 - * debug mode is off.
531 - *
532 - * @param $context ResourceLoaderContext object
533 - * @return Integer: UNIX timestamp
534 - */
535 - public function getModifiedTime( ResourceLoaderContext $context ) {
536 - if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
537 - return $this->modifiedTime[$context->getHash()];
538 - }
539 - wfProfileIn( __METHOD__ );
540 -
541 - // Sort of nasty way we can get a flat list of files depended on by all styles
542 - $styles = array();
543 - foreach ( self::organizeFilesByOption( $this->styles, 'media', 'all' ) as $styleFiles ) {
544 - $styles = array_merge( $styles, $styleFiles );
545 - }
546 - $skinFiles = (array) self::getSkinFiles(
547 - $context->getSkin(), self::organizeFilesByOption( $this->skinStyles, 'media', 'all' )
548 - );
549 - foreach ( $skinFiles as $styleFiles ) {
550 - $styles = array_merge( $styles, $styleFiles );
551 - }
552 -
553 - // Final merge, this should result in a master list of dependent files
554 - $files = array_merge(
555 - $this->scripts,
556 - $styles,
557 - $context->getDebug() ? $this->debugScripts : array(),
558 - isset( $this->languageScripts[$context->getLanguage()] ) ?
559 - (array) $this->languageScripts[$context->getLanguage()] : array(),
560 - (array) self::getSkinFiles( $context->getSkin(), $this->skinScripts ),
561 - $this->loaders,
562 - $this->getFileDependencies( $context->getSkin() )
563 - );
564 -
565 - wfProfileIn( __METHOD__.'-filemtime' );
566 - $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__, 'remapFilename' ), $files ) ) );
567 - wfProfileOut( __METHOD__.'-filemtime' );
568 - $this->modifiedTime[$context->getHash()] = max( $filesMtime, $this->getMsgBlobMtime( $context->getLanguage() ) );
569 - wfProfileOut( __METHOD__ );
570 - return $this->modifiedTime[$context->getHash()];
571 - }
572 -
573 - /* Protected Members */
574 -
575 - /**
576 - * Get the primary JS for this module. This is pulled from the
577 - * script files added through addScripts()
578 - *
579 - * @return String: JS
580 - */
581 - protected function getPrimaryScript() {
582 - return self::concatScripts( $this->scripts );
583 - }
584 -
585 - /**
586 - * Get the primary CSS for this module. This is pulled from the CSS
587 - * files added through addStyles()
588 - *
589 - * @return Array
590 - */
591 - protected function getPrimaryStyles() {
592 - return self::concatStyles( $this->styles );
593 - }
594 -
595 - /**
596 - * Get the debug JS for this module. This is pulled from the script
597 - * files added through addDebugScripts()
598 - *
599 - * @return String: JS
600 - */
601 - protected function getDebugScript() {
602 - return self::concatScripts( $this->debugScripts );
603 - }
604 -
605 - /**
606 - * Get the language-specific JS for a given language. This is pulled
607 - * from the language-specific script files added through addLanguageScripts()
608 - *
609 - * @return String: JS
610 - */
611 - protected function getLanguageScript( $lang ) {
612 - if ( !isset( $this->languageScripts[$lang] ) ) {
613 - return '';
614 - }
615 - return self::concatScripts( $this->languageScripts[$lang] );
616 - }
617 -
618 - /**
619 - * Get the skin-specific JS for a given skin. This is pulled from the
620 - * skin-specific JS files added through addSkinScripts()
621 - *
622 - * @return String: JS
623 - */
624 - protected function getSkinScript( $skin ) {
625 - return self::concatScripts( self::getSkinFiles( $skin, $this->skinScripts ) );
626 - }
627 -
628 - /**
629 - * Get the skin-specific CSS for a given skin. This is pulled from the
630 - * skin-specific CSS files added through addSkinStyles()
631 - *
632 - * @return Array: list of CSS strings keyed by media type
633 - */
634 - protected function getSkinStyles( $skin ) {
635 - return self::concatStyles( self::getSkinFiles( $skin, $this->skinStyles ) );
636 - }
637 -
638 - /**
639 - * Helper function to get skin-specific data from an array.
640 - *
641 - * @param $skin String: skin name
642 - * @param $map Array: map of skin names to arrays
643 - * @return $map[$skin] if set and non-empty, or $map['default'] if set, or an empty array
644 - */
645 - protected static function getSkinFiles( $skin, $map ) {
646 - $retval = array();
647 -
648 - if ( isset( $map[$skin] ) && $map[$skin] ) {
649 - $retval = $map[$skin];
650 - } else if ( isset( $map['default'] ) ) {
651 - $retval = $map['default'];
652 - }
653 -
654 - return $retval;
655 - }
656 -
657 - /**
658 - * Get the contents of a set of files and concatenate them, with
659 - * newlines in between. Each file is used only once.
660 - *
661 - * @param $files Array of file names
662 - * @return String: concatenated contents of $files
663 - */
664 - protected static function concatScripts( $files ) {
665 - return implode( "\n",
666 - array_map(
667 - 'file_get_contents',
668 - array_map(
669 - array( __CLASS__, 'remapFilename' ),
670 - array_unique( (array) $files ) ) ) );
671 - }
672 -
673 - protected static function organizeFilesByOption( $files, $option, $default ) {
674 - $organizedFiles = array();
675 - foreach ( (array) $files as $key => $value ) {
676 - if ( is_int( $key ) ) {
677 - // File name as the value
678 - if ( !isset( $organizedFiles[$default] ) ) {
679 - $organizedFiles[$default] = array();
680 - }
681 - $organizedFiles[$default][] = $value;
682 - } else if ( is_array( $value ) ) {
683 - // File name as the key, options array as the value
684 - $media = isset( $value[$option] ) ? $value[$option] : $default;
685 - if ( !isset( $organizedFiles[$media] ) ) {
686 - $organizedFiles[$media] = array();
687 - }
688 - $organizedFiles[$media][] = $key;
689 - }
690 - }
691 - return $organizedFiles;
692 - }
693 -
694 - /**
695 - * Get the contents of a set of CSS files, remap then and concatenate
696 - * them, with newlines in between. Each file is used only once.
697 - *
698 - * @param $styles Array of file names
699 - * @return Array: list of concatenated and remapped contents of $files keyed by media type
700 - */
701 - protected static function concatStyles( $styles ) {
702 - $styles = self::organizeFilesByOption( $styles, 'media', 'all' );
703 - foreach ( $styles as $media => $files ) {
704 - $styles[$media] =
705 - implode( "\n",
706 - array_map(
707 - array( __CLASS__, 'remapStyle' ),
708 - array_unique( (array) $files ) ) );
709 - }
710 - return $styles;
711 - }
712 -
713 - /**
714 - * Remap a relative to $IP. Used as a callback for array_map()
715 - *
716 - * @param $file String: file name
717 - * @return string $IP/$file
718 - */
719 - protected static function remapFilename( $file ) {
720 - global $IP;
721 -
722 - return "$IP/$file";
723 - }
724 -
725 - /**
726 - * Get the contents of a CSS file and run it through CSSMin::remap().
727 - * This wrapper is needed so we can use array_map() in concatStyles()
728 - *
729 - * @param $file String: file name
730 - * @return string Remapped CSS
731 - */
732 - protected static function remapStyle( $file ) {
733 - global $wgScriptPath;
734 - return CSSMin::remap(
735 - file_get_contents( self::remapFilename( $file ) ),
736 - dirname( $file ),
737 - $wgScriptPath . '/' . dirname( $file ),
738 - true
739 - );
740 - }
741 -}
742 -
743 -/**
744 - * Abstraction for resource loader modules which pull from wiki pages
745 - *
746 - * This can only be used for wiki pages in the MediaWiki and User namespaces, because of it's dependence on the
747 - * functionality of Title::isValidCssJsSubpage.
748 - */
749 -abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
750 -
751 - /* Protected Members */
752 -
753 - // In-object cache for modified time
754 - protected $modifiedTime = array();
755 -
756 - /* Abstract Protected Methods */
757 -
758 - abstract protected function getPages( ResourceLoaderContext $context );
759 -
760 - /* Protected Methods */
761 -
762 - protected function getContent( $page, $ns ) {
763 - if ( $ns === NS_MEDIAWIKI ) {
764 - return wfEmptyMsg( $page ) ? '' : wfMsgExt( $page, 'content' );
765 - }
766 - if ( $title = Title::newFromText( $page, $ns ) ) {
767 - if ( $title->isValidCssJsSubpage() && $revision = Revision::newFromTitle( $title ) ) {
768 - return $revision->getRawText();
769 - }
770 - }
771 - return null;
772 - }
773 -
774 - /* Methods */
775 -
776 - public function getScript( ResourceLoaderContext $context ) {
777 - $scripts = '';
778 - foreach ( $this->getPages( $context ) as $page => $options ) {
779 - if ( $options['type'] === 'script' ) {
780 - if ( $script = $this->getContent( $page, $options['ns'] ) ) {
781 - $ns = MWNamespace::getCanonicalName( $options['ns'] );
782 - $scripts .= "/*$ns:$page */\n$script\n";
783 - }
784 - }
785 - }
786 - return $scripts;
787 - }
788 -
789 - public function getStyles( ResourceLoaderContext $context ) {
790 -
791 - $styles = array();
792 - foreach ( $this->getPages( $context ) as $page => $options ) {
793 - if ( $options['type'] === 'style' ) {
794 - $media = isset( $options['media'] ) ? $options['media'] : 'all';
795 - if ( $style = $this->getContent( $page, $options['ns'] ) ) {
796 - if ( !isset( $styles[$media] ) ) {
797 - $styles[$media] = '';
798 - }
799 - $ns = MWNamespace::getCanonicalName( $options['ns'] );
800 - $styles[$media] .= "/* $ns:$page */\n$style\n";
801 - }
802 - }
803 - }
804 - return $styles;
805 - }
806 -
807 - public function getModifiedTime( ResourceLoaderContext $context ) {
808 - $hash = $context->getHash();
809 - if ( isset( $this->modifiedTime[$hash] ) ) {
810 - return $this->modifiedTime[$hash];
811 - }
812 -
813 - $titles = array();
814 - foreach ( $this->getPages( $context ) as $page => $options ) {
815 - $titles[$options['ns']][$page] = true;
816 - }
817 -
818 - $modifiedTime = 1; // wfTimestamp() interprets 0 as "now"
819 -
820 - if ( $titles ) {
821 - $dbr = wfGetDB( DB_SLAVE );
822 - $latest = $dbr->selectField( 'page', 'MAX(page_touched)',
823 - $dbr->makeWhereFrom2d( $titles, 'page_namespace', 'page_title' ),
824 - __METHOD__ );
825 -
826 - if ( $latest ) {
827 - $modifiedTime = wfTimestamp( TS_UNIX, $latest );
828 - }
829 - }
830 -
831 - return $this->modifiedTime[$hash] = $modifiedTime;
832 - }
833 -}
834 -
835 -/**
836 - * Module for site customizations
837 - */
838 -class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
839 -
840 - /* Protected Methods */
841 -
842 - protected function getPages( ResourceLoaderContext $context ) {
843 - global $wgHandheldStyle;
844 -
845 - $pages = array(
846 - 'Common.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
847 - 'Common.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
848 - ucfirst( $context->getSkin() ) . '.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
849 - ucfirst( $context->getSkin() ) . '.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
850 - 'Print.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'print' ),
851 - );
852 - if ( $wgHandheldStyle ) {
853 - $pages['Handheld.css'] = array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'handheld' );
854 - }
855 - return $pages;
856 - }
857 -
858 - /* Methods */
859 -
860 - public function getGroup() {
861 - return 'site';
862 - }
863 -}
864 -
865 -/**
866 - * Module for user customizations
867 - */
868 -class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
869 -
870 - /* Protected Methods */
871 -
872 - protected function getPages( ResourceLoaderContext $context ) {
873 - global $wgAllowUserCss;
874 -
875 - if ( $context->getUser() && $wgAllowUserCss ) {
876 - $username = $context->getUser();
877 - return array(
878 - "$username/common.js" => array( 'ns' => NS_USER, 'type' => 'script' ),
879 - "$username/" . $context->getSkin() . '.js' => array( 'ns' => NS_USER, 'type' => 'script' ),
880 - "$username/common.css" => array( 'ns' => NS_USER, 'type' => 'style' ),
881 - "$username/" . $context->getSkin() . '.css' => array( 'ns' => NS_USER, 'type' => 'style' ),
882 - );
883 - }
884 - return array();
885 - }
886 -
887 - /* Methods */
888 -
889 - public function getGroup() {
890 - return 'user';
891 - }
892 -}
893 -
894 -/**
895 - * Module for user preference customizations
896 - */
897 -class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
898 -
899 - /* Protected Members */
900 -
901 - protected $modifiedTime = array();
902 -
903 - /* Methods */
904 -
905 - public function getModifiedTime( ResourceLoaderContext $context ) {
906 - $hash = $context->getHash();
907 - if ( isset( $this->modifiedTime[$hash] ) ) {
908 - return $this->modifiedTime[$hash];
909 - }
910 -
911 - global $wgUser;
912 -
913 - if ( $context->getUser() === $wgUser->getName() ) {
914 - return $this->modifiedTime[$hash] = $wgUser->getTouched();
915 - } else {
916 - return 1;
917 - }
918 - }
919 -
920 - /**
921 - * Fetch the context's user options, or if it doesn't match current user,
922 - * the default options.
923 - *
924 - * @param $context ResourceLoaderContext
925 - * @return array
926 - */
927 - protected function contextUserOptions( ResourceLoaderContext $context ) {
928 - global $wgUser;
929 -
930 - // Verify identity -- this is a private module
931 - if ( $context->getUser() === $wgUser->getName() ) {
932 - return $wgUser->getOptions();
933 - } else {
934 - return User::getDefaultOptions();
935 - }
936 - }
937 -
938 - public function getScript( ResourceLoaderContext $context ) {
939 - $encOptions = FormatJson::encode( $this->contextUserOptions( $context ) );
940 - return "mediaWiki.user.options.set( $encOptions );";
941 - }
942 -
943 - public function getStyles( ResourceLoaderContext $context ) {
944 - global $wgAllowUserCssPrefs;
945 -
946 - if ( $wgAllowUserCssPrefs ) {
947 - $options = $this->contextUserOptions( $context );
948 -
949 - // Build CSS rules
950 - $rules = array();
951 - if ( $options['underline'] < 2 ) {
952 - $rules[] = "a { text-decoration: " . ( $options['underline'] ? 'underline' : 'none' ) . "; }";
953 - }
954 - if ( $options['highlightbroken'] ) {
955 - $rules[] = "a.new, #quickbar a.new { color: #ba0000; }\n";
956 - } else {
957 - $rules[] = "a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; }";
958 - $rules[] = "a.new:after, #quickbar a.new:after { content: '?'; color: #ba0000; }";
959 - $rules[] = "a.stub:after, #quickbar a.stub:after { content: '!'; color: #772233; }";
960 - }
961 - if ( $options['justify'] ) {
962 - $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
963 - }
964 - if ( !$options['showtoc'] ) {
965 - $rules[] = "#toc { display: none; }\n";
966 - }
967 - if ( !$options['editsection'] ) {
968 - $rules[] = ".editsection { display: none; }\n";
969 - }
970 - if ( $options['editfont'] !== 'default' ) {
971 - $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
972 - }
973 - return array( 'all' => implode( "\n", $rules ) );
974 - }
975 - return array();
976 - }
977 -
978 - public function getFlip( $context ) {
979 - global $wgContLang;
980 -
981 - return $wgContLang->getDir() !== $context->getDirection();
982 - }
983 -
984 - public function getGroup() {
985 - return 'private';
986 - }
987 -}
988 -
989 -class ResourceLoaderStartUpModule extends ResourceLoaderModule {
990 - /* Protected Members */
991 -
992 - protected $modifiedTime = array();
993 -
994 - /* Protected Methods */
995 -
996 - protected function getConfig( $context ) {
997 - global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension,
998 - $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgBreakFrames,
999 - $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion,
1000 - $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest,
1001 - $wgSitename, $wgFileExtensions;
1002 -
1003 - // Pre-process information
1004 - $separatorTransTable = $wgContLang->separatorTransformTable();
1005 - $separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
1006 - $compactSeparatorTransTable = array(
1007 - implode( "\t", array_keys( $separatorTransTable ) ),
1008 - implode( "\t", $separatorTransTable ),
1009 - );
1010 - $digitTransTable = $wgContLang->digitTransformTable();
1011 - $digitTransTable = $digitTransTable ? $digitTransTable : array();
1012 - $compactDigitTransTable = array(
1013 - implode( "\t", array_keys( $digitTransTable ) ),
1014 - implode( "\t", $digitTransTable ),
1015 - );
1016 - $mainPage = Title::newMainPage();
1017 -
1018 - // Build list of variables
1019 - $vars = array(
1020 - 'wgLoadScript' => $wgLoadScript,
1021 - 'debug' => $context->getDebug(),
1022 - 'skin' => $context->getSkin(),
1023 - 'stylepath' => $wgStylePath,
1024 - 'wgUrlProtocols' => wfUrlProtocols(),
1025 - 'wgArticlePath' => $wgArticlePath,
1026 - 'wgScriptPath' => $wgScriptPath,
1027 - 'wgScriptExtension' => $wgScriptExtension,
1028 - 'wgScript' => $wgScript,
1029 - 'wgVariantArticlePath' => $wgVariantArticlePath,
1030 - 'wgActionPaths' => $wgActionPaths,
1031 - 'wgServer' => $wgServer,
1032 - 'wgUserLanguage' => $context->getLanguage(),
1033 - 'wgContentLanguage' => $wgContLang->getCode(),
1034 - 'wgBreakFrames' => $wgBreakFrames,
1035 - 'wgVersion' => $wgVersion,
1036 - 'wgEnableAPI' => $wgEnableAPI,
1037 - 'wgEnableWriteAPI' => $wgEnableWriteAPI,
1038 - 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
1039 - 'wgDigitTransformTable' => $compactDigitTransTable,
1040 - 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null,
1041 - 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
1042 - 'wgNamespaceIds' => $wgContLang->getNamespaceIds(),
1043 - 'wgSiteName' => $wgSitename,
1044 - 'wgFileExtensions' => $wgFileExtensions,
1045 - 'wgDBname' => $wgDBname,
1046 - );
1047 - if ( $wgContLang->hasVariants() ) {
1048 - $vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
1049 - }
1050 - if ( $wgUseAjax && $wgEnableMWSuggest ) {
1051 - $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
1052 - }
1053 -
1054 - return $vars;
1055 - }
1056 -
1057 - /**
1058 - * Gets registration code for all modules
1059 - *
1060 - * @param $context ResourceLoaderContext object
1061 - * @return String: JavaScript code for registering all modules with the client loader
1062 - */
1063 - public static function getModuleRegistrations( ResourceLoaderContext $context ) {
1064 - global $wgCacheEpoch;
1065 - wfProfileIn( __METHOD__ );
1066 -
1067 - $out = '';
1068 - $registrations = array();
1069 - foreach ( $context->getResourceLoader()->getModules() as $name => $module ) {
1070 - // Support module loader scripts
1071 - if ( ( $loader = $module->getLoaderScript() ) !== false ) {
1072 - $deps = $module->getDependencies();
1073 - $group = $module->getGroup();
1074 - $version = wfTimestamp( TS_ISO_8601_BASIC, round( $module->getModifiedTime( $context ), -2 ) );
1075 - $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $loader );
1076 - }
1077 - // Automatically register module
1078 - else {
1079 - $mtime = max( $module->getModifiedTime( $context ), wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
1080 - // Modules without dependencies or a group pass two arguments (name, timestamp) to
1081 - // mediaWiki.loader.register()
1082 - if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) {
1083 - $registrations[] = array( $name, $mtime );
1084 - }
1085 - // Modules with dependencies but no group pass three arguments (name, timestamp, dependencies)
1086 - // to mediaWiki.loader.register()
1087 - else if ( $module->getGroup() === null ) {
1088 - $registrations[] = array(
1089 - $name, $mtime, $module->getDependencies() );
1090 - }
1091 - // Modules with dependencies pass four arguments (name, timestamp, dependencies, group)
1092 - // to mediaWiki.loader.register()
1093 - else {
1094 - $registrations[] = array(
1095 - $name, $mtime, $module->getDependencies(), $module->getGroup() );
1096 - }
1097 - }
1098 - }
1099 - $out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
1100 -
1101 - wfProfileOut( __METHOD__ );
1102 - return $out;
1103 - }
1104 -
1105 - /* Methods */
1106 -
1107 - public function getScript( ResourceLoaderContext $context ) {
1108 - global $IP, $wgLoadScript;
1109 -
1110 - $out = file_get_contents( "$IP/resources/startup.js" );
1111 - if ( $context->getOnly() === 'scripts' ) {
1112 - // Build load query for jquery and mediawiki modules
1113 - $query = array(
1114 - 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ),
1115 - 'only' => 'scripts',
1116 - 'lang' => $context->getLanguage(),
1117 - 'skin' => $context->getSkin(),
1118 - 'debug' => $context->getDebug() ? 'true' : 'false',
1119 - 'version' => wfTimestamp( TS_ISO_8601_BASIC, round( max(
1120 - $context->getResourceLoader()->getModule( 'jquery' )->getModifiedTime( $context ),
1121 - $context->getResourceLoader()->getModule( 'mediawiki' )->getModifiedTime( $context )
1122 - ), -2 ) )
1123 - );
1124 - // Ensure uniform query order
1125 - ksort( $query );
1126 -
1127 - // Startup function
1128 - $configuration = FormatJson::encode( $this->getConfig( $context ) );
1129 - $registrations = self::getModuleRegistrations( $context );
1130 - $out .= "var startUp = function() {\n\t$registrations\n\tmediaWiki.config.set( $configuration );\n};";
1131 -
1132 - // Conditional script injection
1133 - $scriptTag = Xml::escapeJsString( Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) );
1134 - $out .= "if ( isCompatible() ) {\n\tdocument.write( '$scriptTag' );\n}\ndelete isCompatible;";
1135 - }
1136 -
1137 - return $out;
1138 - }
1139 -
1140 - public function getModifiedTime( ResourceLoaderContext $context ) {
1141 - global $IP, $wgCacheEpoch;
1142 -
1143 - $hash = $context->getHash();
1144 - if ( isset( $this->modifiedTime[$hash] ) ) {
1145 - return $this->modifiedTime[$hash];
1146 - }
1147 - $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" );
1148 -
1149 - // ATTENTION!: Because of the line above, this is not going to cause infinite recursion - think carefully
1150 - // before making changes to this code!
1151 - $time = wfTimestamp( TS_UNIX, $wgCacheEpoch );
1152 - foreach ( $context->getResourceLoader()->getModules() as $module ) {
1153 - $time = max( $time, $module->getModifiedTime( $context ) );
1154 - }
1155 - return $this->modifiedTime[$hash] = $time;
1156 - }
1157 -
1158 - public function getFlip( $context ) {
1159 - global $wgContLang;
1160 -
1161 - return $wgContLang->getDir() !== $context->getDirection();
1162 - }
1163 -
1164 - /* Methods */
1165 -
1166 - public function getGroup() {
1167 - return 'startup';
1168 - }
1169 -}
Index: trunk/phase3/includes/resourceloader/ResourceLoader.php
@@ -0,0 +1,465 @@
 2+<?php
 3+/**
 4+ * This program is free software; you can redistribute it and/or modify
 5+ * it under the terms of the GNU General Public License as published by
 6+ * the Free Software Foundation; either version 2 of the License, or
 7+ * (at your option) any later version.
 8+ *
 9+ * This program is distributed in the hope that it will be useful,
 10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12+ * GNU General Public License for more details.
 13+ *
 14+ * You should have received a copy of the GNU General Public License along
 15+ * with this program; if not, write to the Free Software Foundation, Inc.,
 16+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 17+ * http://www.gnu.org/copyleft/gpl.html
 18+ *
 19+ * @file
 20+ * @author Roan Kattouw
 21+ * @author Trevor Parscal
 22+ */
 23+
 24+defined( 'MEDIAWIKI' ) || die( 1 );
 25+
 26+/**
 27+ * Dynamic JavaScript and CSS resource loading system
 28+ */
 29+class ResourceLoader {
 30+
 31+ /* Protected Static Members */
 32+
 33+ // @var array list of module name/ResourceLoaderModule object pairs
 34+ protected $modules = array();
 35+
 36+ /* Protected Methods */
 37+
 38+ /**
 39+ * Loads information stored in the database about modules
 40+ *
 41+ * This is not inside the module code because it's so much more performant to request all of the information at once
 42+ * than it is to have each module requests it's own information.
 43+ *
 44+ * @param $modules array list of module names to preload information for
 45+ * @param $context ResourceLoaderContext context to load the information within
 46+ */
 47+ protected function preloadModuleInfo( array $modules, ResourceLoaderContext $context ) {
 48+ if ( !count( $modules ) ) {
 49+ return; # or Database*::select() will explode
 50+ }
 51+ $dbr = wfGetDb( DB_SLAVE );
 52+ $skin = $context->getSkin();
 53+ $lang = $context->getLanguage();
 54+
 55+ // Get file dependency information
 56+ $res = $dbr->select( 'module_deps', array( 'md_module', 'md_deps' ), array(
 57+ 'md_module' => $modules,
 58+ 'md_skin' => $context->getSkin()
 59+ ), __METHOD__
 60+ );
 61+
 62+ $modulesWithDeps = array();
 63+ foreach ( $res as $row ) {
 64+ $this->modules[$row->md_module]->setFileDependencies( $skin,
 65+ FormatJson::decode( $row->md_deps, true )
 66+ );
 67+ $modulesWithDeps[] = $row->md_module;
 68+ }
 69+ // Register the absence of a dependencies row too
 70+ foreach ( array_diff( $modules, $modulesWithDeps ) as $name ) {
 71+ $this->modules[$name]->setFileDependencies( $skin, array() );
 72+ }
 73+
 74+ // Get message blob mtimes. Only do this for modules with messages
 75+ $modulesWithMessages = array();
 76+ $modulesWithoutMessages = array();
 77+ foreach ( $modules as $name ) {
 78+ if ( count( $this->modules[$name]->getMessages() ) ) {
 79+ $modulesWithMessages[] = $name;
 80+ } else {
 81+ $modulesWithoutMessages[] = $name;
 82+ }
 83+ }
 84+ if ( count( $modulesWithMessages ) ) {
 85+ $res = $dbr->select( 'msg_resource', array( 'mr_resource', 'mr_timestamp' ), array(
 86+ 'mr_resource' => $modulesWithMessages,
 87+ 'mr_lang' => $lang
 88+ ), __METHOD__
 89+ );
 90+ foreach ( $res as $row ) {
 91+ $this->modules[$row->mr_resource]->setMsgBlobMtime( $lang, $row->mr_timestamp );
 92+ }
 93+ }
 94+ foreach ( $modulesWithoutMessages as $name ) {
 95+ $this->modules[$name]->setMsgBlobMtime( $lang, 0 );
 96+ }
 97+ }
 98+
 99+ /**
 100+ * Runs text through a filter, caching the filtered result for future calls
 101+ *
 102+ * @param $filter String: name of filter to run
 103+ * @param $data String: text to filter, such as JavaScript or CSS text
 104+ * @param $file String: path to file being filtered, (optional: only required for CSS to resolve paths)
 105+ * @return String: filtered data
 106+ */
 107+ protected function filter( $filter, $data ) {
 108+ global $wgMemc;
 109+ wfProfileIn( __METHOD__ );
 110+
 111+ // For empty or whitespace-only things, don't do any processing
 112+ if ( trim( $data ) === '' ) {
 113+ wfProfileOut( __METHOD__ );
 114+ return $data;
 115+ }
 116+
 117+ // Try memcached
 118+ $key = wfMemcKey( 'resourceloader', 'filter', $filter, md5( $data ) );
 119+ $cached = $wgMemc->get( $key );
 120+
 121+ if ( $cached !== false && $cached !== null ) {
 122+ wfProfileOut( __METHOD__ );
 123+ return $cached;
 124+ }
 125+
 126+ // Run the filter
 127+ try {
 128+ switch ( $filter ) {
 129+ case 'minify-js':
 130+ $result = JSMin::minify( $data );
 131+ break;
 132+ case 'minify-css':
 133+ $result = CSSMin::minify( $data );
 134+ break;
 135+ case 'flip-css':
 136+ $result = CSSJanus::transform( $data, true, false );
 137+ break;
 138+ default:
 139+ // Don't cache anything, just pass right through
 140+ wfProfileOut( __METHOD__ );
 141+ return $data;
 142+ }
 143+ } catch ( Exception $exception ) {
 144+ throw new MWException( 'Filter threw an exception: ' . $exception->getMessage() );
 145+ }
 146+
 147+ // Save to memcached
 148+ $wgMemc->set( $key, $result );
 149+
 150+ wfProfileOut( __METHOD__ );
 151+ return $result;
 152+ }
 153+
 154+ /* Methods */
 155+
 156+ /**
 157+ * Registers core modules and runs registration hooks
 158+ */
 159+ public function __construct() {
 160+ global $IP;
 161+
 162+ wfProfileIn( __METHOD__ );
 163+
 164+ // Register core modules
 165+ $this->register( include( "$IP/resources/Resources.php" ) );
 166+ // Register extension modules
 167+ wfRunHooks( 'ResourceLoaderRegisterModules', array( &$this ) );
 168+
 169+ wfProfileOut( __METHOD__ );
 170+ }
 171+
 172+ /**
 173+ * Registers a module with the ResourceLoader system.
 174+ *
 175+ * Note that registering the same object under multiple names is not supported
 176+ * and may silently fail in all kinds of interesting ways.
 177+ *
 178+ * @param $name Mixed: string of name of module or array of name/object pairs
 179+ * @param $object ResourceLoaderModule: module object (optional when using
 180+ * multiple-registration calling style)
 181+ * @return Boolean: false if there were any errors, in which case one or more
 182+ * modules were not registered
 183+ *
 184+ * @todo We need much more clever error reporting, not just in detailing what
 185+ * happened, but in bringing errors to the client in a way that they can
 186+ * easily see them if they want to, such as by using FireBug
 187+ */
 188+ public function register( $name, ResourceLoaderModule $object = null ) {
 189+ wfProfileIn( __METHOD__ );
 190+
 191+ // Allow multiple modules to be registered in one call
 192+ if ( is_array( $name ) && !isset( $object ) ) {
 193+ foreach ( $name as $key => $value ) {
 194+ $this->register( $key, $value );
 195+ }
 196+
 197+ wfProfileOut( __METHOD__ );
 198+ return;
 199+ }
 200+
 201+ // Disallow duplicate registrations
 202+ if ( isset( $this->modules[$name] ) ) {
 203+ // A module has already been registered by this name
 204+ throw new MWException( 'Another module has already been registered as ' . $name );
 205+ }
 206+
 207+ // Validate the input (type hinting lets null through)
 208+ if ( !( $object instanceof ResourceLoaderModule ) ) {
 209+ throw new MWException( 'Invalid ResourceLoader module error. Instances of ResourceLoaderModule expected.' );
 210+ }
 211+
 212+ // Attach module
 213+ $this->modules[$name] = $object;
 214+ $object->setName( $name );
 215+
 216+ wfProfileOut( __METHOD__ );
 217+ }
 218+
 219+ /**
 220+ * Gets a map of all modules and their options
 221+ *
 222+ * @return Array: array( modulename => ResourceLoaderModule )
 223+ */
 224+ public function getModules() {
 225+ return $this->modules;
 226+ }
 227+
 228+ /**
 229+ * Get the ResourceLoaderModule object for a given module name
 230+ *
 231+ * @param $name String: module name
 232+ * @return mixed ResourceLoaderModule or null if not registered
 233+ */
 234+ public function getModule( $name ) {
 235+ return isset( $this->modules[$name] ) ? $this->modules[$name] : null;
 236+ }
 237+
 238+ /**
 239+ * Outputs a response to a resource load-request, including a content-type header
 240+ *
 241+ * @param $context ResourceLoaderContext object
 242+ */
 243+ public function respond( ResourceLoaderContext $context ) {
 244+ global $wgResourceLoaderMaxage, $wgCacheEpoch;
 245+
 246+ wfProfileIn( __METHOD__ );
 247+
 248+ // Split requested modules into two groups, modules and missing
 249+ $modules = array();
 250+ $missing = array();
 251+
 252+ foreach ( $context->getModules() as $name ) {
 253+ if ( isset( $this->modules[$name] ) ) {
 254+ $modules[$name] = $this->modules[$name];
 255+ } else {
 256+ $missing[] = $name;
 257+ }
 258+ }
 259+
 260+ // If a version wasn't specified we need a shorter expiry time for updates to
 261+ // propagate to clients quickly
 262+ if ( is_null( $context->getVersion() ) ) {
 263+ $maxage = $wgResourceLoaderMaxage['unversioned']['client'];
 264+ $smaxage = $wgResourceLoaderMaxage['unversioned']['server'];
 265+ }
 266+ // If a version was specified we can use a longer expiry time since changing
 267+ // version numbers causes cache misses
 268+ else {
 269+ $maxage = $wgResourceLoaderMaxage['versioned']['client'];
 270+ $smaxage = $wgResourceLoaderMaxage['versioned']['server'];
 271+ }
 272+
 273+ // Preload information needed to the mtime calculation below
 274+ $this->preloadModuleInfo( array_keys( $modules ), $context );
 275+
 276+ // To send Last-Modified and support If-Modified-Since, we need to detect
 277+ // the last modified time
 278+ wfProfileIn( __METHOD__.'-getModifiedTime' );
 279+ $mtime = wfTimestamp( TS_UNIX, $wgCacheEpoch );
 280+ foreach ( $modules as $module ) {
 281+ // Bypass squid cache if the request includes any private modules
 282+ if ( $module->getGroup() === 'private' ) {
 283+ $smaxage = 0;
 284+ }
 285+ // Calculate maximum modified time
 286+ $mtime = max( $mtime, $module->getModifiedTime( $context ) );
 287+ }
 288+ wfProfileOut( __METHOD__.'-getModifiedTime' );
 289+
 290+ header( 'Content-Type: ' . ( $context->getOnly() === 'styles' ? 'text/css' : 'text/javascript' ) );
 291+ header( 'Last-Modified: ' . wfTimestamp( TS_RFC2822, $mtime ) );
 292+ header( "Cache-Control: public, max-age=$maxage, s-maxage=$smaxage" );
 293+ header( 'Expires: ' . wfTimestamp( TS_RFC2822, min( $maxage, $smaxage ) + time() ) );
 294+
 295+ // If there's an If-Modified-Since header, respond with a 304 appropriately
 296+ $ims = $context->getRequest()->getHeader( 'If-Modified-Since' );
 297+ if ( $ims !== false && $mtime <= wfTimestamp( TS_UNIX, $ims ) ) {
 298+ header( 'HTTP/1.0 304 Not Modified' );
 299+ header( 'Status: 304 Not Modified' );
 300+ wfProfileOut( __METHOD__ );
 301+ return;
 302+ }
 303+
 304+ $response = $this->makeModuleResponse( $context, $modules, $missing );
 305+ if ( $context->getDebug() && strlen( $warnings = ob_get_contents() ) ) {
 306+ $response .= "/*\n$warnings\n*/";
 307+ }
 308+ // Clear any warnings from the buffer
 309+ ob_clean();
 310+ echo $response;
 311+
 312+ wfProfileOut( __METHOD__ );
 313+ }
 314+
 315+ public function makeModuleResponse( ResourceLoaderContext $context, array $modules, $missing = null ) {
 316+ // Pre-fetch blobs
 317+ $blobs = $context->shouldIncludeMessages() ?
 318+ MessageBlobStore::get( $this, $modules, $context->getLanguage() ) : array();
 319+
 320+ // Generate output
 321+ $out = '';
 322+ foreach ( $modules as $name => $module ) {
 323+ wfProfileIn( __METHOD__ . '-' . $name );
 324+
 325+ // Scripts
 326+ $scripts = '';
 327+ if ( $context->shouldIncludeScripts() ) {
 328+ $scripts .= $module->getScript( $context ) . "\n";
 329+ }
 330+
 331+ // Styles
 332+ $styles = array();
 333+ if ( $context->shouldIncludeStyles() && ( count( $styles = $module->getStyles( $context ) ) ) ) {
 334+ // Flip CSS on a per-module basis
 335+ if ( $this->modules[$name]->getFlip( $context ) ) {
 336+ foreach ( $styles as $media => $style ) {
 337+ $styles[$media] = $this->filter( 'flip-css', $style );
 338+ }
 339+ }
 340+ }
 341+
 342+ // Messages
 343+ $messages = isset( $blobs[$name] ) ? $blobs[$name] : '{}';
 344+
 345+ // Append output
 346+ switch ( $context->getOnly() ) {
 347+ case 'scripts':
 348+ $out .= $scripts;
 349+ break;
 350+ case 'styles':
 351+ $out .= self::makeCombinedStyles( $styles );
 352+ break;
 353+ case 'messages':
 354+ $out .= self::makeMessageSetScript( $messages );
 355+ break;
 356+ default:
 357+ // Minify CSS before embedding in mediaWiki.loader.implement call (unless in debug mode)
 358+ if ( !$context->getDebug() ) {
 359+ foreach ( $styles as $media => $style ) {
 360+ $styles[$media] = $this->filter( 'minify-css', $style );
 361+ }
 362+ }
 363+ $out .= self::makeLoaderImplementScript( $name, $scripts, $styles, $messages );
 364+ break;
 365+ }
 366+
 367+ wfProfileOut( __METHOD__ . '-' . $name );
 368+ }
 369+
 370+ // Update module states
 371+ if ( $context->shouldIncludeScripts() ) {
 372+ // Set the state of modules loaded as only scripts to ready
 373+ if ( count( $modules ) && $context->getOnly() === 'scripts' && !isset( $modules['startup'] ) ) {
 374+ $out .= self::makeLoaderStateScript( array_fill_keys( array_keys( $modules ), 'ready' ) );
 375+ }
 376+ // Set the state of modules which were requested but unavailable as missing
 377+ if ( is_array( $missing ) && count( $missing ) ) {
 378+ $out .= self::makeLoaderStateScript( array_fill_keys( $missing, 'missing' ) );
 379+ }
 380+ }
 381+
 382+ if ( $context->getDebug() ) {
 383+ return $out;
 384+ } else {
 385+ if ( $context->getOnly() === 'styles' ) {
 386+ return $this->filter( 'minify-css', $out );
 387+ } else {
 388+ return $this->filter( 'minify-js', $out );
 389+ }
 390+ }
 391+ }
 392+
 393+ /* Static Methods */
 394+
 395+ public static function makeLoaderImplementScript( $name, $scripts, $styles, $messages ) {
 396+ if ( is_array( $scripts ) ) {
 397+ $scripts = implode( $scripts, "\n" );
 398+ }
 399+ if ( is_array( $styles ) ) {
 400+ $styles = count( $styles ) ? FormatJson::encode( $styles ) : 'null';
 401+ }
 402+ if ( is_array( $messages ) ) {
 403+ $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null';
 404+ }
 405+ return "mediaWiki.loader.implement( '$name', function() {{$scripts}},\n$styles,\n$messages );\n";
 406+ }
 407+
 408+ public static function makeMessageSetScript( $messages ) {
 409+ if ( is_array( $messages ) ) {
 410+ $messages = count( $messages ) ? FormatJson::encode( $messages ) : 'null';
 411+ }
 412+ return "mediaWiki.msg.set( $messages );\n";
 413+ }
 414+
 415+ public static function makeCombinedStyles( array $styles ) {
 416+ $out = '';
 417+ foreach ( $styles as $media => $style ) {
 418+ $out .= "@media $media {\n" . str_replace( "\n", "\n\t", "\t" . $style ) . "\n}\n";
 419+ }
 420+ return $out;
 421+ }
 422+
 423+ public static function makeLoaderStateScript( $name, $state = null ) {
 424+ if ( is_array( $name ) ) {
 425+ $statuses = FormatJson::encode( $name );
 426+ return "mediaWiki.loader.state( $statuses );\n";
 427+ } else {
 428+ $name = Xml::escapeJsString( $name );
 429+ $state = Xml::escapeJsString( $state );
 430+ return "mediaWiki.loader.state( '$name', '$state' );\n";
 431+ }
 432+ }
 433+
 434+ public static function makeCustomLoaderScript( $name, $version, $dependencies, $group, $script ) {
 435+ $name = Xml::escapeJsString( $name );
 436+ $version = (int) $version > 1 ? (int) $version : 1;
 437+ $dependencies = FormatJson::encode( $dependencies );
 438+ $group = FormatJson::encode( $group );
 439+ $script = str_replace( "\n", "\n\t", trim( $script ) );
 440+ return "( function( name, version, dependencies, group ) {\n\t$script\n} )" .
 441+ "( '$name', $version, $dependencies, $group );\n";
 442+ }
 443+
 444+ public static function makeLoaderRegisterScript( $name, $version = null, $dependencies = null, $group = null ) {
 445+ if ( is_array( $name ) ) {
 446+ $registrations = FormatJson::encode( $name );
 447+ return "mediaWiki.loader.register( $registrations );\n";
 448+ } else {
 449+ $name = Xml::escapeJsString( $name );
 450+ $version = (int) $version > 1 ? (int) $version : 1;
 451+ $dependencies = FormatJson::encode( $dependencies );
 452+ $group = FormatJson::encode( $group );
 453+ return "mediaWiki.loader.register( '$name', $version, $dependencies, $group );\n";
 454+ }
 455+ }
 456+
 457+ public static function makeLoaderConditionalScript( $script ) {
 458+ $script = str_replace( "\n", "\n\t", trim( $script ) );
 459+ return "if ( window.mediaWiki ) {\n\t$script\n}\n";
 460+ }
 461+
 462+ public static function makeConfigSetScript( array $configuration ) {
 463+ $configuration = FormatJson::encode( $configuration );
 464+ return "mediaWiki.config.set( $configuration );\n";
 465+ }
 466+}
Property changes on: trunk/phase3/includes/resourceloader/ResourceLoader.php
___________________________________________________________________
Added: svn:mergeinfo
1467 Merged /branches/resourceloader/phase3/includes/ResourceLoader.php:r68366-69676,69678-71999,72001-72255,72257-72305,72307-72342
2468 Merged /branches/sqlite/includes/ResourceLoader.php:r58211-58321
3469 Merged /branches/new-installer/phase3/includes/ResourceLoader.php:r43664-66004
4470 Merged /branches/wmf-deployment/includes/ResourceLoader.php:r53381
5471 Merged /branches/REL1_15/phase3/includes/ResourceLoader.php:r51646
Added: svn:eol-style
6472 + native
Index: trunk/phase3/includes/resourceloader/ResourceLoaderContext.php
@@ -0,0 +1,138 @@
 2+<?php
 3+/**
 4+ * This program is free software; you can redistribute it and/or modify
 5+ * it under the terms of the GNU General Public License as published by
 6+ * the Free Software Foundation; either version 2 of the License, or
 7+ * (at your option) any later version.
 8+ *
 9+ * This program is distributed in the hope that it will be useful,
 10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12+ * GNU General Public License for more details.
 13+ *
 14+ * You should have received a copy of the GNU General Public License along
 15+ * with this program; if not, write to the Free Software Foundation, Inc.,
 16+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 17+ * http://www.gnu.org/copyleft/gpl.html
 18+ *
 19+ * @file
 20+ * @author Trevor Parscal
 21+ * @author Roan Kattouw
 22+ */
 23+
 24+defined( 'MEDIAWIKI' ) || die( 1 );
 25+
 26+/**
 27+ * Object passed around to modules which contains information about the state
 28+ * of a specific loader request
 29+ */
 30+class ResourceLoaderContext {
 31+
 32+ /* Protected Members */
 33+
 34+ protected $resourceLoader;
 35+ protected $request;
 36+ protected $modules;
 37+ protected $language;
 38+ protected $direction;
 39+ protected $skin;
 40+ protected $user;
 41+ protected $debug;
 42+ protected $only;
 43+ protected $version;
 44+ protected $hash;
 45+
 46+ /* Methods */
 47+
 48+ public function __construct( ResourceLoader $resourceLoader, WebRequest $request ) {
 49+ global $wgLang, $wgDefaultSkin, $wgResourceLoaderDebug;
 50+
 51+ $this->resourceLoader = $resourceLoader;
 52+ $this->request = $request;
 53+ // Interpret request
 54+ $modules = $request->getVal( 'modules' );
 55+ $this->modules = $modules ? explode( '|', $modules ) : array();
 56+ $this->language = $request->getVal( 'lang' );
 57+ $this->direction = $request->getVal( 'dir' );
 58+ $this->skin = $request->getVal( 'skin' );
 59+ $this->user = $request->getVal( 'user' );
 60+ $this->debug = $request->getFuzzyBool( 'debug', $wgResourceLoaderDebug );
 61+ $this->only = $request->getVal( 'only' );
 62+ $this->version = $request->getVal( 'version' );
 63+
 64+ // Fallback on system defaults
 65+ if ( !$this->language ) {
 66+ $this->language = $wgLang->getCode();
 67+ }
 68+
 69+ if ( !$this->direction ) {
 70+ $this->direction = Language::factory( $this->language )->getDir();
 71+ }
 72+
 73+ if ( !$this->skin ) {
 74+ $this->skin = $wgDefaultSkin;
 75+ }
 76+ }
 77+
 78+ public function getResourceLoader() {
 79+ return $this->resourceLoader;
 80+ }
 81+
 82+ public function getRequest() {
 83+ return $this->request;
 84+ }
 85+
 86+ public function getModules() {
 87+ return $this->modules;
 88+ }
 89+
 90+ public function getLanguage() {
 91+ return $this->language;
 92+ }
 93+
 94+ public function getDirection() {
 95+ return $this->direction;
 96+ }
 97+
 98+ public function getSkin() {
 99+ return $this->skin;
 100+ }
 101+
 102+ public function getUser() {
 103+ return $this->user;
 104+ }
 105+
 106+ public function getDebug() {
 107+ return $this->debug;
 108+ }
 109+
 110+ public function getOnly() {
 111+ return $this->only;
 112+ }
 113+
 114+ public function getVersion() {
 115+ return $this->version;
 116+ }
 117+
 118+ public function shouldIncludeScripts() {
 119+ return is_null( $this->only ) || $this->only === 'scripts';
 120+ }
 121+
 122+ public function shouldIncludeStyles() {
 123+ return is_null( $this->only ) || $this->only === 'styles';
 124+ }
 125+
 126+ public function shouldIncludeMessages() {
 127+ return is_null( $this->only ) || $this->only === 'messages';
 128+ }
 129+
 130+ public function getHash() {
 131+ if ( isset( $this->hash ) ) {
 132+ $this->hash = implode( '|', array(
 133+ $this->language, $this->direction, $this->skin, $this->user,
 134+ $this->debug, $this->only, $this->version
 135+ ) );
 136+ }
 137+ return $this->hash;
 138+ }
 139+}
Property changes on: trunk/phase3/includes/resourceloader/ResourceLoaderContext.php
___________________________________________________________________
Added: svn:eol-style
1140 + native
Index: trunk/phase3/includes/resourceloader/ResourceLoaderModule.php
@@ -0,0 +1,1168 @@
 2+<?php
 3+/**
 4+ * This program is free software; you can redistribute it and/or modify
 5+ * it under the terms of the GNU General Public License as published by
 6+ * the Free Software Foundation; either version 2 of the License, or
 7+ * (at your option) any later version.
 8+ *
 9+ * This program is distributed in the hope that it will be useful,
 10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
 11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 12+ * GNU General Public License for more details.
 13+ *
 14+ * You should have received a copy of the GNU General Public License along
 15+ * with this program; if not, write to the Free Software Foundation, Inc.,
 16+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 17+ * http://www.gnu.org/copyleft/gpl.html
 18+ *
 19+ * @file
 20+ * @author Trevor Parscal
 21+ * @author Roan Kattouw
 22+ */
 23+
 24+defined( 'MEDIAWIKI' ) || die( 1 );
 25+
 26+/**
 27+ * Abstraction for resource loader modules, with name registration and maxage functionality.
 28+ */
 29+abstract class ResourceLoaderModule {
 30+
 31+ /* Protected Members */
 32+
 33+ protected $name = null;
 34+
 35+ // In-object cache for file dependencies
 36+ protected $fileDeps = array();
 37+ // In-object cache for message blob mtime
 38+ protected $msgBlobMtime = array();
 39+
 40+ /* Methods */
 41+
 42+ /**
 43+ * Get this module's name. This is set when the module is registered
 44+ * with ResourceLoader::register()
 45+ *
 46+ * @return Mixed: name (string) or null if no name was set
 47+ */
 48+ public function getName() {
 49+ return $this->name;
 50+ }
 51+
 52+ /**
 53+ * Set this module's name. This is called by ResourceLodaer::register()
 54+ * when registering the module. Other code should not call this.
 55+ *
 56+ * @param $name String: name
 57+ */
 58+ public function setName( $name ) {
 59+ $this->name = $name;
 60+ }
 61+
 62+ /**
 63+ * Get whether CSS for this module should be flipped
 64+ */
 65+ public function getFlip( $context ) {
 66+ return $context->getDirection() === 'rtl';
 67+ }
 68+
 69+ /**
 70+ * Get all JS for this module for a given language and skin.
 71+ * Includes all relevant JS except loader scripts.
 72+ *
 73+ * @param $context ResourceLoaderContext object
 74+ * @return String: JS
 75+ */
 76+ public function getScript( ResourceLoaderContext $context ) {
 77+ // Stub, override expected
 78+ return '';
 79+ }
 80+
 81+ /**
 82+ * Get all CSS for this module for a given skin.
 83+ *
 84+ * @param $context ResourceLoaderContext object
 85+ * @return array: strings of CSS keyed by media type
 86+ */
 87+ public function getStyles( ResourceLoaderContext $context ) {
 88+ // Stub, override expected
 89+ return '';
 90+ }
 91+
 92+ /**
 93+ * Get the messages needed for this module.
 94+ *
 95+ * To get a JSON blob with messages, use MessageBlobStore::get()
 96+ *
 97+ * @return array of message keys. Keys may occur more than once
 98+ */
 99+ public function getMessages() {
 100+ // Stub, override expected
 101+ return array();
 102+ }
 103+
 104+ /**
 105+ * Get the group this module is in.
 106+ *
 107+ * @return string of group name
 108+ */
 109+ public function getGroup() {
 110+ // Stub, override expected
 111+ return null;
 112+ }
 113+
 114+ /**
 115+ * Get the loader JS for this module, if set.
 116+ *
 117+ * @return Mixed: loader JS (string) or false if no custom loader set
 118+ */
 119+ public function getLoaderScript() {
 120+ // Stub, override expected
 121+ return false;
 122+ }
 123+
 124+ /**
 125+ * Get a list of modules this module depends on.
 126+ *
 127+ * Dependency information is taken into account when loading a module
 128+ * on the client side. When adding a module on the server side,
 129+ * dependency information is NOT taken into account and YOU are
 130+ * responsible for adding dependent modules as well. If you don't do
 131+ * this, the client side loader will send a second request back to the
 132+ * server to fetch the missing modules, which kind of defeats the
 133+ * purpose of the resource loader.
 134+ *
 135+ * To add dependencies dynamically on the client side, use a custom
 136+ * loader script, see getLoaderScript()
 137+ * @return Array of module names (strings)
 138+ */
 139+ public function getDependencies() {
 140+ // Stub, override expected
 141+ return array();
 142+ }
 143+
 144+ /**
 145+ * Get the files this module depends on indirectly for a given skin.
 146+ * Currently these are only image files referenced by the module's CSS.
 147+ *
 148+ * @param $skin String: skin name
 149+ * @return array of files
 150+ */
 151+ public function getFileDependencies( $skin ) {
 152+ // Try in-object cache first
 153+ if ( isset( $this->fileDeps[$skin] ) ) {
 154+ return $this->fileDeps[$skin];
 155+ }
 156+
 157+ $dbr = wfGetDB( DB_SLAVE );
 158+ $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
 159+ 'md_module' => $this->getName(),
 160+ 'md_skin' => $skin,
 161+ ), __METHOD__
 162+ );
 163+ if ( !is_null( $deps ) ) {
 164+ return $this->fileDeps[$skin] = (array) FormatJson::decode( $deps, true );
 165+ }
 166+ return $this->fileDeps[$skin] = array();
 167+ }
 168+
 169+ /**
 170+ * Set preloaded file dependency information. Used so we can load this
 171+ * information for all modules at once.
 172+ * @param $skin string Skin name
 173+ * @param $deps array Array of file names
 174+ */
 175+ public function setFileDependencies( $skin, $deps ) {
 176+ $this->fileDeps[$skin] = $deps;
 177+ }
 178+
 179+ /**
 180+ * Get the last modification timestamp of the message blob for this
 181+ * module in a given language.
 182+ * @param $lang string Language code
 183+ * @return int UNIX timestamp, or 0 if no blob found
 184+ */
 185+ public function getMsgBlobMtime( $lang ) {
 186+ if ( !count( $this->getMessages() ) )
 187+ return 0;
 188+
 189+ $dbr = wfGetDB( DB_SLAVE );
 190+ $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
 191+ 'mr_resource' => $this->getName(),
 192+ 'mr_lang' => $lang
 193+ ), __METHOD__
 194+ );
 195+ $this->msgBlobMtime[$lang] = $msgBlobMtime ? wfTimestamp( TS_UNIX, $msgBlobMtime ) : 0;
 196+ return $this->msgBlobMtime[$lang];
 197+ }
 198+
 199+ /**
 200+ * Set a preloaded message blob last modification timestamp. Used so we
 201+ * can load this information for all modules at once.
 202+ * @param $lang string Language code
 203+ * @param $mtime int UNIX timestamp or 0 if there is no such blob
 204+ */
 205+ public function setMsgBlobMtime( $lang, $mtime ) {
 206+ $this->msgBlobMtime[$lang] = $mtime;
 207+ }
 208+
 209+ /* Abstract Methods */
 210+
 211+ /**
 212+ * Get this module's last modification timestamp for a given
 213+ * combination of language, skin and debug mode flag. This is typically
 214+ * the highest of each of the relevant components' modification
 215+ * timestamps. Whenever anything happens that changes the module's
 216+ * contents for these parameters, the mtime should increase.
 217+ *
 218+ * @param $context ResourceLoaderContext object
 219+ * @return int UNIX timestamp
 220+ */
 221+ public function getModifiedTime( ResourceLoaderContext $context ) {
 222+ // 0 would mean now
 223+ return 1;
 224+ }
 225+}
 226+
 227+/**
 228+ * Module based on local JS/CSS files. This is the most common type of module.
 229+ */
 230+class ResourceLoaderFileModule extends ResourceLoaderModule {
 231+ /* Protected Members */
 232+
 233+ protected $scripts = array();
 234+ protected $styles = array();
 235+ protected $messages = array();
 236+ protected $group;
 237+ protected $dependencies = array();
 238+ protected $debugScripts = array();
 239+ protected $languageScripts = array();
 240+ protected $skinScripts = array();
 241+ protected $skinStyles = array();
 242+ protected $loaders = array();
 243+ protected $parameters = array();
 244+
 245+ // In-object cache for file dependencies
 246+ protected $fileDeps = array();
 247+ // In-object cache for mtime
 248+ protected $modifiedTime = array();
 249+
 250+ /* Methods */
 251+
 252+ /**
 253+ * Construct a new module from an options array.
 254+ *
 255+ * @param $options array Options array. If empty, an empty module will be constructed
 256+ *
 257+ * $options format:
 258+ * array(
 259+ * // Required module options (mutually exclusive)
 260+ * 'scripts' => 'dir/script.js' | array( 'dir/script1.js', 'dir/script2.js' ... ),
 261+ *
 262+ * // Optional module options
 263+ * 'languageScripts' => array(
 264+ * '[lang name]' => 'dir/lang.js' | '[lang name]' => array( 'dir/lang1.js', 'dir/lang2.js' ... )
 265+ * ...
 266+ * ),
 267+ * 'skinScripts' => 'dir/skin.js' | array( 'dir/skin1.js', 'dir/skin2.js' ... ),
 268+ * 'debugScripts' => 'dir/debug.js' | array( 'dir/debug1.js', 'dir/debug2.js' ... ),
 269+ *
 270+ * // Non-raw module options
 271+ * 'dependencies' => 'module' | array( 'module1', 'module2' ... )
 272+ * 'loaderScripts' => 'dir/loader.js' | array( 'dir/loader1.js', 'dir/loader2.js' ... ),
 273+ * 'styles' => 'dir/file.css' | array( 'dir/file1.css', 'dir/file2.css' ... ), |
 274+ * array( 'dir/file1.css' => array( 'media' => 'print' ) ),
 275+ * 'skinStyles' => array(
 276+ * '[skin name]' => 'dir/skin.css' | array( 'dir/skin1.css', 'dir/skin2.css' ... ) |
 277+ * array( 'dir/file1.css' => array( 'media' => 'print' )
 278+ * ...
 279+ * ),
 280+ * 'messages' => array( 'message1', 'message2' ... ),
 281+ * 'group' => 'stuff',
 282+ * )
 283+ *
 284+ * @param $basePath String: base path to prepend to all paths in $options
 285+ */
 286+ public function __construct( $options = array(), $basePath = null ) {
 287+ foreach ( $options as $option => $value ) {
 288+ switch ( $option ) {
 289+ case 'scripts':
 290+ case 'debugScripts':
 291+ case 'languageScripts':
 292+ case 'skinScripts':
 293+ case 'loaders':
 294+ $this->{$option} = (array)$value;
 295+ // Automatically prefix script paths
 296+ if ( is_string( $basePath ) ) {
 297+ foreach ( $this->{$option} as $key => $value ) {
 298+ $this->{$option}[$key] = $basePath . $value;
 299+ }
 300+ }
 301+ break;
 302+ case 'styles':
 303+ case 'skinStyles':
 304+ $this->{$option} = (array)$value;
 305+ // Automatically prefix style paths
 306+ if ( is_string( $basePath ) ) {
 307+ foreach ( $this->{$option} as $key => $value ) {
 308+ if ( is_array( $value ) ) {
 309+ $this->{$option}[$basePath . $key] = $value;
 310+ unset( $this->{$option}[$key] );
 311+ } else {
 312+ $this->{$option}[$key] = $basePath . $value;
 313+ }
 314+ }
 315+ }
 316+ break;
 317+ case 'dependencies':
 318+ case 'messages':
 319+ $this->{$option} = (array)$value;
 320+ break;
 321+ case 'group':
 322+ $this->group = (string)$value;
 323+ break;
 324+ }
 325+ }
 326+ }
 327+
 328+ /**
 329+ * Add script files to this module. In order to be valid, a module
 330+ * must contain at least one script file.
 331+ *
 332+ * @param $scripts Mixed: path to script file (string) or array of paths
 333+ */
 334+ public function addScripts( $scripts ) {
 335+ $this->scripts = array_merge( $this->scripts, (array)$scripts );
 336+ }
 337+
 338+ /**
 339+ * Add style (CSS) files to this module.
 340+ *
 341+ * @param $styles Mixed: path to CSS file (string) or array of paths
 342+ */
 343+ public function addStyles( $styles ) {
 344+ $this->styles = array_merge( $this->styles, (array)$styles );
 345+ }
 346+
 347+ /**
 348+ * Add messages to this module.
 349+ *
 350+ * @param $messages Mixed: message key (string) or array of message keys
 351+ */
 352+ public function addMessages( $messages ) {
 353+ $this->messages = array_merge( $this->messages, (array)$messages );
 354+ }
 355+
 356+ /**
 357+ * Sets the group of this module.
 358+ *
 359+ * @param $group string group name
 360+ */
 361+ public function setGroup( $group ) {
 362+ $this->group = $group;
 363+ }
 364+
 365+ /**
 366+ * Add dependencies. Dependency information is taken into account when
 367+ * loading a module on the client side. When adding a module on the
 368+ * server side, dependency information is NOT taken into account and
 369+ * YOU are responsible for adding dependent modules as well. If you
 370+ * don't do this, the client side loader will send a second request
 371+ * back to the server to fetch the missing modules, which kind of
 372+ * defeats the point of using the resource loader in the first place.
 373+ *
 374+ * To add dependencies dynamically on the client side, use a custom
 375+ * loader (see addLoaders())
 376+ *
 377+ * @param $dependencies Mixed: module name (string) or array of module names
 378+ */
 379+ public function addDependencies( $dependencies ) {
 380+ $this->dependencies = array_merge( $this->dependencies, (array)$dependencies );
 381+ }
 382+
 383+ /**
 384+ * Add debug scripts to the module. These scripts are only included
 385+ * in debug mode.
 386+ *
 387+ * @param $scripts Mixed: path to script file (string) or array of paths
 388+ */
 389+ public function addDebugScripts( $scripts ) {
 390+ $this->debugScripts = array_merge( $this->debugScripts, (array)$scripts );
 391+ }
 392+
 393+ /**
 394+ * Add language-specific scripts. These scripts are only included for
 395+ * a given language.
 396+ *
 397+ * @param $lang String: language code
 398+ * @param $scripts Mixed: path to script file (string) or array of paths
 399+ */
 400+ public function addLanguageScripts( $lang, $scripts ) {
 401+ $this->languageScripts = array_merge_recursive(
 402+ $this->languageScripts,
 403+ array( $lang => $scripts )
 404+ );
 405+ }
 406+
 407+ /**
 408+ * Add skin-specific scripts. These scripts are only included for
 409+ * a given skin.
 410+ *
 411+ * @param $skin String: skin name, or 'default'
 412+ * @param $scripts Mixed: path to script file (string) or array of paths
 413+ */
 414+ public function addSkinScripts( $skin, $scripts ) {
 415+ $this->skinScripts = array_merge_recursive(
 416+ $this->skinScripts,
 417+ array( $skin => $scripts )
 418+ );
 419+ }
 420+
 421+ /**
 422+ * Add skin-specific CSS. These CSS files are only included for a
 423+ * given skin. If there are no skin-specific CSS files for a skin,
 424+ * the files defined for 'default' will be used, if any.
 425+ *
 426+ * @param $skin String: skin name, or 'default'
 427+ * @param $scripts Mixed: path to CSS file (string) or array of paths
 428+ */
 429+ public function addSkinStyles( $skin, $scripts ) {
 430+ $this->skinStyles = array_merge_recursive(
 431+ $this->skinStyles,
 432+ array( $skin => $scripts )
 433+ );
 434+ }
 435+
 436+ /**
 437+ * Add loader scripts. These scripts are loaded on every page and are
 438+ * responsible for registering this module using
 439+ * mediaWiki.loader.register(). If there are no loader scripts defined,
 440+ * the resource loader will register the module itself.
 441+ *
 442+ * Loader scripts are used to determine a module's dependencies
 443+ * dynamically on the client side (e.g. based on browser type/version).
 444+ * Note that loader scripts are included on every page, so they should
 445+ * be lightweight and use mediaWiki.loader.register()'s callback
 446+ * feature to defer dependency calculation.
 447+ *
 448+ * @param $scripts Mixed: path to script file (string) or array of paths
 449+ */
 450+ public function addLoaders( $scripts ) {
 451+ $this->loaders = array_merge( $this->loaders, (array)$scripts );
 452+ }
 453+
 454+ public function getScript( ResourceLoaderContext $context ) {
 455+ $retval = $this->getPrimaryScript() . "\n" .
 456+ $this->getLanguageScript( $context->getLanguage() ) . "\n" .
 457+ $this->getSkinScript( $context->getSkin() );
 458+
 459+ if ( $context->getDebug() ) {
 460+ $retval .= $this->getDebugScript();
 461+ }
 462+
 463+ return $retval;
 464+ }
 465+
 466+ public function getStyles( ResourceLoaderContext $context ) {
 467+ $styles = array();
 468+ foreach ( $this->getPrimaryStyles() as $media => $style ) {
 469+ if ( !isset( $styles[$media] ) ) {
 470+ $styles[$media] = '';
 471+ }
 472+ $styles[$media] .= $style;
 473+ }
 474+ foreach ( $this->getSkinStyles( $context->getSkin() ) as $media => $style ) {
 475+ if ( !isset( $styles[$media] ) ) {
 476+ $styles[$media] = '';
 477+ }
 478+ $styles[$media] .= $style;
 479+ }
 480+
 481+ // Collect referenced files
 482+ $files = array();
 483+ foreach ( $styles as $style ) {
 484+ // Extract and store the list of referenced files
 485+ $files = array_merge( $files, CSSMin::getLocalFileReferences( $style ) );
 486+ }
 487+
 488+ // Only store if modified
 489+ if ( $files !== $this->getFileDependencies( $context->getSkin() ) ) {
 490+ $encFiles = FormatJson::encode( $files );
 491+ $dbw = wfGetDB( DB_MASTER );
 492+ $dbw->replace( 'module_deps',
 493+ array( array( 'md_module', 'md_skin' ) ), array(
 494+ 'md_module' => $this->getName(),
 495+ 'md_skin' => $context->getSkin(),
 496+ 'md_deps' => $encFiles,
 497+ )
 498+ );
 499+ }
 500+
 501+ return $styles;
 502+ }
 503+
 504+ public function getMessages() {
 505+ return $this->messages;
 506+ }
 507+
 508+ public function getGroup() {
 509+ return $this->group;
 510+ }
 511+
 512+ public function getDependencies() {
 513+ return $this->dependencies;
 514+ }
 515+
 516+ public function getLoaderScript() {
 517+ if ( count( $this->loaders ) == 0 ) {
 518+ return false;
 519+ }
 520+
 521+ return self::concatScripts( $this->loaders );
 522+ }
 523+
 524+ /**
 525+ * Get the last modified timestamp of this module, which is calculated
 526+ * as the highest last modified timestamp of its constituent files and
 527+ * the files it depends on (see getFileDependencies()). Only files
 528+ * relevant to the given language and skin are taken into account, and
 529+ * files only relevant in debug mode are not taken into account when
 530+ * debug mode is off.
 531+ *
 532+ * @param $context ResourceLoaderContext object
 533+ * @return Integer: UNIX timestamp
 534+ */
 535+ public function getModifiedTime( ResourceLoaderContext $context ) {
 536+ if ( isset( $this->modifiedTime[$context->getHash()] ) ) {
 537+ return $this->modifiedTime[$context->getHash()];
 538+ }
 539+ wfProfileIn( __METHOD__ );
 540+
 541+ // Sort of nasty way we can get a flat list of files depended on by all styles
 542+ $styles = array();
 543+ foreach ( self::organizeFilesByOption( $this->styles, 'media', 'all' ) as $styleFiles ) {
 544+ $styles = array_merge( $styles, $styleFiles );
 545+ }
 546+ $skinFiles = (array) self::getSkinFiles(
 547+ $context->getSkin(), self::organizeFilesByOption( $this->skinStyles, 'media', 'all' )
 548+ );
 549+ foreach ( $skinFiles as $styleFiles ) {
 550+ $styles = array_merge( $styles, $styleFiles );
 551+ }
 552+
 553+ // Final merge, this should result in a master list of dependent files
 554+ $files = array_merge(
 555+ $this->scripts,
 556+ $styles,
 557+ $context->getDebug() ? $this->debugScripts : array(),
 558+ isset( $this->languageScripts[$context->getLanguage()] ) ?
 559+ (array) $this->languageScripts[$context->getLanguage()] : array(),
 560+ (array) self::getSkinFiles( $context->getSkin(), $this->skinScripts ),
 561+ $this->loaders,
 562+ $this->getFileDependencies( $context->getSkin() )
 563+ );
 564+
 565+ wfProfileIn( __METHOD__.'-filemtime' );
 566+ $filesMtime = max( array_map( 'filemtime', array_map( array( __CLASS__, 'remapFilename' ), $files ) ) );
 567+ wfProfileOut( __METHOD__.'-filemtime' );
 568+ $this->modifiedTime[$context->getHash()] = max( $filesMtime, $this->getMsgBlobMtime( $context->getLanguage() ) );
 569+ wfProfileOut( __METHOD__ );
 570+ return $this->modifiedTime[$context->getHash()];
 571+ }
 572+
 573+ /* Protected Members */
 574+
 575+ /**
 576+ * Get the primary JS for this module. This is pulled from the
 577+ * script files added through addScripts()
 578+ *
 579+ * @return String: JS
 580+ */
 581+ protected function getPrimaryScript() {
 582+ return self::concatScripts( $this->scripts );
 583+ }
 584+
 585+ /**
 586+ * Get the primary CSS for this module. This is pulled from the CSS
 587+ * files added through addStyles()
 588+ *
 589+ * @return Array
 590+ */
 591+ protected function getPrimaryStyles() {
 592+ return self::concatStyles( $this->styles );
 593+ }
 594+
 595+ /**
 596+ * Get the debug JS for this module. This is pulled from the script
 597+ * files added through addDebugScripts()
 598+ *
 599+ * @return String: JS
 600+ */
 601+ protected function getDebugScript() {
 602+ return self::concatScripts( $this->debugScripts );
 603+ }
 604+
 605+ /**
 606+ * Get the language-specific JS for a given language. This is pulled
 607+ * from the language-specific script files added through addLanguageScripts()
 608+ *
 609+ * @return String: JS
 610+ */
 611+ protected function getLanguageScript( $lang ) {
 612+ if ( !isset( $this->languageScripts[$lang] ) ) {
 613+ return '';
 614+ }
 615+ return self::concatScripts( $this->languageScripts[$lang] );
 616+ }
 617+
 618+ /**
 619+ * Get the skin-specific JS for a given skin. This is pulled from the
 620+ * skin-specific JS files added through addSkinScripts()
 621+ *
 622+ * @return String: JS
 623+ */
 624+ protected function getSkinScript( $skin ) {
 625+ return self::concatScripts( self::getSkinFiles( $skin, $this->skinScripts ) );
 626+ }
 627+
 628+ /**
 629+ * Get the skin-specific CSS for a given skin. This is pulled from the
 630+ * skin-specific CSS files added through addSkinStyles()
 631+ *
 632+ * @return Array: list of CSS strings keyed by media type
 633+ */
 634+ protected function getSkinStyles( $skin ) {
 635+ return self::concatStyles( self::getSkinFiles( $skin, $this->skinStyles ) );
 636+ }
 637+
 638+ /**
 639+ * Helper function to get skin-specific data from an array.
 640+ *
 641+ * @param $skin String: skin name
 642+ * @param $map Array: map of skin names to arrays
 643+ * @return $map[$skin] if set and non-empty, or $map['default'] if set, or an empty array
 644+ */
 645+ protected static function getSkinFiles( $skin, $map ) {
 646+ $retval = array();
 647+
 648+ if ( isset( $map[$skin] ) && $map[$skin] ) {
 649+ $retval = $map[$skin];
 650+ } else if ( isset( $map['default'] ) ) {
 651+ $retval = $map['default'];
 652+ }
 653+
 654+ return $retval;
 655+ }
 656+
 657+ /**
 658+ * Get the contents of a set of files and concatenate them, with
 659+ * newlines in between. Each file is used only once.
 660+ *
 661+ * @param $files Array of file names
 662+ * @return String: concatenated contents of $files
 663+ */
 664+ protected static function concatScripts( $files ) {
 665+ return implode( "\n",
 666+ array_map(
 667+ 'file_get_contents',
 668+ array_map(
 669+ array( __CLASS__, 'remapFilename' ),
 670+ array_unique( (array) $files ) ) ) );
 671+ }
 672+
 673+ protected static function organizeFilesByOption( $files, $option, $default ) {
 674+ $organizedFiles = array();
 675+ foreach ( (array) $files as $key => $value ) {
 676+ if ( is_int( $key ) ) {
 677+ // File name as the value
 678+ if ( !isset( $organizedFiles[$default] ) ) {
 679+ $organizedFiles[$default] = array();
 680+ }
 681+ $organizedFiles[$default][] = $value;
 682+ } else if ( is_array( $value ) ) {
 683+ // File name as the key, options array as the value
 684+ $media = isset( $value[$option] ) ? $value[$option] : $default;
 685+ if ( !isset( $organizedFiles[$media] ) ) {
 686+ $organizedFiles[$media] = array();
 687+ }
 688+ $organizedFiles[$media][] = $key;
 689+ }
 690+ }
 691+ return $organizedFiles;
 692+ }
 693+
 694+ /**
 695+ * Get the contents of a set of CSS files, remap then and concatenate
 696+ * them, with newlines in between. Each file is used only once.
 697+ *
 698+ * @param $styles Array of file names
 699+ * @return Array: list of concatenated and remapped contents of $files keyed by media type
 700+ */
 701+ protected static function concatStyles( $styles ) {
 702+ $styles = self::organizeFilesByOption( $styles, 'media', 'all' );
 703+ foreach ( $styles as $media => $files ) {
 704+ $styles[$media] =
 705+ implode( "\n",
 706+ array_map(
 707+ array( __CLASS__, 'remapStyle' ),
 708+ array_unique( (array) $files ) ) );
 709+ }
 710+ return $styles;
 711+ }
 712+
 713+ /**
 714+ * Remap a relative to $IP. Used as a callback for array_map()
 715+ *
 716+ * @param $file String: file name
 717+ * @return string $IP/$file
 718+ */
 719+ protected static function remapFilename( $file ) {
 720+ global $IP;
 721+
 722+ return "$IP/$file";
 723+ }
 724+
 725+ /**
 726+ * Get the contents of a CSS file and run it through CSSMin::remap().
 727+ * This wrapper is needed so we can use array_map() in concatStyles()
 728+ *
 729+ * @param $file String: file name
 730+ * @return string Remapped CSS
 731+ */
 732+ protected static function remapStyle( $file ) {
 733+ global $wgScriptPath;
 734+ return CSSMin::remap(
 735+ file_get_contents( self::remapFilename( $file ) ),
 736+ dirname( $file ),
 737+ $wgScriptPath . '/' . dirname( $file ),
 738+ true
 739+ );
 740+ }
 741+}
 742+
 743+/**
 744+ * Abstraction for resource loader modules which pull from wiki pages
 745+ *
 746+ * This can only be used for wiki pages in the MediaWiki and User namespaces, because of it's dependence on the
 747+ * functionality of Title::isValidCssJsSubpage.
 748+ */
 749+abstract class ResourceLoaderWikiModule extends ResourceLoaderModule {
 750+
 751+ /* Protected Members */
 752+
 753+ // In-object cache for modified time
 754+ protected $modifiedTime = array();
 755+
 756+ /* Abstract Protected Methods */
 757+
 758+ abstract protected function getPages( ResourceLoaderContext $context );
 759+
 760+ /* Protected Methods */
 761+
 762+ protected function getContent( $page, $ns ) {
 763+ if ( $ns === NS_MEDIAWIKI ) {
 764+ return wfEmptyMsg( $page ) ? '' : wfMsgExt( $page, 'content' );
 765+ }
 766+ if ( $title = Title::newFromText( $page, $ns ) ) {
 767+ if ( $title->isValidCssJsSubpage() && $revision = Revision::newFromTitle( $title ) ) {
 768+ return $revision->getRawText();
 769+ }
 770+ }
 771+ return null;
 772+ }
 773+
 774+ /* Methods */
 775+
 776+ public function getScript( ResourceLoaderContext $context ) {
 777+ $scripts = '';
 778+ foreach ( $this->getPages( $context ) as $page => $options ) {
 779+ if ( $options['type'] === 'script' ) {
 780+ if ( $script = $this->getContent( $page, $options['ns'] ) ) {
 781+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 782+ $scripts .= "/*$ns:$page */\n$script\n";
 783+ }
 784+ }
 785+ }
 786+ return $scripts;
 787+ }
 788+
 789+ public function getStyles( ResourceLoaderContext $context ) {
 790+
 791+ $styles = array();
 792+ foreach ( $this->getPages( $context ) as $page => $options ) {
 793+ if ( $options['type'] === 'style' ) {
 794+ $media = isset( $options['media'] ) ? $options['media'] : 'all';
 795+ if ( $style = $this->getContent( $page, $options['ns'] ) ) {
 796+ if ( !isset( $styles[$media] ) ) {
 797+ $styles[$media] = '';
 798+ }
 799+ $ns = MWNamespace::getCanonicalName( $options['ns'] );
 800+ $styles[$media] .= "/* $ns:$page */\n$style\n";
 801+ }
 802+ }
 803+ }
 804+ return $styles;
 805+ }
 806+
 807+ public function getModifiedTime( ResourceLoaderContext $context ) {
 808+ $hash = $context->getHash();
 809+ if ( isset( $this->modifiedTime[$hash] ) ) {
 810+ return $this->modifiedTime[$hash];
 811+ }
 812+
 813+ $titles = array();
 814+ foreach ( $this->getPages( $context ) as $page => $options ) {
 815+ $titles[$options['ns']][$page] = true;
 816+ }
 817+
 818+ $modifiedTime = 1; // wfTimestamp() interprets 0 as "now"
 819+
 820+ if ( $titles ) {
 821+ $dbr = wfGetDB( DB_SLAVE );
 822+ $latest = $dbr->selectField( 'page', 'MAX(page_touched)',
 823+ $dbr->makeWhereFrom2d( $titles, 'page_namespace', 'page_title' ),
 824+ __METHOD__ );
 825+
 826+ if ( $latest ) {
 827+ $modifiedTime = wfTimestamp( TS_UNIX, $latest );
 828+ }
 829+ }
 830+
 831+ return $this->modifiedTime[$hash] = $modifiedTime;
 832+ }
 833+}
 834+
 835+/**
 836+ * Module for site customizations
 837+ */
 838+class ResourceLoaderSiteModule extends ResourceLoaderWikiModule {
 839+
 840+ /* Protected Methods */
 841+
 842+ protected function getPages( ResourceLoaderContext $context ) {
 843+ global $wgHandheldStyle;
 844+
 845+ $pages = array(
 846+ 'Common.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 847+ 'Common.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 848+ ucfirst( $context->getSkin() ) . '.js' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'script' ),
 849+ ucfirst( $context->getSkin() ) . '.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style' ),
 850+ 'Print.css' => array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'print' ),
 851+ );
 852+ if ( $wgHandheldStyle ) {
 853+ $pages['Handheld.css'] = array( 'ns' => NS_MEDIAWIKI, 'type' => 'style', 'media' => 'handheld' );
 854+ }
 855+ return $pages;
 856+ }
 857+
 858+ /* Methods */
 859+
 860+ public function getGroup() {
 861+ return 'site';
 862+ }
 863+}
 864+
 865+/**
 866+ * Module for user customizations
 867+ */
 868+class ResourceLoaderUserModule extends ResourceLoaderWikiModule {
 869+
 870+ /* Protected Methods */
 871+
 872+ protected function getPages( ResourceLoaderContext $context ) {
 873+ global $wgAllowUserCss;
 874+
 875+ if ( $context->getUser() && $wgAllowUserCss ) {
 876+ $username = $context->getUser();
 877+ return array(
 878+ "$username/common.js" => array( 'ns' => NS_USER, 'type' => 'script' ),
 879+ "$username/" . $context->getSkin() . '.js' => array( 'ns' => NS_USER, 'type' => 'script' ),
 880+ "$username/common.css" => array( 'ns' => NS_USER, 'type' => 'style' ),
 881+ "$username/" . $context->getSkin() . '.css' => array( 'ns' => NS_USER, 'type' => 'style' ),
 882+ );
 883+ }
 884+ return array();
 885+ }
 886+
 887+ /* Methods */
 888+
 889+ public function getGroup() {
 890+ return 'user';
 891+ }
 892+}
 893+
 894+/**
 895+ * Module for user preference customizations
 896+ */
 897+class ResourceLoaderUserOptionsModule extends ResourceLoaderModule {
 898+
 899+ /* Protected Members */
 900+
 901+ protected $modifiedTime = array();
 902+
 903+ /* Methods */
 904+
 905+ public function getModifiedTime( ResourceLoaderContext $context ) {
 906+ $hash = $context->getHash();
 907+ if ( isset( $this->modifiedTime[$hash] ) ) {
 908+ return $this->modifiedTime[$hash];
 909+ }
 910+
 911+ global $wgUser;
 912+
 913+ if ( $context->getUser() === $wgUser->getName() ) {
 914+ return $this->modifiedTime[$hash] = $wgUser->getTouched();
 915+ } else {
 916+ return 1;
 917+ }
 918+ }
 919+
 920+ /**
 921+ * Fetch the context's user options, or if it doesn't match current user,
 922+ * the default options.
 923+ *
 924+ * @param $context ResourceLoaderContext
 925+ * @return array
 926+ */
 927+ protected function contextUserOptions( ResourceLoaderContext $context ) {
 928+ global $wgUser;
 929+
 930+ // Verify identity -- this is a private module
 931+ if ( $context->getUser() === $wgUser->getName() ) {
 932+ return $wgUser->getOptions();
 933+ } else {
 934+ return User::getDefaultOptions();
 935+ }
 936+ }
 937+
 938+ public function getScript( ResourceLoaderContext $context ) {
 939+ $encOptions = FormatJson::encode( $this->contextUserOptions( $context ) );
 940+ return "mediaWiki.user.options.set( $encOptions );";
 941+ }
 942+
 943+ public function getStyles( ResourceLoaderContext $context ) {
 944+ global $wgAllowUserCssPrefs;
 945+
 946+ if ( $wgAllowUserCssPrefs ) {
 947+ $options = $this->contextUserOptions( $context );
 948+
 949+ // Build CSS rules
 950+ $rules = array();
 951+ if ( $options['underline'] < 2 ) {
 952+ $rules[] = "a { text-decoration: " . ( $options['underline'] ? 'underline' : 'none' ) . "; }";
 953+ }
 954+ if ( $options['highlightbroken'] ) {
 955+ $rules[] = "a.new, #quickbar a.new { color: #ba0000; }\n";
 956+ } else {
 957+ $rules[] = "a.new, #quickbar a.new, a.stub, #quickbar a.stub { color: inherit; }";
 958+ $rules[] = "a.new:after, #quickbar a.new:after { content: '?'; color: #ba0000; }";
 959+ $rules[] = "a.stub:after, #quickbar a.stub:after { content: '!'; color: #772233; }";
 960+ }
 961+ if ( $options['justify'] ) {
 962+ $rules[] = "#article, #bodyContent, #mw_content { text-align: justify; }\n";
 963+ }
 964+ if ( !$options['showtoc'] ) {
 965+ $rules[] = "#toc { display: none; }\n";
 966+ }
 967+ if ( !$options['editsection'] ) {
 968+ $rules[] = ".editsection { display: none; }\n";
 969+ }
 970+ if ( $options['editfont'] !== 'default' ) {
 971+ $rules[] = "textarea { font-family: {$options['editfont']}; }\n";
 972+ }
 973+ return array( 'all' => implode( "\n", $rules ) );
 974+ }
 975+ return array();
 976+ }
 977+
 978+ public function getFlip( $context ) {
 979+ global $wgContLang;
 980+
 981+ return $wgContLang->getDir() !== $context->getDirection();
 982+ }
 983+
 984+ public function getGroup() {
 985+ return 'private';
 986+ }
 987+}
 988+
 989+class ResourceLoaderStartUpModule extends ResourceLoaderModule {
 990+ /* Protected Members */
 991+
 992+ protected $modifiedTime = array();
 993+
 994+ /* Protected Methods */
 995+
 996+ protected function getConfig( $context ) {
 997+ global $wgLoadScript, $wgScript, $wgStylePath, $wgScriptExtension,
 998+ $wgArticlePath, $wgScriptPath, $wgServer, $wgContLang, $wgBreakFrames,
 999+ $wgVariantArticlePath, $wgActionPaths, $wgUseAjax, $wgVersion,
 1000+ $wgEnableAPI, $wgEnableWriteAPI, $wgDBname, $wgEnableMWSuggest,
 1001+ $wgSitename, $wgFileExtensions;
 1002+
 1003+ // Pre-process information
 1004+ $separatorTransTable = $wgContLang->separatorTransformTable();
 1005+ $separatorTransTable = $separatorTransTable ? $separatorTransTable : array();
 1006+ $compactSeparatorTransTable = array(
 1007+ implode( "\t", array_keys( $separatorTransTable ) ),
 1008+ implode( "\t", $separatorTransTable ),
 1009+ );
 1010+ $digitTransTable = $wgContLang->digitTransformTable();
 1011+ $digitTransTable = $digitTransTable ? $digitTransTable : array();
 1012+ $compactDigitTransTable = array(
 1013+ implode( "\t", array_keys( $digitTransTable ) ),
 1014+ implode( "\t", $digitTransTable ),
 1015+ );
 1016+ $mainPage = Title::newMainPage();
 1017+
 1018+ // Build list of variables
 1019+ $vars = array(
 1020+ 'wgLoadScript' => $wgLoadScript,
 1021+ 'debug' => $context->getDebug(),
 1022+ 'skin' => $context->getSkin(),
 1023+ 'stylepath' => $wgStylePath,
 1024+ 'wgUrlProtocols' => wfUrlProtocols(),
 1025+ 'wgArticlePath' => $wgArticlePath,
 1026+ 'wgScriptPath' => $wgScriptPath,
 1027+ 'wgScriptExtension' => $wgScriptExtension,
 1028+ 'wgScript' => $wgScript,
 1029+ 'wgVariantArticlePath' => $wgVariantArticlePath,
 1030+ 'wgActionPaths' => $wgActionPaths,
 1031+ 'wgServer' => $wgServer,
 1032+ 'wgUserLanguage' => $context->getLanguage(),
 1033+ 'wgContentLanguage' => $wgContLang->getCode(),
 1034+ 'wgBreakFrames' => $wgBreakFrames,
 1035+ 'wgVersion' => $wgVersion,
 1036+ 'wgEnableAPI' => $wgEnableAPI,
 1037+ 'wgEnableWriteAPI' => $wgEnableWriteAPI,
 1038+ 'wgSeparatorTransformTable' => $compactSeparatorTransTable,
 1039+ 'wgDigitTransformTable' => $compactDigitTransTable,
 1040+ 'wgMainPageTitle' => $mainPage ? $mainPage->getPrefixedText() : null,
 1041+ 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
 1042+ 'wgNamespaceIds' => $wgContLang->getNamespaceIds(),
 1043+ 'wgSiteName' => $wgSitename,
 1044+ 'wgFileExtensions' => $wgFileExtensions,
 1045+ 'wgDBname' => $wgDBname,
 1046+ );
 1047+ if ( $wgContLang->hasVariants() ) {
 1048+ $vars['wgUserVariant'] = $wgContLang->getPreferredVariant();
 1049+ }
 1050+ if ( $wgUseAjax && $wgEnableMWSuggest ) {
 1051+ $vars['wgMWSuggestTemplate'] = SearchEngine::getMWSuggestTemplate();
 1052+ }
 1053+
 1054+ return $vars;
 1055+ }
 1056+
 1057+ /**
 1058+ * Gets registration code for all modules
 1059+ *
 1060+ * @param $context ResourceLoaderContext object
 1061+ * @return String: JavaScript code for registering all modules with the client loader
 1062+ */
 1063+ public static function getModuleRegistrations( ResourceLoaderContext $context ) {
 1064+ global $wgCacheEpoch;
 1065+ wfProfileIn( __METHOD__ );
 1066+
 1067+ $out = '';
 1068+ $registrations = array();
 1069+ foreach ( $context->getResourceLoader()->getModules() as $name => $module ) {
 1070+ // Support module loader scripts
 1071+ if ( ( $loader = $module->getLoaderScript() ) !== false ) {
 1072+ $deps = $module->getDependencies();
 1073+ $group = $module->getGroup();
 1074+ $version = wfTimestamp( TS_ISO_8601_BASIC, round( $module->getModifiedTime( $context ), -2 ) );
 1075+ $out .= ResourceLoader::makeCustomLoaderScript( $name, $version, $deps, $group, $loader );
 1076+ }
 1077+ // Automatically register module
 1078+ else {
 1079+ $mtime = max( $module->getModifiedTime( $context ), wfTimestamp( TS_UNIX, $wgCacheEpoch ) );
 1080+ // Modules without dependencies or a group pass two arguments (name, timestamp) to
 1081+ // mediaWiki.loader.register()
 1082+ if ( !count( $module->getDependencies() && $module->getGroup() === null ) ) {
 1083+ $registrations[] = array( $name, $mtime );
 1084+ }
 1085+ // Modules with dependencies but no group pass three arguments (name, timestamp, dependencies)
 1086+ // to mediaWiki.loader.register()
 1087+ else if ( $module->getGroup() === null ) {
 1088+ $registrations[] = array(
 1089+ $name, $mtime, $module->getDependencies() );
 1090+ }
 1091+ // Modules with dependencies pass four arguments (name, timestamp, dependencies, group)
 1092+ // to mediaWiki.loader.register()
 1093+ else {
 1094+ $registrations[] = array(
 1095+ $name, $mtime, $module->getDependencies(), $module->getGroup() );
 1096+ }
 1097+ }
 1098+ }
 1099+ $out .= ResourceLoader::makeLoaderRegisterScript( $registrations );
 1100+
 1101+ wfProfileOut( __METHOD__ );
 1102+ return $out;
 1103+ }
 1104+
 1105+ /* Methods */
 1106+
 1107+ public function getScript( ResourceLoaderContext $context ) {
 1108+ global $IP, $wgLoadScript;
 1109+
 1110+ $out = file_get_contents( "$IP/resources/startup.js" );
 1111+ if ( $context->getOnly() === 'scripts' ) {
 1112+ // Build load query for jquery and mediawiki modules
 1113+ $query = array(
 1114+ 'modules' => implode( '|', array( 'jquery', 'mediawiki' ) ),
 1115+ 'only' => 'scripts',
 1116+ 'lang' => $context->getLanguage(),
 1117+ 'skin' => $context->getSkin(),
 1118+ 'debug' => $context->getDebug() ? 'true' : 'false',
 1119+ 'version' => wfTimestamp( TS_ISO_8601_BASIC, round( max(
 1120+ $context->getResourceLoader()->getModule( 'jquery' )->getModifiedTime( $context ),
 1121+ $context->getResourceLoader()->getModule( 'mediawiki' )->getModifiedTime( $context )
 1122+ ), -2 ) )
 1123+ );
 1124+ // Ensure uniform query order
 1125+ ksort( $query );
 1126+
 1127+ // Startup function
 1128+ $configuration = FormatJson::encode( $this->getConfig( $context ) );
 1129+ $registrations = self::getModuleRegistrations( $context );
 1130+ $out .= "var startUp = function() {\n\t$registrations\n\tmediaWiki.config.set( $configuration );\n};";
 1131+
 1132+ // Conditional script injection
 1133+ $scriptTag = Xml::escapeJsString( Html::linkedScript( $wgLoadScript . '?' . wfArrayToCGI( $query ) ) );
 1134+ $out .= "if ( isCompatible() ) {\n\tdocument.write( '$scriptTag' );\n}\ndelete isCompatible;";
 1135+ }
 1136+
 1137+ return $out;
 1138+ }
 1139+
 1140+ public function getModifiedTime( ResourceLoaderContext $context ) {
 1141+ global $IP, $wgCacheEpoch;
 1142+
 1143+ $hash = $context->getHash();
 1144+ if ( isset( $this->modifiedTime[$hash] ) ) {
 1145+ return $this->modifiedTime[$hash];
 1146+ }
 1147+ $this->modifiedTime[$hash] = filemtime( "$IP/resources/startup.js" );
 1148+
 1149+ // ATTENTION!: Because of the line above, this is not going to cause infinite recursion - think carefully
 1150+ // before making changes to this code!
 1151+ $time = wfTimestamp( TS_UNIX, $wgCacheEpoch );
 1152+ foreach ( $context->getResourceLoader()->getModules() as $module ) {
 1153+ $time = max( $time, $module->getModifiedTime( $context ) );
 1154+ }
 1155+ return $this->modifiedTime[$hash] = $time;
 1156+ }
 1157+
 1158+ public function getFlip( $context ) {
 1159+ global $wgContLang;
 1160+
 1161+ return $wgContLang->getDir() !== $context->getDirection();
 1162+ }
 1163+
 1164+ /* Methods */
 1165+
 1166+ public function getGroup() {
 1167+ return 'startup';
 1168+ }
 1169+}
Property changes on: trunk/phase3/includes/resourceloader/ResourceLoaderModule.php
___________________________________________________________________
Added: svn:eol-style
11170 + native
Index: trunk/phase3/includes/AutoLoader.php
@@ -197,15 +197,15 @@
198198 'RegexlikeReplacer' => 'includes/StringUtils.php',
199199 'ReplacementArray' => 'includes/StringUtils.php',
200200 'Replacer' => 'includes/StringUtils.php',
201 - 'ResourceLoader' => 'includes/ResourceLoader.php',
202 - 'ResourceLoaderContext' => 'includes/ResourceLoaderContext.php',
203 - 'ResourceLoaderModule' => 'includes/ResourceLoaderModule.php',
204 - 'ResourceLoaderWikiModule' => 'includes/ResourceLoaderModule.php',
205 - 'ResourceLoaderFileModule' => 'includes/ResourceLoaderModule.php',
206 - 'ResourceLoaderSiteModule' => 'includes/ResourceLoaderModule.php',
207 - 'ResourceLoaderUserModule' => 'includes/ResourceLoaderModule.php',
208 - 'ResourceLoaderUserOptionsModule' => 'includes/ResourceLoaderModule.php',
209 - 'ResourceLoaderStartUpModule' => 'includes/ResourceLoaderModule.php',
 201+ 'ResourceLoader' => 'includes/resourceloader/ResourceLoader.php',
 202+ 'ResourceLoaderContext' => 'includes/resourceloader/ResourceLoaderContext.php',
 203+ 'ResourceLoaderModule' => 'includes/resourceloader/ResourceLoaderModule.php',
 204+ 'ResourceLoaderWikiModule' => 'includes/resourceloader/ResourceLoaderModule.php',
 205+ 'ResourceLoaderFileModule' => 'includes/resourceloader/ResourceLoaderModule.php',
 206+ 'ResourceLoaderSiteModule' => 'includes/resourceloader/ResourceLoaderModule.php',
 207+ 'ResourceLoaderUserModule' => 'includes/resourceloader/ResourceLoaderModule.php',
 208+ 'ResourceLoaderUserOptionsModule' => 'includes/resourceloader/ResourceLoaderModule.php',
 209+ 'ResourceLoaderStartUpModule' => 'includes/resourceloader/ResourceLoaderModule.php',
210210 'ReverseChronologicalPager' => 'includes/Pager.php',
211211 'Revision' => 'includes/Revision.php',
212212 'RevisionDelete' => 'includes/RevisionDelete.php',

Status & tagging log