PageRenderTime 24ms CodeModel.GetById 32ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/resourceloader/ResourceLoaderStartUpModule.php

https://gitlab.com/link233/bootmw
PHP | 415 lines | 228 code | 47 blank | 140 comment | 15 complexity | 949b7a71f69ef2c6a4aa283a2ff1c1f9 MD5 | raw file
  1. <?php
  2. /**
  3. * Module for ResourceLoader initialization.
  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. class ResourceLoaderStartUpModule extends ResourceLoaderModule {
  25. // Cache for getConfigSettings() as it's called by multiple methods
  26. protected $configVars = [];
  27. protected $targets = [ 'desktop', 'mobile' ];
  28. /**
  29. * @param ResourceLoaderContext $context
  30. * @return array
  31. */
  32. protected function getConfigSettings( $context ) {
  33. $hash = $context->getHash();
  34. if ( isset( $this->configVars[$hash] ) ) {
  35. return $this->configVars[$hash];
  36. }
  37. global $wgContLang;
  38. $conf = $this->getConfig();
  39. // We can't use Title::newMainPage() if 'mainpage' is in
  40. // $wgForceUIMsgAsContentMsg because that will try to use the session
  41. // user's language and we have no session user. This does the
  42. // equivalent but falling back to our ResourceLoaderContext language
  43. // instead.
  44. $mainPage = Title::newFromText( $context->msg( 'mainpage' )->inContentLanguage()->text() );
  45. if ( !$mainPage ) {
  46. $mainPage = Title::newFromText( 'Main Page' );
  47. }
  48. /**
  49. * Namespace related preparation
  50. * - wgNamespaceIds: Key-value pairs of all localized, canonical and aliases for namespaces.
  51. * - wgCaseSensitiveNamespaces: Array of namespaces that are case-sensitive.
  52. */
  53. $namespaceIds = $wgContLang->getNamespaceIds();
  54. $caseSensitiveNamespaces = [];
  55. foreach ( MWNamespace::getCanonicalNamespaces() as $index => $name ) {
  56. $namespaceIds[$wgContLang->lc( $name )] = $index;
  57. if ( !MWNamespace::isCapitalized( $index ) ) {
  58. $caseSensitiveNamespaces[] = $index;
  59. }
  60. }
  61. // Build list of variables
  62. $vars = [
  63. 'wgLoadScript' => wfScript( 'load' ),
  64. 'debug' => $context->getDebug(),
  65. 'skin' => $context->getSkin(),
  66. 'stylepath' => $conf->get( 'StylePath' ),
  67. 'wgUrlProtocols' => wfUrlProtocols(),
  68. 'wgArticlePath' => $conf->get( 'ArticlePath' ),
  69. 'wgScriptPath' => $conf->get( 'ScriptPath' ),
  70. 'wgScriptExtension' => '.php',
  71. 'wgScript' => wfScript(),
  72. 'wgSearchType' => $conf->get( 'SearchType' ),
  73. 'wgVariantArticlePath' => $conf->get( 'VariantArticlePath' ),
  74. // Force object to avoid "empty" associative array from
  75. // becoming [] instead of {} in JS (bug 34604)
  76. 'wgActionPaths' => (object)$conf->get( 'ActionPaths' ),
  77. 'wgServer' => $conf->get( 'Server' ),
  78. 'wgServerName' => $conf->get( 'ServerName' ),
  79. 'wgUserLanguage' => $context->getLanguage(),
  80. 'wgContentLanguage' => $wgContLang->getCode(),
  81. 'wgTranslateNumerals' => $conf->get( 'TranslateNumerals' ),
  82. 'wgVersion' => $conf->get( 'Version' ),
  83. 'wgEnableAPI' => $conf->get( 'EnableAPI' ),
  84. 'wgEnableWriteAPI' => $conf->get( 'EnableWriteAPI' ),
  85. 'wgMainPageTitle' => $mainPage->getPrefixedText(),
  86. 'wgFormattedNamespaces' => $wgContLang->getFormattedNamespaces(),
  87. 'wgNamespaceIds' => $namespaceIds,
  88. 'wgContentNamespaces' => MWNamespace::getContentNamespaces(),
  89. 'wgSiteName' => $conf->get( 'Sitename' ),
  90. 'wgDBname' => $conf->get( 'DBname' ),
  91. 'wgExtraSignatureNamespaces' => $conf->get( 'ExtraSignatureNamespaces' ),
  92. 'wgAvailableSkins' => Skin::getSkinNames(),
  93. 'wgExtensionAssetsPath' => $conf->get( 'ExtensionAssetsPath' ),
  94. // MediaWiki sets cookies to have this prefix by default
  95. 'wgCookiePrefix' => $conf->get( 'CookiePrefix' ),
  96. 'wgCookieDomain' => $conf->get( 'CookieDomain' ),
  97. 'wgCookiePath' => $conf->get( 'CookiePath' ),
  98. 'wgCookieExpiration' => $conf->get( 'CookieExpiration' ),
  99. 'wgResourceLoaderMaxQueryLength' => $conf->get( 'ResourceLoaderMaxQueryLength' ),
  100. 'wgCaseSensitiveNamespaces' => $caseSensitiveNamespaces,
  101. 'wgLegalTitleChars' => Title::convertByteClassToUnicodeClass( Title::legalChars() ),
  102. 'wgResourceLoaderStorageVersion' => $conf->get( 'ResourceLoaderStorageVersion' ),
  103. 'wgResourceLoaderStorageEnabled' => $conf->get( 'ResourceLoaderStorageEnabled' ),
  104. 'wgResourceLoaderLegacyModules' => self::getLegacyModules(),
  105. 'wgForeignUploadTargets' => $conf->get( 'ForeignUploadTargets' ),
  106. 'wgEnableUploads' => $conf->get( 'EnableUploads' ),
  107. ];
  108. Hooks::run( 'ResourceLoaderGetConfigVars', [ &$vars ] );
  109. $this->configVars[$hash] = $vars;
  110. return $this->configVars[$hash];
  111. }
  112. /**
  113. * Recursively get all explicit and implicit dependencies for to the given module.
  114. *
  115. * @param array $registryData
  116. * @param string $moduleName
  117. * @return array
  118. */
  119. protected static function getImplicitDependencies( array $registryData, $moduleName ) {
  120. static $dependencyCache = [];
  121. // The list of implicit dependencies won't be altered, so we can
  122. // cache them without having to worry.
  123. if ( !isset( $dependencyCache[$moduleName] ) ) {
  124. if ( !isset( $registryData[$moduleName] ) ) {
  125. // Dependencies may not exist
  126. $dependencyCache[$moduleName] = [];
  127. } else {
  128. $data = $registryData[$moduleName];
  129. $dependencyCache[$moduleName] = $data['dependencies'];
  130. foreach ( $data['dependencies'] as $dependency ) {
  131. // Recursively get the dependencies of the dependencies
  132. $dependencyCache[$moduleName] = array_merge(
  133. $dependencyCache[$moduleName],
  134. self::getImplicitDependencies( $registryData, $dependency )
  135. );
  136. }
  137. }
  138. }
  139. return $dependencyCache[$moduleName];
  140. }
  141. /**
  142. * Optimize the dependency tree in $this->modules.
  143. *
  144. * The optimization basically works like this:
  145. * Given we have module A with the dependencies B and C
  146. * and module B with the dependency C.
  147. * Now we don't have to tell the client to explicitly fetch module
  148. * C as that's already included in module B.
  149. *
  150. * This way we can reasonably reduce the amount of module registration
  151. * data send to the client.
  152. *
  153. * @param array &$registryData Modules keyed by name with properties:
  154. * - string 'version'
  155. * - array 'dependencies'
  156. * - string|null 'group'
  157. * - string 'source'
  158. */
  159. public static function compileUnresolvedDependencies( array &$registryData ) {
  160. foreach ( $registryData as $name => &$data ) {
  161. $dependencies = $data['dependencies'];
  162. foreach ( $data['dependencies'] as $dependency ) {
  163. $implicitDependencies = self::getImplicitDependencies( $registryData, $dependency );
  164. $dependencies = array_diff( $dependencies, $implicitDependencies );
  165. }
  166. // Rebuild keys
  167. $data['dependencies'] = array_values( $dependencies );
  168. }
  169. }
  170. /**
  171. * Get registration code for all modules.
  172. *
  173. * @param ResourceLoaderContext $context
  174. * @return string JavaScript code for registering all modules with the client loader
  175. */
  176. public function getModuleRegistrations( ResourceLoaderContext $context ) {
  177. $resourceLoader = $context->getResourceLoader();
  178. $target = $context->getRequest()->getVal( 'target', 'desktop' );
  179. // Bypass target filter if this request is Special:JavaScriptTest.
  180. // To prevent misuse in production, this is only allowed if testing is enabled server-side.
  181. $byPassTargetFilter = $this->getConfig()->get( 'EnableJavaScriptTest' ) && $target === 'test';
  182. $out = '';
  183. $registryData = [];
  184. // Get registry data
  185. foreach ( $resourceLoader->getModuleNames() as $name ) {
  186. $module = $resourceLoader->getModule( $name );
  187. $moduleTargets = $module->getTargets();
  188. if ( !$byPassTargetFilter && !in_array( $target, $moduleTargets ) ) {
  189. continue;
  190. }
  191. if ( $module->isRaw() ) {
  192. // Don't register "raw" modules (like 'jquery' and 'mediawiki') client-side because
  193. // depending on them is illegal anyway and would only lead to them being reloaded
  194. // causing any state to be lost (like jQuery plugins, mw.config etc.)
  195. continue;
  196. }
  197. $versionHash = $module->getVersionHash( $context );
  198. if ( strlen( $versionHash ) !== 8 ) {
  199. $context->getLogger()->warning(
  200. "Module '{module}' produced an invalid version hash: '{version}'.",
  201. [
  202. 'module' => $name,
  203. 'version' => $versionHash,
  204. ]
  205. );
  206. // Module implementation either broken or deviated from ResourceLoader::makeHash
  207. // Asserted by tests/phpunit/structure/ResourcesTest.
  208. $versionHash = ResourceLoader::makeHash( $versionHash );
  209. }
  210. $skipFunction = $module->getSkipFunction();
  211. if ( $skipFunction !== null && !ResourceLoader::inDebugMode() ) {
  212. $skipFunction = ResourceLoader::filter( 'minify-js', $skipFunction );
  213. }
  214. $registryData[$name] = [
  215. 'version' => $versionHash,
  216. 'dependencies' => $module->getDependencies( $context ),
  217. 'group' => $module->getGroup(),
  218. 'source' => $module->getSource(),
  219. 'skip' => $skipFunction,
  220. ];
  221. }
  222. self::compileUnresolvedDependencies( $registryData );
  223. // Register sources
  224. $out .= ResourceLoader::makeLoaderSourcesScript( $resourceLoader->getSources() );
  225. // Figure out the different call signatures for mw.loader.register
  226. $registrations = [];
  227. foreach ( $registryData as $name => $data ) {
  228. // Call mw.loader.register(name, version, dependencies, group, source, skip)
  229. $registrations[] = [
  230. $name,
  231. $data['version'],
  232. $data['dependencies'],
  233. $data['group'],
  234. // Swap default (local) for null
  235. $data['source'] === 'local' ? null : $data['source'],
  236. $data['skip']
  237. ];
  238. }
  239. // Register modules
  240. $out .= "\n" . ResourceLoader::makeLoaderRegisterScript( $registrations );
  241. return $out;
  242. }
  243. /**
  244. * @return bool
  245. */
  246. public function isRaw() {
  247. return true;
  248. }
  249. /**
  250. * Base modules required for the base environment of ResourceLoader
  251. *
  252. * @return array
  253. */
  254. public static function getStartupModules() {
  255. return [ 'jquery', 'mediawiki' ];
  256. }
  257. public static function getLegacyModules() {
  258. global $wgIncludeLegacyJavaScript;
  259. $legacyModules = [];
  260. if ( $wgIncludeLegacyJavaScript ) {
  261. $legacyModules[] = 'mediawiki.legacy.wikibits';
  262. }
  263. return $legacyModules;
  264. }
  265. /**
  266. * Get the load URL of the startup modules.
  267. *
  268. * This is a helper for getScript(), but can also be called standalone, such
  269. * as when generating an AppCache manifest.
  270. *
  271. * @param ResourceLoaderContext $context
  272. * @return string
  273. */
  274. public static function getStartupModulesUrl( ResourceLoaderContext $context ) {
  275. $rl = $context->getResourceLoader();
  276. $moduleNames = self::getStartupModules();
  277. $query = [
  278. 'modules' => ResourceLoader::makePackedModulesString( $moduleNames ),
  279. 'only' => 'scripts',
  280. 'lang' => $context->getLanguage(),
  281. 'skin' => $context->getSkin(),
  282. 'debug' => $context->getDebug() ? 'true' : 'false',
  283. 'version' => $rl->getCombinedVersion( $context, $moduleNames ),
  284. ];
  285. // Ensure uniform query order
  286. ksort( $query );
  287. return wfAppendQuery( wfScript( 'load' ), $query );
  288. }
  289. /**
  290. * @param ResourceLoaderContext $context
  291. * @return string
  292. */
  293. public function getScript( ResourceLoaderContext $context ) {
  294. global $IP;
  295. if ( $context->getOnly() !== 'scripts' ) {
  296. return '/* Requires only=script */';
  297. }
  298. $out = file_get_contents( "$IP/resources/src/startup.js" );
  299. $pairs = array_map( function ( $value ) {
  300. $value = FormatJson::encode( $value, ResourceLoader::inDebugMode(), FormatJson::ALL_OK );
  301. // Fix indentation
  302. $value = str_replace( "\n", "\n\t", $value );
  303. return $value;
  304. }, [
  305. '$VARS.wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
  306. '$VARS.configuration' => $this->getConfigSettings( $context ),
  307. '$VARS.baseModulesUri' => self::getStartupModulesUrl( $context ),
  308. ] );
  309. $pairs['$CODE.registrations()'] = str_replace(
  310. "\n",
  311. "\n\t",
  312. trim( $this->getModuleRegistrations( $context ) )
  313. );
  314. return strtr( $out, $pairs );
  315. }
  316. /**
  317. * @return bool
  318. */
  319. public function supportsURLLoading() {
  320. return false;
  321. }
  322. /**
  323. * Get the definition summary for this module.
  324. *
  325. * @param ResourceLoaderContext $context
  326. * @return array
  327. */
  328. public function getDefinitionSummary( ResourceLoaderContext $context ) {
  329. global $IP;
  330. $summary = parent::getDefinitionSummary( $context );
  331. $summary[] = [
  332. // Detect changes to variables exposed in mw.config (T30899).
  333. 'vars' => $this->getConfigSettings( $context ),
  334. // Changes how getScript() creates mw.Map for mw.config
  335. 'wgLegacyJavaScriptGlobals' => $this->getConfig()->get( 'LegacyJavaScriptGlobals' ),
  336. // Detect changes to the module registrations
  337. 'moduleHashes' => $this->getAllModuleHashes( $context ),
  338. 'fileMtimes' => [
  339. filemtime( "$IP/resources/src/startup.js" ),
  340. ],
  341. ];
  342. return $summary;
  343. }
  344. /**
  345. * Helper method for getDefinitionSummary().
  346. *
  347. * @param ResourceLoaderContext $context
  348. * @return string SHA-1
  349. */
  350. protected function getAllModuleHashes( ResourceLoaderContext $context ) {
  351. $rl = $context->getResourceLoader();
  352. // Preload for getCombinedVersion()
  353. $rl->preloadModuleInfo( $rl->getModuleNames(), $context );
  354. // ATTENTION: Because of the line below, this is not going to cause infinite recursion.
  355. // Think carefully before making changes to this code!
  356. // Pre-populate versionHash with something because the loop over all modules below includes
  357. // the startup module (this module).
  358. // See ResourceLoaderModule::getVersionHash() for usage of this cache.
  359. $this->versionHash[$context->getHash()] = null;
  360. return $rl->getCombinedVersion( $context, $rl->getModuleNames() );
  361. }
  362. /**
  363. * @return string
  364. */
  365. public function getGroup() {
  366. return 'startup';
  367. }
  368. }