Merge "Timestamp from Year/Month selector on forms should be wiki time"
[lhc/web/wiklou.git] / includes / resourceloader / ResourceLoaderModule.php
1 <?php
2 /**
3 * Abstraction for resource loader modules.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @author Trevor Parscal
22 * @author Roan Kattouw
23 */
24
25 /**
26 * Abstraction for resource loader modules, with name registration and maxage functionality.
27 */
28 abstract class ResourceLoaderModule {
29 # Type of resource
30 const TYPE_SCRIPTS = 'scripts';
31 const TYPE_STYLES = 'styles';
32 const TYPE_COMBINED = 'combined';
33
34 # sitewide core module like a skin file or jQuery component
35 const ORIGIN_CORE_SITEWIDE = 1;
36
37 # per-user module generated by the software
38 const ORIGIN_CORE_INDIVIDUAL = 2;
39
40 # sitewide module generated from user-editable files, like MediaWiki:Common.js, or
41 # modules accessible to multiple users, such as those generated by the Gadgets extension.
42 const ORIGIN_USER_SITEWIDE = 3;
43
44 # per-user module generated from user-editable files, like User:Me/vector.js
45 const ORIGIN_USER_INDIVIDUAL = 4;
46
47 # an access constant; make sure this is kept as the largest number in this group
48 const ORIGIN_ALL = 10;
49
50 # script and style modules form a hierarchy of trustworthiness, with core modules like
51 # skins and jQuery as most trustworthy, and user scripts as least trustworthy. We can
52 # limit the types of scripts and styles we allow to load on, say, sensitive special
53 # pages like Special:UserLogin and Special:Preferences
54 protected $origin = self::ORIGIN_CORE_SITEWIDE;
55
56 /* Protected Members */
57
58 protected $name = null;
59 protected $targets = array( 'desktop' );
60
61 // In-object cache for file dependencies
62 protected $fileDeps = array();
63 // In-object cache for message blob mtime
64 protected $msgBlobMtime = array();
65
66 /**
67 * @var Config
68 */
69 protected $config;
70
71 /* Methods */
72
73 /**
74 * Get this module's name. This is set when the module is registered
75 * with ResourceLoader::register()
76 *
77 * @return string|null Name (string) or null if no name was set
78 */
79 public function getName() {
80 return $this->name;
81 }
82
83 /**
84 * Set this module's name. This is called by ResourceLoader::register()
85 * when registering the module. Other code should not call this.
86 *
87 * @param string $name Name
88 */
89 public function setName( $name ) {
90 $this->name = $name;
91 }
92
93 /**
94 * Get this module's origin. This is set when the module is registered
95 * with ResourceLoader::register()
96 *
97 * @return int ResourceLoaderModule class constant, the subclass default
98 * if not set manually
99 */
100 public function getOrigin() {
101 return $this->origin;
102 }
103
104 /**
105 * Set this module's origin. This is called by ResourceLoader::register()
106 * when registering the module. Other code should not call this.
107 *
108 * @param int $origin Origin
109 */
110 public function setOrigin( $origin ) {
111 $this->origin = $origin;
112 }
113
114 /**
115 * @param ResourceLoaderContext $context
116 * @return bool
117 */
118 public function getFlip( $context ) {
119 global $wgContLang;
120
121 return $wgContLang->getDir() !== $context->getDirection();
122 }
123
124 /**
125 * Get all JS for this module for a given language and skin.
126 * Includes all relevant JS except loader scripts.
127 *
128 * @param ResourceLoaderContext $context
129 * @return string JavaScript code
130 */
131 public function getScript( ResourceLoaderContext $context ) {
132 // Stub, override expected
133 return '';
134 }
135
136 /**
137 * Takes named templates by the module and returns an array mapping.
138 *
139 * @return array of templates mapping template alias to content
140 */
141 public function getTemplates() {
142 // Stub, override expected.
143 return array();
144 }
145
146 /**
147 * @return Config
148 * @since 1.24
149 */
150 public function getConfig() {
151 if ( $this->config === null ) {
152 // Ugh, fall back to default
153 $this->config = ConfigFactory::getDefaultInstance()->makeConfig( 'main' );
154 }
155
156 return $this->config;
157 }
158
159 /**
160 * @param Config $config
161 * @since 1.24
162 */
163 public function setConfig( Config $config ) {
164 $this->config = $config;
165 }
166
167 /**
168 * Get the URL or URLs to load for this module's JS in debug mode.
169 * The default behavior is to return a load.php?only=scripts URL for
170 * the module, but file-based modules will want to override this to
171 * load the files directly.
172 *
173 * This function is called only when 1) we're in debug mode, 2) there
174 * is no only= parameter and 3) supportsURLLoading() returns true.
175 * #2 is important to prevent an infinite loop, therefore this function
176 * MUST return either an only= URL or a non-load.php URL.
177 *
178 * @param ResourceLoaderContext $context
179 * @return array Array of URLs
180 */
181 public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
182 $resourceLoader = $context->getResourceLoader();
183 $derivative = new DerivativeResourceLoaderContext( $context );
184 $derivative->setModules( array( $this->getName() ) );
185 $derivative->setOnly( 'scripts' );
186 $derivative->setDebug( true );
187
188 $url = $resourceLoader->createLoaderURL(
189 $this->getSource(),
190 $derivative
191 );
192
193 return array( $url );
194 }
195
196 /**
197 * Whether this module supports URL loading. If this function returns false,
198 * getScript() will be used even in cases (debug mode, no only param) where
199 * getScriptURLsForDebug() would normally be used instead.
200 * @return bool
201 */
202 public function supportsURLLoading() {
203 return true;
204 }
205
206 /**
207 * Get all CSS for this module for a given skin.
208 *
209 * @param ResourceLoaderContext $context
210 * @return array List of CSS strings or array of CSS strings keyed by media type.
211 * like array( 'screen' => '.foo { width: 0 }' );
212 * or array( 'screen' => array( '.foo { width: 0 }' ) );
213 */
214 public function getStyles( ResourceLoaderContext $context ) {
215 // Stub, override expected
216 return array();
217 }
218
219 /**
220 * Get the URL or URLs to load for this module's CSS in debug mode.
221 * The default behavior is to return a load.php?only=styles URL for
222 * the module, but file-based modules will want to override this to
223 * load the files directly. See also getScriptURLsForDebug()
224 *
225 * @param ResourceLoaderContext $context
226 * @return array Array( mediaType => array( URL1, URL2, ... ), ... )
227 */
228 public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
229 $resourceLoader = $context->getResourceLoader();
230 $derivative = new DerivativeResourceLoaderContext( $context );
231 $derivative->setModules( array( $this->getName() ) );
232 $derivative->setOnly( 'styles' );
233 $derivative->setDebug( true );
234
235 $url = $resourceLoader->createLoaderURL(
236 $this->getSource(),
237 $derivative
238 );
239
240 return array( 'all' => array( $url ) );
241 }
242
243 /**
244 * Get the messages needed for this module.
245 *
246 * To get a JSON blob with messages, use MessageBlobStore::get()
247 *
248 * @return array List of message keys. Keys may occur more than once
249 */
250 public function getMessages() {
251 // Stub, override expected
252 return array();
253 }
254
255 /**
256 * Get the group this module is in.
257 *
258 * @return string Group name
259 */
260 public function getGroup() {
261 // Stub, override expected
262 return null;
263 }
264
265 /**
266 * Get the origin of this module. Should only be overridden for foreign modules.
267 *
268 * @return string Origin name, 'local' for local modules
269 */
270 public function getSource() {
271 // Stub, override expected
272 return 'local';
273 }
274
275 /**
276 * Where on the HTML page should this module's JS be loaded?
277 * - 'top': in the "<head>"
278 * - 'bottom': at the bottom of the "<body>"
279 *
280 * @return string
281 */
282 public function getPosition() {
283 return 'bottom';
284 }
285
286 /**
287 * Whether this module's JS expects to work without the client-side ResourceLoader module.
288 * Returning true from this function will prevent mw.loader.state() call from being
289 * appended to the bottom of the script.
290 *
291 * @return bool
292 */
293 public function isRaw() {
294 return false;
295 }
296
297 /**
298 * Get the loader JS for this module, if set.
299 *
300 * @return mixed JavaScript loader code as a string or boolean false if no custom loader set
301 */
302 public function getLoaderScript() {
303 // Stub, override expected
304 return false;
305 }
306
307 /**
308 * Get a list of modules this module depends on.
309 *
310 * Dependency information is taken into account when loading a module
311 * on the client side.
312 *
313 * To add dependencies dynamically on the client side, use a custom
314 * loader script, see getLoaderScript()
315 * @return array List of module names as strings
316 */
317 public function getDependencies() {
318 // Stub, override expected
319 return array();
320 }
321
322 /**
323 * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
324 *
325 * @return array Array of strings
326 */
327 public function getTargets() {
328 return $this->targets;
329 }
330
331 /**
332 * Get the skip function.
333 *
334 * Modules that provide fallback functionality can provide a "skip function". This
335 * function, if provided, will be passed along to the module registry on the client.
336 * When this module is loaded (either directly or as a dependency of another module),
337 * then this function is executed first. If the function returns true, the module will
338 * instantly be considered "ready" without requesting the associated module resources.
339 *
340 * The value returned here must be valid javascript for execution in a private function.
341 * It must not contain the "function () {" and "}" wrapper though.
342 *
343 * @return string|null A JavaScript function body returning a boolean value, or null
344 */
345 public function getSkipFunction() {
346 return null;
347 }
348
349 /**
350 * Get the files this module depends on indirectly for a given skin.
351 * Currently these are only image files referenced by the module's CSS.
352 *
353 * @param string $skin Skin name
354 * @return array List of files
355 */
356 public function getFileDependencies( $skin ) {
357 // Try in-object cache first
358 if ( isset( $this->fileDeps[$skin] ) ) {
359 return $this->fileDeps[$skin];
360 }
361
362 $dbr = wfGetDB( DB_SLAVE );
363 $deps = $dbr->selectField( 'module_deps', 'md_deps', array(
364 'md_module' => $this->getName(),
365 'md_skin' => $skin,
366 ), __METHOD__
367 );
368 if ( !is_null( $deps ) ) {
369 $this->fileDeps[$skin] = (array)FormatJson::decode( $deps, true );
370 } else {
371 $this->fileDeps[$skin] = array();
372 }
373 return $this->fileDeps[$skin];
374 }
375
376 /**
377 * Set preloaded file dependency information. Used so we can load this
378 * information for all modules at once.
379 * @param string $skin Skin name
380 * @param array $deps Array of file names
381 */
382 public function setFileDependencies( $skin, $deps ) {
383 $this->fileDeps[$skin] = $deps;
384 }
385
386 /**
387 * Get the last modification timestamp of the message blob for this
388 * module in a given language.
389 * @param string $lang Language code
390 * @return int UNIX timestamp
391 */
392 public function getMsgBlobMtime( $lang ) {
393 if ( !isset( $this->msgBlobMtime[$lang] ) ) {
394 if ( !count( $this->getMessages() ) ) {
395 return 1;
396 }
397
398 $dbr = wfGetDB( DB_SLAVE );
399 $msgBlobMtime = $dbr->selectField( 'msg_resource', 'mr_timestamp', array(
400 'mr_resource' => $this->getName(),
401 'mr_lang' => $lang
402 ), __METHOD__
403 );
404 // If no blob was found, but the module does have messages, that means we need
405 // to regenerate it. Return NOW
406 if ( $msgBlobMtime === false ) {
407 $msgBlobMtime = wfTimestampNow();
408 }
409 $this->msgBlobMtime[$lang] = wfTimestamp( TS_UNIX, $msgBlobMtime );
410 }
411 return $this->msgBlobMtime[$lang];
412 }
413
414 /**
415 * Set a preloaded message blob last modification timestamp. Used so we
416 * can load this information for all modules at once.
417 * @param string $lang Language code
418 * @param int $mtime UNIX timestamp
419 */
420 public function setMsgBlobMtime( $lang, $mtime ) {
421 $this->msgBlobMtime[$lang] = $mtime;
422 }
423
424 /* Abstract Methods */
425
426 /**
427 * Get this module's last modification timestamp for a given
428 * combination of language, skin and debug mode flag. This is typically
429 * the highest of each of the relevant components' modification
430 * timestamps. Whenever anything happens that changes the module's
431 * contents for these parameters, the mtime should increase.
432 *
433 * NOTE: The mtime of the module's messages is NOT automatically included.
434 * If you want this to happen, you'll need to call getMsgBlobMtime()
435 * yourself and take its result into consideration.
436 *
437 * NOTE: The mtime of the module's hash is NOT automatically included.
438 * If your module provides a getModifiedHash() method, you'll need to call getHashMtime()
439 * yourself and take its result into consideration.
440 *
441 * @param ResourceLoaderContext $context Context object
442 * @return int UNIX timestamp
443 */
444 public function getModifiedTime( ResourceLoaderContext $context ) {
445 return 1;
446 }
447
448 /**
449 * Helper method for calculating when the module's hash (if it has one) changed.
450 *
451 * @param ResourceLoaderContext $context
452 * @return int UNIX timestamp
453 */
454 public function getHashMtime( ResourceLoaderContext $context ) {
455 $hash = $this->getModifiedHash( $context );
456 if ( !is_string( $hash ) ) {
457 return 1;
458 }
459
460 // Embed the hash itself in the cache key. This allows for a few nifty things:
461 // - During deployment, servers with old and new versions of the code communicating
462 // with the same memcached will not override the same key repeatedly increasing
463 // the timestamp.
464 // - In case of the definition changing and then changing back in a short period of time
465 // (e.g. in case of a revert or a corrupt server) the old timestamp and client-side cache
466 // url will be re-used.
467 // - If different context-combinations (e.g. same skin, same language or some combination
468 // thereof) result in the same definition, they will use the same hash and timestamp.
469 $cache = wfGetCache( CACHE_ANYTHING );
470 $key = wfMemcKey( 'resourceloader', 'hashmtime', $this->getName(), $hash );
471
472 $data = $cache->get( $key );
473 if ( is_int( $data ) && $data > 0 ) {
474 // We've seen this hash before, re-use the timestamp of when we first saw it.
475 return $data;
476 }
477
478 $timestamp = time();
479 $cache->set( $key, $timestamp );
480 return $timestamp;
481 }
482
483 /**
484 * Get the hash for whatever this module may contain.
485 *
486 * This is the method subclasses should implement if they want to make
487 * use of getHashMTime() inside getModifiedTime().
488 *
489 * @param ResourceLoaderContext $context
490 * @return string|null Hash
491 */
492 public function getModifiedHash( ResourceLoaderContext $context ) {
493 return null;
494 }
495
496 /**
497 * Helper method for calculating when this module's definition summary was last changed.
498 *
499 * @since 1.23
500 *
501 * @param ResourceLoaderContext $context
502 * @return int UNIX timestamp
503 */
504 public function getDefinitionMtime( ResourceLoaderContext $context ) {
505 $summary = $this->getDefinitionSummary( $context );
506 if ( $summary === null ) {
507 return 1;
508 }
509
510 $hash = md5( json_encode( $summary ) );
511 $cache = wfGetCache( CACHE_ANYTHING );
512 $key = wfMemcKey( 'resourceloader', 'moduledefinition', $this->getName(), $hash );
513
514 $data = $cache->get( $key );
515 if ( is_int( $data ) && $data > 0 ) {
516 // We've seen this hash before, re-use the timestamp of when we first saw it.
517 return $data;
518 }
519
520 wfDebugLog( 'resourceloader', __METHOD__ . ": New definition for module "
521 . "{$this->getName()} in context \"{$context->getHash()}\"" );
522 // WMF logging for T94810
523 global $wgRequest;
524 if ( isset( $wgRequest ) && $context->getUser() ) {
525 wfDebugLog( 'resourceloader', __METHOD__ . ": Request with user parameter in "
526 . "context \"{$context->getHash()}\" from " . $wgRequest->getRequestURL() );
527 }
528
529 $timestamp = time();
530 $cache->set( $key, $timestamp );
531 return $timestamp;
532 }
533
534 /**
535 * Get the definition summary for this module.
536 *
537 * This is the method subclasses should implement if they want to make
538 * use of getDefinitionMTime() inside getModifiedTime().
539 *
540 * Return an array containing values from all significant properties of this
541 * module's definition. Be sure to include things that are explicitly ordered,
542 * in their actaul order (bug 37812).
543 *
544 * Avoid including things that are insiginificant (e.g. order of message
545 * keys is insignificant and should be sorted to avoid unnecessary cache
546 * invalidation).
547 *
548 * Avoid including things already considered by other methods inside your
549 * getModifiedTime(), such as file mtime timestamps.
550 *
551 * Serialisation is done using json_encode, which means object state is not
552 * taken into account when building the hash. This data structure must only
553 * contain arrays and scalars as values (avoid object instances) which means
554 * it requires abstraction.
555 *
556 * @since 1.23
557 *
558 * @param ResourceLoaderContext $context
559 * @return array|null
560 */
561 public function getDefinitionSummary( ResourceLoaderContext $context ) {
562 return array(
563 'class' => get_class( $this ),
564 );
565 }
566
567 /**
568 * Check whether this module is known to be empty. If a child class
569 * has an easy and cheap way to determine that this module is
570 * definitely going to be empty, it should override this method to
571 * return true in that case. Callers may optimize the request for this
572 * module away if this function returns true.
573 * @param ResourceLoaderContext $context
574 * @return bool
575 */
576 public function isKnownEmpty( ResourceLoaderContext $context ) {
577 return false;
578 }
579
580 /** @var JSParser Lazy-initialized; use self::javaScriptParser() */
581 private static $jsParser;
582 private static $parseCacheVersion = 1;
583
584 /**
585 * Validate a given script file; if valid returns the original source.
586 * If invalid, returns replacement JS source that throws an exception.
587 *
588 * @param string $fileName
589 * @param string $contents
590 * @return string JS with the original, or a replacement error
591 */
592 protected function validateScriptFile( $fileName, $contents ) {
593 if ( $this->getConfig()->get( 'ResourceLoaderValidateJS' ) ) {
594 // Try for cache hit
595 // Use CACHE_ANYTHING since filtering is very slow compared to DB queries
596 $key = wfMemcKey( 'resourceloader', 'jsparse', self::$parseCacheVersion, md5( $contents ) );
597 $cache = wfGetCache( CACHE_ANYTHING );
598 $cacheEntry = $cache->get( $key );
599 if ( is_string( $cacheEntry ) ) {
600 return $cacheEntry;
601 }
602
603 $parser = self::javaScriptParser();
604 try {
605 $parser->parse( $contents, $fileName, 1 );
606 $result = $contents;
607 } catch ( Exception $e ) {
608 // We'll save this to cache to avoid having to validate broken JS over and over...
609 $err = $e->getMessage();
610 $result = "throw new Error(" . Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");";
611 }
612
613 $cache->set( $key, $result );
614 return $result;
615 } else {
616 return $contents;
617 }
618 }
619
620 /**
621 * @return JSParser
622 */
623 protected static function javaScriptParser() {
624 if ( !self::$jsParser ) {
625 self::$jsParser = new JSParser();
626 }
627 return self::$jsParser;
628 }
629
630 /**
631 * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist
632 * but returns 1 instead.
633 * @param string $filename File name
634 * @return int UNIX timestamp
635 */
636 protected static function safeFilemtime( $filename ) {
637 wfSuppressWarnings();
638 $mtime = filemtime( $filename ) ?: 1;
639 wfRestoreWarnings();
640
641 return $mtime;
642 }
643 }