ResourceLoaderModule.php 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995
  1. <?php
  2. /**
  3. * Abstraction for ResourceLoader 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. use MediaWiki\MediaWikiServices;
  25. use Psr\Log\LoggerAwareInterface;
  26. use Psr\Log\LoggerInterface;
  27. use Psr\Log\NullLogger;
  28. use Wikimedia\RelPath;
  29. use Wikimedia\ScopedCallback;
  30. /**
  31. * Abstraction for ResourceLoader modules, with name registration and maxage functionality.
  32. */
  33. abstract class ResourceLoaderModule implements LoggerAwareInterface {
  34. # Type of resource
  35. const TYPE_SCRIPTS = 'scripts';
  36. const TYPE_STYLES = 'styles';
  37. const TYPE_COMBINED = 'combined';
  38. # Desired load type
  39. // Module only has styles (loaded via <style> or <link rel=stylesheet>)
  40. const LOAD_STYLES = 'styles';
  41. // Module may have other resources (loaded via mw.loader from a script)
  42. const LOAD_GENERAL = 'general';
  43. # sitewide core module like a skin file or jQuery component
  44. const ORIGIN_CORE_SITEWIDE = 1;
  45. # per-user module generated by the software
  46. const ORIGIN_CORE_INDIVIDUAL = 2;
  47. # sitewide module generated from user-editable files, like MediaWiki:Common.js, or
  48. # modules accessible to multiple users, such as those generated by the Gadgets extension.
  49. const ORIGIN_USER_SITEWIDE = 3;
  50. # per-user module generated from user-editable files, like User:Me/vector.js
  51. const ORIGIN_USER_INDIVIDUAL = 4;
  52. # an access constant; make sure this is kept as the largest number in this group
  53. const ORIGIN_ALL = 10;
  54. # script and style modules form a hierarchy of trustworthiness, with core modules like
  55. # skins and jQuery as most trustworthy, and user scripts as least trustworthy. We can
  56. # limit the types of scripts and styles we allow to load on, say, sensitive special
  57. # pages like Special:UserLogin and Special:Preferences
  58. protected $origin = self::ORIGIN_CORE_SITEWIDE;
  59. protected $name = null;
  60. protected $targets = [ 'desktop' ];
  61. // In-object cache for file dependencies
  62. protected $fileDeps = [];
  63. // In-object cache for message blob (keyed by language)
  64. protected $msgBlobs = [];
  65. // In-object cache for version hash
  66. protected $versionHash = [];
  67. // In-object cache for module content
  68. protected $contents = [];
  69. /**
  70. * @var Config
  71. */
  72. protected $config;
  73. /**
  74. * @var array|bool
  75. */
  76. protected $deprecated = false;
  77. /**
  78. * @var LoggerInterface
  79. */
  80. protected $logger;
  81. /**
  82. * Get this module's name. This is set when the module is registered
  83. * with ResourceLoader::register()
  84. *
  85. * @return string|null Name (string) or null if no name was set
  86. */
  87. public function getName() {
  88. return $this->name;
  89. }
  90. /**
  91. * Set this module's name. This is called by ResourceLoader::register()
  92. * when registering the module. Other code should not call this.
  93. *
  94. * @param string $name
  95. */
  96. public function setName( $name ) {
  97. $this->name = $name;
  98. }
  99. /**
  100. * Get this module's origin. This is set when the module is registered
  101. * with ResourceLoader::register()
  102. *
  103. * @return int ResourceLoaderModule class constant, the subclass default
  104. * if not set manually
  105. */
  106. public function getOrigin() {
  107. return $this->origin;
  108. }
  109. /**
  110. * @param ResourceLoaderContext $context
  111. * @return bool
  112. */
  113. public function getFlip( $context ) {
  114. return MediaWikiServices::getInstance()->getContentLanguage()->getDir() !==
  115. $context->getDirection();
  116. }
  117. /**
  118. * Get JS representing deprecation information for the current module if available
  119. *
  120. * @return string JavaScript code
  121. */
  122. public function getDeprecationInformation() {
  123. $deprecationInfo = $this->deprecated;
  124. if ( $deprecationInfo ) {
  125. $name = $this->getName();
  126. $warning = 'This page is using the deprecated ResourceLoader module "' . $name . '".';
  127. if ( is_string( $deprecationInfo ) ) {
  128. $warning .= "\n" . $deprecationInfo;
  129. }
  130. return Xml::encodeJsCall(
  131. 'mw.log.warn',
  132. [ $warning ]
  133. );
  134. } else {
  135. return '';
  136. }
  137. }
  138. /**
  139. * Get all JS for this module for a given language and skin.
  140. * Includes all relevant JS except loader scripts.
  141. *
  142. * @param ResourceLoaderContext $context
  143. * @return string JavaScript code
  144. */
  145. public function getScript( ResourceLoaderContext $context ) {
  146. // Stub, override expected
  147. return '';
  148. }
  149. /**
  150. * Takes named templates by the module and returns an array mapping.
  151. *
  152. * @return array of templates mapping template alias to content
  153. */
  154. public function getTemplates() {
  155. // Stub, override expected.
  156. return [];
  157. }
  158. /**
  159. * @return Config
  160. * @since 1.24
  161. */
  162. public function getConfig() {
  163. if ( $this->config === null ) {
  164. // Ugh, fall back to default
  165. $this->config = MediaWikiServices::getInstance()->getMainConfig();
  166. }
  167. return $this->config;
  168. }
  169. /**
  170. * @param Config $config
  171. * @since 1.24
  172. */
  173. public function setConfig( Config $config ) {
  174. $this->config = $config;
  175. }
  176. /**
  177. * @since 1.27
  178. * @param LoggerInterface $logger
  179. * @return null
  180. */
  181. public function setLogger( LoggerInterface $logger ) {
  182. $this->logger = $logger;
  183. }
  184. /**
  185. * @since 1.27
  186. * @return LoggerInterface
  187. */
  188. protected function getLogger() {
  189. if ( !$this->logger ) {
  190. $this->logger = new NullLogger();
  191. }
  192. return $this->logger;
  193. }
  194. /**
  195. * Get the URL or URLs to load for this module's JS in debug mode.
  196. * The default behavior is to return a load.php?only=scripts URL for
  197. * the module, but file-based modules will want to override this to
  198. * load the files directly.
  199. *
  200. * This function is called only when 1) we're in debug mode, 2) there
  201. * is no only= parameter and 3) supportsURLLoading() returns true.
  202. * #2 is important to prevent an infinite loop, therefore this function
  203. * MUST return either an only= URL or a non-load.php URL.
  204. *
  205. * @param ResourceLoaderContext $context
  206. * @return array Array of URLs
  207. */
  208. public function getScriptURLsForDebug( ResourceLoaderContext $context ) {
  209. $resourceLoader = $context->getResourceLoader();
  210. $derivative = new DerivativeResourceLoaderContext( $context );
  211. $derivative->setModules( [ $this->getName() ] );
  212. $derivative->setOnly( 'scripts' );
  213. $derivative->setDebug( true );
  214. $url = $resourceLoader->createLoaderURL(
  215. $this->getSource(),
  216. $derivative
  217. );
  218. return [ $url ];
  219. }
  220. /**
  221. * Whether this module supports URL loading. If this function returns false,
  222. * getScript() will be used even in cases (debug mode, no only param) where
  223. * getScriptURLsForDebug() would normally be used instead.
  224. * @return bool
  225. */
  226. public function supportsURLLoading() {
  227. return true;
  228. }
  229. /**
  230. * Get all CSS for this module for a given skin.
  231. *
  232. * @param ResourceLoaderContext $context
  233. * @return array List of CSS strings or array of CSS strings keyed by media type.
  234. * like [ 'screen' => '.foo { width: 0 }' ];
  235. * or [ 'screen' => [ '.foo { width: 0 }' ] ];
  236. */
  237. public function getStyles( ResourceLoaderContext $context ) {
  238. // Stub, override expected
  239. return [];
  240. }
  241. /**
  242. * Get the URL or URLs to load for this module's CSS in debug mode.
  243. * The default behavior is to return a load.php?only=styles URL for
  244. * the module, but file-based modules will want to override this to
  245. * load the files directly. See also getScriptURLsForDebug()
  246. *
  247. * @param ResourceLoaderContext $context
  248. * @return array [ mediaType => [ URL1, URL2, ... ], ... ]
  249. */
  250. public function getStyleURLsForDebug( ResourceLoaderContext $context ) {
  251. $resourceLoader = $context->getResourceLoader();
  252. $derivative = new DerivativeResourceLoaderContext( $context );
  253. $derivative->setModules( [ $this->getName() ] );
  254. $derivative->setOnly( 'styles' );
  255. $derivative->setDebug( true );
  256. $url = $resourceLoader->createLoaderURL(
  257. $this->getSource(),
  258. $derivative
  259. );
  260. return [ 'all' => [ $url ] ];
  261. }
  262. /**
  263. * Get the messages needed for this module.
  264. *
  265. * To get a JSON blob with messages, use MessageBlobStore::get()
  266. *
  267. * @return array List of message keys. Keys may occur more than once
  268. */
  269. public function getMessages() {
  270. // Stub, override expected
  271. return [];
  272. }
  273. /**
  274. * Get the group this module is in.
  275. *
  276. * @return string Group name
  277. */
  278. public function getGroup() {
  279. // Stub, override expected
  280. return null;
  281. }
  282. /**
  283. * Get the source of this module. Should only be overridden for foreign modules.
  284. *
  285. * @return string Source name, 'local' for local modules
  286. */
  287. public function getSource() {
  288. // Stub, override expected
  289. return 'local';
  290. }
  291. /**
  292. * Whether this module's JS expects to work without the client-side ResourceLoader module.
  293. * Returning true from this function will prevent mw.loader.state() call from being
  294. * appended to the bottom of the script.
  295. *
  296. * @return bool
  297. */
  298. public function isRaw() {
  299. return false;
  300. }
  301. /**
  302. * Get a list of modules this module depends on.
  303. *
  304. * Dependency information is taken into account when loading a module
  305. * on the client side.
  306. *
  307. * Note: It is expected that $context will be made non-optional in the near
  308. * future.
  309. *
  310. * @param ResourceLoaderContext|null $context
  311. * @return array List of module names as strings
  312. */
  313. public function getDependencies( ResourceLoaderContext $context = null ) {
  314. // Stub, override expected
  315. return [];
  316. }
  317. /**
  318. * Get target(s) for the module, eg ['desktop'] or ['desktop', 'mobile']
  319. *
  320. * @return array Array of strings
  321. */
  322. public function getTargets() {
  323. return $this->targets;
  324. }
  325. /**
  326. * Get the module's load type.
  327. *
  328. * @since 1.28
  329. * @return string ResourceLoaderModule LOAD_* constant
  330. */
  331. public function getType() {
  332. return self::LOAD_GENERAL;
  333. }
  334. /**
  335. * Get the skip function.
  336. *
  337. * Modules that provide fallback functionality can provide a "skip function". This
  338. * function, if provided, will be passed along to the module registry on the client.
  339. * When this module is loaded (either directly or as a dependency of another module),
  340. * then this function is executed first. If the function returns true, the module will
  341. * instantly be considered "ready" without requesting the associated module resources.
  342. *
  343. * The value returned here must be valid javascript for execution in a private function.
  344. * It must not contain the "function () {" and "}" wrapper though.
  345. *
  346. * @return string|null A JavaScript function body returning a boolean value, or null
  347. */
  348. public function getSkipFunction() {
  349. return null;
  350. }
  351. /**
  352. * Get the files this module depends on indirectly for a given skin.
  353. *
  354. * These are only image files referenced by the module's stylesheet.
  355. *
  356. * @param ResourceLoaderContext $context
  357. * @return array List of files
  358. */
  359. protected function getFileDependencies( ResourceLoaderContext $context ) {
  360. $vary = $context->getSkin() . '|' . $context->getLanguage();
  361. // Try in-object cache first
  362. if ( !isset( $this->fileDeps[$vary] ) ) {
  363. $dbr = wfGetDB( DB_REPLICA );
  364. $deps = $dbr->selectField( 'module_deps',
  365. 'md_deps',
  366. [
  367. 'md_module' => $this->getName(),
  368. 'md_skin' => $vary,
  369. ],
  370. __METHOD__
  371. );
  372. if ( !is_null( $deps ) ) {
  373. $this->fileDeps[$vary] = self::expandRelativePaths(
  374. (array)json_decode( $deps, true )
  375. );
  376. } else {
  377. $this->fileDeps[$vary] = [];
  378. }
  379. }
  380. return $this->fileDeps[$vary];
  381. }
  382. /**
  383. * Set in-object cache for file dependencies.
  384. *
  385. * This is used to retrieve data in batches. See ResourceLoader::preloadModuleInfo().
  386. * To save the data, use saveFileDependencies().
  387. *
  388. * @param ResourceLoaderContext $context
  389. * @param string[] $files Array of file names
  390. */
  391. public function setFileDependencies( ResourceLoaderContext $context, $files ) {
  392. $vary = $context->getSkin() . '|' . $context->getLanguage();
  393. $this->fileDeps[$vary] = $files;
  394. }
  395. /**
  396. * Set the files this module depends on indirectly for a given skin.
  397. *
  398. * @since 1.27
  399. * @param ResourceLoaderContext $context
  400. * @param array $localFileRefs List of files
  401. */
  402. protected function saveFileDependencies( ResourceLoaderContext $context, $localFileRefs ) {
  403. try {
  404. // Related bugs and performance considerations:
  405. // 1. Don't needlessly change the database value with the same list in a
  406. // different order or with duplicates.
  407. // 2. Use relative paths to avoid ghost entries when $IP changes. (T111481)
  408. // 3. Don't needlessly replace the database with the same value
  409. // just because $IP changed (e.g. when upgrading a wiki).
  410. // 4. Don't create an endless replace loop on every request for this
  411. // module when '../' is used anywhere. Even though both are expanded
  412. // (one expanded by getFileDependencies from the DB, the other is
  413. // still raw as originally read by RL), the latter has not
  414. // been normalized yet.
  415. // Normalise
  416. $localFileRefs = array_values( array_unique( $localFileRefs ) );
  417. sort( $localFileRefs );
  418. $localPaths = self::getRelativePaths( $localFileRefs );
  419. $storedPaths = self::getRelativePaths( $this->getFileDependencies( $context ) );
  420. // If the list has been modified since last time we cached it, update the cache
  421. if ( $localPaths !== $storedPaths ) {
  422. $vary = $context->getSkin() . '|' . $context->getLanguage();
  423. $cache = ObjectCache::getLocalClusterInstance();
  424. $key = $cache->makeKey( __METHOD__, $this->getName(), $vary );
  425. $scopeLock = $cache->getScopedLock( $key, 0 );
  426. if ( !$scopeLock ) {
  427. return; // T124649; avoid write slams
  428. }
  429. // No needless escaping as this isn't HTML output.
  430. // Only stored in the database and parsed in PHP.
  431. $deps = json_encode( $localPaths, JSON_UNESCAPED_SLASHES );
  432. $dbw = wfGetDB( DB_MASTER );
  433. $dbw->upsert( 'module_deps',
  434. [
  435. 'md_module' => $this->getName(),
  436. 'md_skin' => $vary,
  437. 'md_deps' => $deps,
  438. ],
  439. [ 'md_module', 'md_skin' ],
  440. [
  441. 'md_deps' => $deps,
  442. ]
  443. );
  444. if ( $dbw->trxLevel() ) {
  445. $dbw->onTransactionResolution(
  446. function () use ( &$scopeLock ) {
  447. ScopedCallback::consume( $scopeLock ); // release after commit
  448. },
  449. __METHOD__
  450. );
  451. }
  452. }
  453. } catch ( Exception $e ) {
  454. wfDebugLog( 'resourceloader', __METHOD__ . ": failed to update DB: $e" );
  455. }
  456. }
  457. /**
  458. * Make file paths relative to MediaWiki directory.
  459. *
  460. * This is used to make file paths safe for storing in a database without the paths
  461. * becoming stale or incorrect when MediaWiki is moved or upgraded (T111481).
  462. *
  463. * @since 1.27
  464. * @param array $filePaths
  465. * @return array
  466. */
  467. public static function getRelativePaths( array $filePaths ) {
  468. global $IP;
  469. return array_map( function ( $path ) use ( $IP ) {
  470. return RelPath::getRelativePath( $path, $IP );
  471. }, $filePaths );
  472. }
  473. /**
  474. * Expand directories relative to $IP.
  475. *
  476. * @since 1.27
  477. * @param array $filePaths
  478. * @return array
  479. */
  480. public static function expandRelativePaths( array $filePaths ) {
  481. global $IP;
  482. return array_map( function ( $path ) use ( $IP ) {
  483. return RelPath::joinPath( $IP, $path );
  484. }, $filePaths );
  485. }
  486. /**
  487. * Get the hash of the message blob.
  488. *
  489. * @since 1.27
  490. * @param ResourceLoaderContext $context
  491. * @return string|null JSON blob or null if module has no messages
  492. */
  493. protected function getMessageBlob( ResourceLoaderContext $context ) {
  494. if ( !$this->getMessages() ) {
  495. // Don't bother consulting MessageBlobStore
  496. return null;
  497. }
  498. // Message blobs may only vary language, not by context keys
  499. $lang = $context->getLanguage();
  500. if ( !isset( $this->msgBlobs[$lang] ) ) {
  501. $this->getLogger()->warning( 'Message blob for {module} should have been preloaded', [
  502. 'module' => $this->getName(),
  503. ] );
  504. $store = $context->getResourceLoader()->getMessageBlobStore();
  505. $this->msgBlobs[$lang] = $store->getBlob( $this, $lang );
  506. }
  507. return $this->msgBlobs[$lang];
  508. }
  509. /**
  510. * Set in-object cache for message blobs.
  511. *
  512. * Used to allow fetching of message blobs in batches. See ResourceLoader::preloadModuleInfo().
  513. *
  514. * @since 1.27
  515. * @param string|null $blob JSON blob or null
  516. * @param string $lang Language code
  517. */
  518. public function setMessageBlob( $blob, $lang ) {
  519. $this->msgBlobs[$lang] = $blob;
  520. }
  521. /**
  522. * Get headers to send as part of a module web response.
  523. *
  524. * It is not supported to send headers through this method that are
  525. * required to be unique or otherwise sent once in an HTTP response
  526. * because clients may make batch requests for multiple modules (as
  527. * is the default behaviour for ResourceLoader clients).
  528. *
  529. * For exclusive or aggregated headers, see ResourceLoader::sendResponseHeaders().
  530. *
  531. * @since 1.30
  532. * @param ResourceLoaderContext $context
  533. * @return string[] Array of HTTP response headers
  534. */
  535. final public function getHeaders( ResourceLoaderContext $context ) {
  536. $headers = [];
  537. $formattedLinks = [];
  538. foreach ( $this->getPreloadLinks( $context ) as $url => $attribs ) {
  539. $link = "<{$url}>;rel=preload";
  540. foreach ( $attribs as $key => $val ) {
  541. $link .= ";{$key}={$val}";
  542. }
  543. $formattedLinks[] = $link;
  544. }
  545. if ( $formattedLinks ) {
  546. $headers[] = 'Link: ' . implode( ',', $formattedLinks );
  547. }
  548. return $headers;
  549. }
  550. /**
  551. * Get a list of resources that web browsers may preload.
  552. *
  553. * Behaviour of rel=preload link is specified at <https://www.w3.org/TR/preload/>.
  554. *
  555. * Use case for ResourceLoader originally part of T164299.
  556. *
  557. * @par Example
  558. * @code
  559. * protected function getPreloadLinks() {
  560. * return [
  561. * 'https://example.org/script.js' => [ 'as' => 'script' ],
  562. * 'https://example.org/image.png' => [ 'as' => 'image' ],
  563. * ];
  564. * }
  565. * @endcode
  566. *
  567. * @par Example using HiDPI image variants
  568. * @code
  569. * protected function getPreloadLinks() {
  570. * return [
  571. * 'https://example.org/logo.png' => [
  572. * 'as' => 'image',
  573. * 'media' => 'not all and (min-resolution: 2dppx)',
  574. * ],
  575. * 'https://example.org/logo@2x.png' => [
  576. * 'as' => 'image',
  577. * 'media' => '(min-resolution: 2dppx)',
  578. * ],
  579. * ];
  580. * }
  581. * @endcode
  582. *
  583. * @see ResourceLoaderModule::getHeaders
  584. * @since 1.30
  585. * @param ResourceLoaderContext $context
  586. * @return array Keyed by url, values must be an array containing
  587. * at least an 'as' key. Optionally a 'media' key as well.
  588. */
  589. protected function getPreloadLinks( ResourceLoaderContext $context ) {
  590. return [];
  591. }
  592. /**
  593. * Get module-specific LESS variables, if any.
  594. *
  595. * @since 1.27
  596. * @param ResourceLoaderContext $context
  597. * @return array Module-specific LESS variables.
  598. */
  599. protected function getLessVars( ResourceLoaderContext $context ) {
  600. return [];
  601. }
  602. /**
  603. * Get an array of this module's resources. Ready for serving to the web.
  604. *
  605. * @since 1.26
  606. * @param ResourceLoaderContext $context
  607. * @return array
  608. */
  609. public function getModuleContent( ResourceLoaderContext $context ) {
  610. $contextHash = $context->getHash();
  611. // Cache this expensive operation. This calls builds the scripts, styles, and messages
  612. // content which typically involves filesystem and/or database access.
  613. if ( !array_key_exists( $contextHash, $this->contents ) ) {
  614. $this->contents[$contextHash] = $this->buildContent( $context );
  615. }
  616. return $this->contents[$contextHash];
  617. }
  618. /**
  619. * Bundle all resources attached to this module into an array.
  620. *
  621. * @since 1.26
  622. * @param ResourceLoaderContext $context
  623. * @return array
  624. */
  625. final protected function buildContent( ResourceLoaderContext $context ) {
  626. $rl = $context->getResourceLoader();
  627. $stats = MediaWikiServices::getInstance()->getStatsdDataFactory();
  628. $statStart = microtime( true );
  629. // This MUST build both scripts and styles, regardless of whether $context->getOnly()
  630. // is 'scripts' or 'styles' because the result is used by getVersionHash which
  631. // must be consistent regardles of the 'only' filter on the current request.
  632. // Also, when introducing new module content resources (e.g. templates, headers),
  633. // these should only be included in the array when they are non-empty so that
  634. // existing modules not using them do not get their cache invalidated.
  635. $content = [];
  636. // Scripts
  637. // If we are in debug mode, we'll want to return an array of URLs if possible
  638. // However, we can't do this if the module doesn't support it.
  639. // We also can't do this if there is an only= parameter, because we have to give
  640. // the module a way to return a load.php URL without causing an infinite loop
  641. if ( $context->getDebug() && !$context->getOnly() && $this->supportsURLLoading() ) {
  642. $scripts = $this->getScriptURLsForDebug( $context );
  643. } else {
  644. $scripts = $this->getScript( $context );
  645. // Make the script safe to concatenate by making sure there is at least one
  646. // trailing new line at the end of the content. Previously, this looked for
  647. // a semi-colon instead, but that breaks concatenation if the semicolon
  648. // is inside a comment like "// foo();". Instead, simply use a
  649. // line break as separator which matches JavaScript native logic for implicitly
  650. // ending statements even if a semi-colon is missing.
  651. // Bugs: T29054, T162719.
  652. if ( is_string( $scripts )
  653. && strlen( $scripts )
  654. && substr( $scripts, -1 ) !== "\n"
  655. ) {
  656. $scripts .= "\n";
  657. }
  658. }
  659. $content['scripts'] = $scripts;
  660. // Styles
  661. $styles = [];
  662. // Don't create empty stylesheets like [ '' => '' ] for modules
  663. // that don't *have* any stylesheets (T40024).
  664. $stylePairs = $this->getStyles( $context );
  665. if ( count( $stylePairs ) ) {
  666. // If we are in debug mode without &only= set, we'll want to return an array of URLs
  667. // See comment near shouldIncludeScripts() for more details
  668. if ( $context->getDebug() && !$context->getOnly() && $this->supportsURLLoading() ) {
  669. $styles = [
  670. 'url' => $this->getStyleURLsForDebug( $context )
  671. ];
  672. } else {
  673. // Minify CSS before embedding in mw.loader.implement call
  674. // (unless in debug mode)
  675. if ( !$context->getDebug() ) {
  676. foreach ( $stylePairs as $media => $style ) {
  677. // Can be either a string or an array of strings.
  678. if ( is_array( $style ) ) {
  679. $stylePairs[$media] = [];
  680. foreach ( $style as $cssText ) {
  681. if ( is_string( $cssText ) ) {
  682. $stylePairs[$media][] =
  683. ResourceLoader::filter( 'minify-css', $cssText );
  684. }
  685. }
  686. } elseif ( is_string( $style ) ) {
  687. $stylePairs[$media] = ResourceLoader::filter( 'minify-css', $style );
  688. }
  689. }
  690. }
  691. // Wrap styles into @media groups as needed and flatten into a numerical array
  692. $styles = [
  693. 'css' => $rl->makeCombinedStyles( $stylePairs )
  694. ];
  695. }
  696. }
  697. $content['styles'] = $styles;
  698. // Messages
  699. $blob = $this->getMessageBlob( $context );
  700. if ( $blob ) {
  701. $content['messagesBlob'] = $blob;
  702. }
  703. $templates = $this->getTemplates();
  704. if ( $templates ) {
  705. $content['templates'] = $templates;
  706. }
  707. $headers = $this->getHeaders( $context );
  708. if ( $headers ) {
  709. $content['headers'] = $headers;
  710. }
  711. $statTiming = microtime( true ) - $statStart;
  712. $statName = strtr( $this->getName(), '.', '_' );
  713. $stats->timing( "resourceloader_build.all", 1000 * $statTiming );
  714. $stats->timing( "resourceloader_build.$statName", 1000 * $statTiming );
  715. return $content;
  716. }
  717. /**
  718. * Get a string identifying the current version of this module in a given context.
  719. *
  720. * Whenever anything happens that changes the module's response (e.g. scripts, styles, and
  721. * messages) this value must change. This value is used to store module responses in cache.
  722. * (Both client-side and server-side.)
  723. *
  724. * It is not recommended to override this directly. Use getDefinitionSummary() instead.
  725. * If overridden, one must call the parent getVersionHash(), append data and re-hash.
  726. *
  727. * This method should be quick because it is frequently run by ResourceLoaderStartUpModule to
  728. * propagate changes to the client and effectively invalidate cache.
  729. *
  730. * @since 1.26
  731. * @param ResourceLoaderContext $context
  732. * @return string Hash (should use ResourceLoader::makeHash)
  733. */
  734. public function getVersionHash( ResourceLoaderContext $context ) {
  735. // Cache this somewhat expensive operation. Especially because some classes
  736. // (e.g. startup module) iterate more than once over all modules to get versions.
  737. $contextHash = $context->getHash();
  738. if ( !array_key_exists( $contextHash, $this->versionHash ) ) {
  739. if ( $this->enableModuleContentVersion() ) {
  740. // Detect changes directly by hashing the module contents.
  741. $str = json_encode( $this->getModuleContent( $context ) );
  742. } else {
  743. // Infer changes based on definition and other metrics
  744. $summary = $this->getDefinitionSummary( $context );
  745. if ( !isset( $summary['_cacheEpoch'] ) ) {
  746. throw new LogicException( 'getDefinitionSummary must call parent method' );
  747. }
  748. $str = json_encode( $summary );
  749. }
  750. $this->versionHash[$contextHash] = ResourceLoader::makeHash( $str );
  751. }
  752. return $this->versionHash[$contextHash];
  753. }
  754. /**
  755. * Whether to generate version hash based on module content.
  756. *
  757. * If a module requires database or file system access to build the module
  758. * content, consider disabling this in favour of manually tracking relevant
  759. * aspects in getDefinitionSummary(). See getVersionHash() for how this is used.
  760. *
  761. * @return bool
  762. */
  763. public function enableModuleContentVersion() {
  764. return false;
  765. }
  766. /**
  767. * Get the definition summary for this module.
  768. *
  769. * This is the method subclasses are recommended to use to track values in their
  770. * version hash. Call this in getVersionHash() and pass it to e.g. json_encode.
  771. *
  772. * Subclasses must call the parent getDefinitionSummary() and build on that.
  773. * It is recommended that each subclass appends its own new array. This prevents
  774. * clashes or accidental overwrites of existing keys and gives each subclass
  775. * its own scope for simple array keys.
  776. *
  777. * @code
  778. * $summary = parent::getDefinitionSummary( $context );
  779. * $summary[] = [
  780. * 'foo' => 123,
  781. * 'bar' => 'quux',
  782. * ];
  783. * return $summary;
  784. * @endcode
  785. *
  786. * Return an array containing values from all significant properties of this
  787. * module's definition.
  788. *
  789. * Be careful not to normalise too much. Especially preserve the order of things
  790. * that carry significance in getScript and getStyles (T39812).
  791. *
  792. * Avoid including things that are insiginificant (e.g. order of message keys is
  793. * insignificant and should be sorted to avoid unnecessary cache invalidation).
  794. *
  795. * This data structure must exclusively contain arrays and scalars as values (avoid
  796. * object instances) to allow simple serialisation using json_encode.
  797. *
  798. * If modules have a hash or timestamp from another source, that may be incuded as-is.
  799. *
  800. * A number of utility methods are available to help you gather data. These are not
  801. * called by default and must be included by the subclass' getDefinitionSummary().
  802. *
  803. * - getMessageBlob()
  804. *
  805. * @since 1.23
  806. * @param ResourceLoaderContext $context
  807. * @return array|null
  808. */
  809. public function getDefinitionSummary( ResourceLoaderContext $context ) {
  810. return [
  811. '_class' => static::class,
  812. '_cacheEpoch' => $this->getConfig()->get( 'CacheEpoch' ),
  813. ];
  814. }
  815. /**
  816. * Check whether this module is known to be empty. If a child class
  817. * has an easy and cheap way to determine that this module is
  818. * definitely going to be empty, it should override this method to
  819. * return true in that case. Callers may optimize the request for this
  820. * module away if this function returns true.
  821. * @param ResourceLoaderContext $context
  822. * @return bool
  823. */
  824. public function isKnownEmpty( ResourceLoaderContext $context ) {
  825. return false;
  826. }
  827. /**
  828. * Check whether this module should be embeded rather than linked
  829. *
  830. * Modules returning true here will be embedded rather than loaded by
  831. * ResourceLoaderClientHtml.
  832. *
  833. * @since 1.30
  834. * @param ResourceLoaderContext $context
  835. * @return bool
  836. */
  837. public function shouldEmbedModule( ResourceLoaderContext $context ) {
  838. return $this->getGroup() === 'private';
  839. }
  840. /** @var JSParser Lazy-initialized; use self::javaScriptParser() */
  841. private static $jsParser;
  842. private static $parseCacheVersion = 1;
  843. /**
  844. * Validate a given script file; if valid returns the original source.
  845. * If invalid, returns replacement JS source that throws an exception.
  846. *
  847. * @param string $fileName
  848. * @param string $contents
  849. * @return string JS with the original, or a replacement error
  850. */
  851. protected function validateScriptFile( $fileName, $contents ) {
  852. if ( !$this->getConfig()->get( 'ResourceLoaderValidateJS' ) ) {
  853. return $contents;
  854. }
  855. $cache = ObjectCache::getMainWANInstance();
  856. return $cache->getWithSetCallback(
  857. $cache->makeGlobalKey(
  858. 'resourceloader',
  859. 'jsparse',
  860. self::$parseCacheVersion,
  861. md5( $contents ),
  862. $fileName
  863. ),
  864. $cache::TTL_WEEK,
  865. function () use ( $contents, $fileName ) {
  866. $parser = self::javaScriptParser();
  867. try {
  868. $parser->parse( $contents, $fileName, 1 );
  869. $result = $contents;
  870. } catch ( Exception $e ) {
  871. // We'll save this to cache to avoid having to re-validate broken JS
  872. $err = $e->getMessage();
  873. $result = "mw.log.error(" .
  874. Xml::encodeJsVar( "JavaScript parse error: $err" ) . ");";
  875. }
  876. return $result;
  877. }
  878. );
  879. }
  880. /**
  881. * @return JSParser
  882. */
  883. protected static function javaScriptParser() {
  884. if ( !self::$jsParser ) {
  885. self::$jsParser = new JSParser();
  886. }
  887. return self::$jsParser;
  888. }
  889. /**
  890. * Safe version of filemtime(), which doesn't throw a PHP warning if the file doesn't exist.
  891. * Defaults to 1.
  892. *
  893. * @param string $filePath File path
  894. * @return int UNIX timestamp
  895. */
  896. protected static function safeFilemtime( $filePath ) {
  897. Wikimedia\suppressWarnings();
  898. $mtime = filemtime( $filePath ) ?: 1;
  899. Wikimedia\restoreWarnings();
  900. return $mtime;
  901. }
  902. /**
  903. * Compute a non-cryptographic string hash of a file's contents.
  904. * If the file does not exist or cannot be read, returns an empty string.
  905. *
  906. * @since 1.26 Uses MD4 instead of SHA1.
  907. * @param string $filePath File path
  908. * @return string Hash
  909. */
  910. protected static function safeFileHash( $filePath ) {
  911. return FileContentsHasher::getFileContentsHash( $filePath );
  912. }
  913. }