ResourceLoaderStartUpModule.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. <?php
  2. /**
  3. * This program is free software; you can redistribute it and/or modify
  4. * it under the terms of the GNU General Public License as published by
  5. * the Free Software Foundation; either version 2 of the License, or
  6. * (at your option) any later version.
  7. *
  8. * This program is distributed in the hope that it will be useful,
  9. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. * GNU General Public License for more details.
  12. *
  13. * You should have received a copy of the GNU General Public License along
  14. * with this program; if not, write to the Free Software Foundation, Inc.,
  15. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  16. * http://www.gnu.org/copyleft/gpl.html
  17. *
  18. * @file
  19. * @author Trevor Parscal
  20. * @author Roan Kattouw
  21. */
  22. use MediaWiki\MediaWikiServices;
  23. /**
  24. * Module for ResourceLoader initialization.
  25. *
  26. * See also <https://www.mediawiki.org/wiki/ResourceLoader/Features#Startup_Module>
  27. *
  28. * The startup module, as being called only from ResourceLoaderClientHtml, has
  29. * the ability to vary based extra query parameters, in addition to those
  30. * from ResourceLoaderContext:
  31. *
  32. * - target: Only register modules in the client intended for this target.
  33. * Default: "desktop".
  34. * See also: OutputPage::setTarget(), ResourceLoaderModule::getTargets().
  35. *
  36. * - safemode: Only register modules that have ORIGIN_CORE as their origin.
  37. * This effectively disables ORIGIN_USER modules. (T185303)
  38. * See also: OutputPage::disallowUserJs()
  39. */
  40. class ResourceLoaderStartUpModule extends ResourceLoaderModule {
  41. protected $targets = [ 'desktop', 'mobile' ];
  42. /**
  43. * @param ResourceLoaderContext $context
  44. * @return array
  45. */
  46. private function getConfigSettings( $context ) {
  47. $conf = $this->getConfig();
  48. // We can't use Title::newMainPage() if 'mainpage' is in
  49. // $wgForceUIMsgAsContentMsg because that will try to use the session
  50. // user's language and we have no session user. This does the
  51. // equivalent but falling back to our ResourceLoaderContext language
  52. // instead.
  53. $mainPage = Title::newFromText( $context->msg( 'mainpage' )->inContentLanguage()->text() );
  54. if ( !$mainPage ) {
  55. $mainPage = Title::newFromText( 'Main Page' );
  56. }
  57. /**
  58. * Namespace related preparation
  59. * - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces.
  60. * - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive.
  61. */
  62. $contLang = MediaWikiServices::getInstance()->getContentLanguage();
  63. $namespaceIds = $contLang->getNamespaceIds();
  64. $caseSensitiveNamespaces = [];
  65. foreach ( MWNamespace::getCanonicalNamespaces() as $index => $name ) {
  66. $namespaceIds[$contLang->lc( $name )] = $index;
  67. if ( !MWNamespace::isCapitalized( $index ) ) {
  68. $caseSensitiveNamespaces[] = $index;
  69. }
  70. }
  71. $illegalFileChars = $conf->get( 'IllegalFileChars' );
  72. $oldCommentSchema = $conf->get( 'CommentTableSchemaMigrationStage' ) === MIGRATION_OLD;
  73. // Build list of variables
  74. $vars = [
  75. 'wgLoadScript' => wfScript( 'load' ),
  76. 'debug' => $context->getDebug(),
  77. 'skin' => $context->getSkin(),
  78. 'stylepath' => $conf->get( 'StylePath' ),
  79. 'wgUrlProtocols' => wfUrlProtocols(),
  80. 'wgArticlePath' => $conf->get( 'ArticlePath' ),
  81. 'wgScriptPath' => $conf->get( 'ScriptPath' ),
  82. 'wgScript' => wfScript(),
  83. 'wgSearchType' => $conf->get( 'SearchType' ),
  84. 'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ),
  85. // Force object to avoid "empty" associative array from
  86. // becoming [] instead of {} in JS (T36604)
  87. 'wgActionPaths' => (object)$conf->get( 'ActionPaths' ),
  88. 'wgServer' => $conf->get( 'Server' ),
  89. 'wgServerName' => $conf->get( 'ServerName' ),
  90. 'wgUserLanguage' => $context->getLanguage(),
  91. 'wgContentLanguage' => $contLang->getCode(),
  92. 'wgTranslateNumerals' => $conf->get( 'TranslateNumerals' ),
  93. 'wgVersion' => $conf->get( 'Version' ),
  94. 'wgEnableAPI' => true, // Deprecated since MW 1.32
  95. 'wgEnableWriteAPI' => true, // Deprecated since MW 1.32
  96. 'wgMainPageTitle' => $mainPage->getPrefixedText(),
  97. 'wgFormattedNamespaces' => $contLang->getFormattedNamespaces(),
  98. 'wgNamespaceIds' => $namespaceIds,
  99. 'wgContentNamespaces' => MWNamespace::getContentNamespaces(),
  100. 'wgSiteName' => $conf->get( 'Sitename' ),
  101. 'wgDBname' => $conf->get( 'DBname' ),
  102. 'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
  103. 'wgAvailableSkins' => Skin::getSkinNames(),
  104. 'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
  105. // MediaWiki sets cookies to have this prefix by default
  106. 'wgCookiePrefix' => $conf->get( 'CookiePrefix' ),
  107. 'wgCookieDomain' => $conf->get( 'CookieDomain' ),
  108. 'wgCookiePath' => $conf->get( 'CookiePath' ),
  109. 'wgCookieExpiration' => $conf->get( 'CookieExpiration' ),
  110. 'wgResourceLoaderMaxQueryLength' => $conf->get( 'ResourceLoaderMaxQueryLength' ),
  111. 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
  112. 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
  113. 'wgIllegalFileChars' => Title::convertByteClassToUnicodeClass( $illegalFileChars ),
  114. 'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ),
  115. 'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ),
  116. 'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
  117. 'wgEnableUploads' => $conf->get( 'EnableUploads' ),
  118. 'wgCommentByteLimit' => $oldCommentSchema ? 255 : null,
  119. 'wgCommentCodePointLimit' => $oldCommentSchema ? null : CommentStore::COMMENT_CHARACTER_LIMIT,
  120. ];
  121. Hooks::run( 'ResourceLoaderGetConfigVars', [ &$vars ] );
  122. return $vars;
  123. }
  124. /**
  125. * Recursively get all explicit and implicit dependencies for to the given module.
  126. *
  127. * @param array $registryData
  128. * @param string $moduleName
  129. * @return array
  130. */
  131. protected static function getImplicitDependencies( array $registryData, $moduleName ) {
  132. static $dependencyCache = [];
  133. // The list of implicit dependencies won't be altered, so we can
  134. // cache them without having to worry.
  135. if ( !isset( $dependencyCache[$moduleName] ) ) {
  136. if ( !isset( $registryData[$moduleName] ) ) {
  137. // Dependencies may not exist
  138. $dependencyCache[$moduleName] = [];
  139. } else {
  140. $data = $registryData[$moduleName];
  141. $dependencyCache[$moduleName] = $data['dependencies'];
  142. foreach ( $data['dependencies'] as $dependency ) {
  143. // Recursively get the dependencies of the dependencies
  144. $dependencyCache[$moduleName] = array_merge(
  145. $dependencyCache[$moduleName],
  146. self::getImplicitDependencies( $registryData, $dependency )
  147. );
  148. }
  149. }
  150. }
  151. return $dependencyCache[$moduleName];
  152. }
  153. /**
  154. * Optimize the dependency tree in $this->modules.
  155. *
  156. * The optimization basically works like this:
  157. * Given we have module A with the dependencies B and C
  158. * and module B with the dependency C.
  159. * Now we don't have to tell the client to explicitly fetch module
  160. * C as that's already included in module B.
  161. *
  162. * This way we can reasonably reduce the amount of module registration
  163. * data send to the client.
  164. *
  165. * @param array &$registryData Modules keyed by name with properties:
  166. * - string 'version'
  167. * - array 'dependencies'
  168. * - string|null 'group'
  169. * - string 'source'
  170. */
  171. public static function compileUnresolvedDependencies( array &$registryData ) {
  172. foreach ( $registryData as $name => &$data ) {
  173. $dependencies = $data['dependencies'];
  174. foreach ( $data['dependencies'] as $dependency ) {
  175. $implicitDependencies = self::getImplicitDependencies( $registryData, $dependency );
  176. $dependencies = array_diff( $dependencies, $implicitDependencies );
  177. }
  178. // Rebuild keys
  179. $data['dependencies'] = array_values( $dependencies );
  180. }
  181. }
  182. /**
  183. * Get registration code for all modules.
  184. *
  185. * @param ResourceLoaderContext $context
  186. * @return string JavaScript code for registering all modules with the client loader
  187. */
  188. public function getModuleRegistrations( ResourceLoaderContext $context ) {
  189. $resourceLoader = $context->getResourceLoader();
  190. // Future developers: Use WebRequest::getRawVal() instead getVal().
  191. // The getVal() method performs slow Language+UTF logic. (f303bb9360)
  192. $target = $context->getRequest()->getRawVal( 'target', 'desktop' );
  193. $safemode = $context->getRequest()->getRawVal( 'safemode' ) === '1';
  194. // Bypass target filter if this request is Special:JavaScriptTest.
  195. // To prevent misuse in production, this is only allowed if testing is enabled server-side.
  196. $byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
  197. $out = '';
  198. $states = [];
  199. $registryData = [];
  200. $moduleNames = $resourceLoader->getModuleNames();
  201. // Preload with a batch so that the below calls to getVersionHash() for each module
  202. // don't require on-demand loading of more information.
  203. try {
  204. $resourceLoader->preloadModuleInfo( $moduleNames, $context );
  205. } catch ( Exception $e ) {
  206. // Don't fail the request (T152266)
  207. // Also print the error in the main output
  208. $resourceLoader->outputErrorAndLog( $e,
  209. 'Preloading module info from startup failed: {exception}',
  210. [ 'exception' => $e ]
  211. );
  212. }
  213. // Get registry data
  214. foreach ( $moduleNames as $name ) {
  215. $module = $resourceLoader->getModule( $name );
  216. $moduleTargets = $module->getTargets();
  217. if (
  218. ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) )
  219. || ( $safemode && $module->getOrigin() > ResourceLoaderModule::ORIGIN_CORE_INDIVIDUAL )
  220. ) {
  221. continue;
  222. }
  223. if ( $module->isRaw() ) {
  224. // Don't register "raw" modules (like 'startup') client-side because depending on them
  225. // is illegal anyway and would only lead to them being loaded a second time,
  226. // causing any state to be lost.
  227. // ATTENTION: Because of the line below, this is not going to cause infinite recursion.
  228. // Think carefully before making changes to this code!
  229. // The below code is going to call ResourceLoaderModule::getVersionHash() for every module.
  230. // For StartUpModule (this module) the hash is computed based on the manifest content,
  231. // which is the very thing we are computing right here. As such, this must skip iterating
  232. // over 'startup' itself.
  233. continue;
  234. }
  235. try {
  236. $versionHash = $module->getVersionHash( $context );
  237. } catch ( Exception $e ) {
  238. // Don't fail the request (T152266)
  239. // Also print the error in the main output
  240. $resourceLoader->outputErrorAndLog( $e,
  241. 'Calculating version for "{module}" failed: {exception}',
  242. [
  243. 'module' => $name,
  244. 'exception' => $e,
  245. ]
  246. );
  247. $versionHash = '';
  248. $states[$name] = 'error';
  249. }
  250. if ( $versionHash !== '' && strlen( $versionHash ) !== 7 ) {
  251. $context->getLogger()->warning(
  252. "Module '{module}' produced an invalid version hash: '{version}'.",
  253. [
  254. 'module' => $name,
  255. 'version' => $versionHash,
  256. ]
  257. );
  258. // Module implementation either broken or deviated from ResourceLoader::makeHash
  259. // Asserted by tests/phpunit/structure/ResourcesTest.
  260. $versionHash = ResourceLoader::makeHash( $versionHash );
  261. }
  262. $skipFunction = $module->getSkipFunction();
  263. if ( $skipFunction !== null && !ResourceLoader::inDebugMode() ) {
  264. $skipFunction = ResourceLoader::filter( 'minify-js', $skipFunction );
  265. }
  266. $registryData[$name] = [
  267. 'version' => $versionHash,
  268. 'dependencies' => $module->getDependencies( $context ),
  269. 'group' => $module->getGroup(),
  270. 'source' => $module->getSource(),
  271. 'skip' => $skipFunction,
  272. ];
  273. }
  274. self::compileUnresolvedDependencies( $registryData );
  275. // Register sources
  276. $out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() );
  277. // Figure out the different call signatures for mw.loader.register
  278. $registrations = [];
  279. foreach ( $registryData as $name => $data ) {
  280. // Call mw.loader.register(name, version, dependencies, group, source, skip)
  281. $registrations[] = [
  282. $name,
  283. $data['version'],
  284. $data['dependencies'],
  285. $data['group'],
  286. // Swap default (local) for null
  287. $data['source'] === 'local' ? null : $data['source'],
  288. $data['skip']
  289. ];
  290. }
  291. // Register modules
  292. $out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $registrations );
  293. if ( $states ) {
  294. $out .= "\n" . ResourceLoader::makeLoaderStateScript( $states );
  295. }
  296. return $out;
  297. }
  298. /**
  299. * @return bool
  300. */
  301. public function isRaw() {
  302. return true;
  303. }
  304. /**
  305. * Internal modules used by ResourceLoader that cannot be depended on.
  306. *
  307. * These module(s) should have isRaw() return true, and are not
  308. * legal dependencies (enforced by structure/ResourcesTest).
  309. *
  310. * @deprecated since 1.32 No longer used.
  311. * @return array
  312. */
  313. public static function getStartupModules() {
  314. wfDeprecated( __METHOD__, '1.32' );
  315. return [];
  316. }
  317. /**
  318. * @deprecated since 1.32 No longer used.
  319. * @return array
  320. */
  321. public static function getLegacyModules() {
  322. wfDeprecated( __METHOD__, '1.32' );
  323. return [];
  324. }
  325. /**
  326. * @private For internal use by SpecialJavaScriptTest
  327. * @since 1.32
  328. * @return array
  329. */
  330. public function getBaseModulesInternal() {
  331. return $this->getBaseModules();
  332. }
  333. /**
  334. * Base modules implicitly available to all modules.
  335. *
  336. * @return array
  337. */
  338. private function getBaseModules() {
  339. global $wgIncludeLegacyJavaScript;
  340. $baseModules = [ 'jquery', 'mediawiki.base' ];
  341. if ( $wgIncludeLegacyJavaScript ) {
  342. $baseModules[] = 'mediawiki.legacy.wikibits';
  343. }
  344. return $baseModules;
  345. }
  346. /**
  347. * @param ResourceLoaderContext $context
  348. * @return string JavaScript code
  349. */
  350. public function getScript( ResourceLoaderContext $context ) {
  351. global $IP;
  352. if ( $context->getOnly() !== 'scripts' ) {
  353. return '/* Requires only=script */';
  354. }
  355. $startupCode = file_get_contents( "$IP/resources/src/startup/startup.js" );
  356. // The files read here MUST be kept in sync with maintenance/jsduck/eg-iframe.html,
  357. // and MUST be considered by 'fileHashes' in StartUpModule::getDefinitionSummary().
  358. $mwLoaderCode = file_get_contents( "$IP/resources/src/startup/mediawiki.js" ) .
  359. file_get_contents( "$IP/resources/src/startup/mediawiki.requestIdleCallback.js" );
  360. if ( $context->getDebug() ) {
  361. $mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/mediawiki.log.js" );
  362. }
  363. if ( $this->getConfig()->get( 'ResourceLoaderEnableJSProfiler' ) ) {
  364. $mwLoaderCode .= file_get_contents( "$IP/resources/src/startup/profiler.js" );
  365. }
  366. // Perform replacements for mediawiki.js
  367. $mwLoaderPairs = [
  368. '$VARS.baseModules' => ResourceLoader::encodeJsonForScript( $this->getBaseModules() ),
  369. ];
  370. $profilerStubs = [
  371. '$CODE.profileExecuteStart();' => 'mw.loader.profiler.onExecuteStart( module );',
  372. '$CODE.profileExecuteEnd();' => 'mw.loader.profiler.onExecuteEnd( module );',
  373. '$CODE.profileScriptStart();' => 'mw.loader.profiler.onScriptStart( module );',
  374. '$CODE.profileScriptEnd();' => 'mw.loader.profiler.onScriptEnd( module );',
  375. ];
  376. if ( $this->getConfig()->get( 'ResourceLoaderEnableJSProfiler' ) ) {
  377. // When profiling is enabled, insert the calls.
  378. $mwLoaderPairs += $profilerStubs;
  379. } else {
  380. // When disabled (by default), insert nothing.
  381. $mwLoaderPairs += array_fill_keys( array_keys( $profilerStubs ), '' );
  382. }
  383. $mwLoaderCode = strtr( $mwLoaderCode, $mwLoaderPairs );
  384. // Perform string replacements for startup.js
  385. $pairs = [
  386. '$VARS.wgLegacyJavaScriptGlobals' => ResourceLoader::encodeJsonForScript(
  387. $this->getConfig()->get( 'LegacyJavaScriptGlobals' )
  388. ),
  389. '$VARS.configuration' => ResourceLoader::encodeJsonForScript(
  390. $this->getConfigSettings( $context )
  391. ),
  392. // Raw JavaScript code (not JSON)
  393. '$CODE.registrations();' => trim( $this->getModuleRegistrations( $context ) ),
  394. '$CODE.defineLoader();' => $mwLoaderCode,
  395. ];
  396. $startupCode = strtr( $startupCode, $pairs );
  397. return $startupCode;
  398. }
  399. /**
  400. * @return bool
  401. */
  402. public function supportsURLLoading() {
  403. return false;
  404. }
  405. /**
  406. * @return bool
  407. */
  408. public function enableModuleContentVersion() {
  409. // Enabling this means that ResourceLoader::getVersionHash will simply call getScript()
  410. // and hash it to determine the version (as used by E-Tag HTTP response header).
  411. return true;
  412. }
  413. /**
  414. * @return string
  415. */
  416. public function getGroup() {
  417. return 'startup';
  418. }
  419. }