PageRenderTime 54ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/includes/registration/ExtensionRegistry.php

https://bitbucket.org/andersus/querytalogo
PHP | 416 lines | 261 code | 33 blank | 122 comment | 30 complexity | 96e60e678d02243e34ce670e35de7fce MD5 | raw file
Possible License(s): LGPL-3.0, MPL-2.0-no-copyleft-exception, JSON, MIT, CC0-1.0, BSD-3-Clause, Apache-2.0, BSD-2-Clause, LGPL-2.1, GPL-2.0
  1. <?php
  2. use MediaWiki\MediaWikiServices;
  3. /**
  4. * ExtensionRegistry class
  5. *
  6. * The Registry loads JSON files, and uses a Processor
  7. * to extract information from them. It also registers
  8. * classes with the autoloader.
  9. *
  10. * @since 1.25
  11. */
  12. class ExtensionRegistry {
  13. /**
  14. * "requires" key that applies to MediaWiki core/$wgVersion
  15. */
  16. const MEDIAWIKI_CORE = 'MediaWiki';
  17. /**
  18. * Version of the highest supported manifest version
  19. */
  20. const MANIFEST_VERSION = 2;
  21. /**
  22. * Version of the oldest supported manifest version
  23. */
  24. const OLDEST_MANIFEST_VERSION = 1;
  25. /**
  26. * Bump whenever the registration cache needs resetting
  27. */
  28. const CACHE_VERSION = 6;
  29. /**
  30. * Special key that defines the merge strategy
  31. *
  32. * @since 1.26
  33. */
  34. const MERGE_STRATEGY = '_merge_strategy';
  35. /**
  36. * Array of loaded things, keyed by name, values are credits information
  37. *
  38. * @var array
  39. */
  40. private $loaded = [];
  41. /**
  42. * List of paths that should be loaded
  43. *
  44. * @var array
  45. */
  46. protected $queued = [];
  47. /**
  48. * Whether we are done loading things
  49. *
  50. * @var bool
  51. */
  52. private $finished = false;
  53. /**
  54. * Items in the JSON file that aren't being
  55. * set as globals
  56. *
  57. * @var array
  58. */
  59. protected $attributes = [];
  60. /**
  61. * @var ExtensionRegistry
  62. */
  63. private static $instance;
  64. /**
  65. * @return ExtensionRegistry
  66. */
  67. public static function getInstance() {
  68. if ( self::$instance === null ) {
  69. self::$instance = new self();
  70. }
  71. return self::$instance;
  72. }
  73. /**
  74. * @param string $path Absolute path to the JSON file
  75. */
  76. public function queue( $path ) {
  77. global $wgExtensionInfoMTime;
  78. $mtime = $wgExtensionInfoMTime;
  79. if ( $mtime === false ) {
  80. if ( file_exists( $path ) ) {
  81. $mtime = filemtime( $path );
  82. } else {
  83. throw new Exception( "$path does not exist!" );
  84. }
  85. if ( !$mtime ) {
  86. $err = error_get_last();
  87. throw new Exception( "Couldn't stat $path: {$err['message']}" );
  88. }
  89. }
  90. $this->queued[$path] = $mtime;
  91. }
  92. /**
  93. * @throws MWException If the queue is already marked as finished (no further things should
  94. * be loaded then).
  95. */
  96. public function loadFromQueue() {
  97. global $wgVersion, $wgDevelopmentWarnings;
  98. if ( !$this->queued ) {
  99. return;
  100. }
  101. if ( $this->finished ) {
  102. throw new MWException(
  103. "The following paths tried to load late: "
  104. . implode( ', ', array_keys( $this->queued ) )
  105. );
  106. }
  107. // A few more things to vary the cache on
  108. $versions = [
  109. 'registration' => self::CACHE_VERSION,
  110. 'mediawiki' => $wgVersion
  111. ];
  112. // We use a try/catch because we don't want to fail here
  113. // if $wgObjectCaches is not configured properly for APC setup
  114. try {
  115. $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache();
  116. } catch ( MWException $e ) {
  117. $cache = new EmptyBagOStuff();
  118. }
  119. // See if this queue is in APC
  120. $key = $cache->makeKey(
  121. 'registration',
  122. md5( json_encode( $this->queued + $versions ) )
  123. );
  124. $data = $cache->get( $key );
  125. if ( $data ) {
  126. $this->exportExtractedData( $data );
  127. } else {
  128. $data = $this->readFromQueue( $this->queued );
  129. $this->exportExtractedData( $data );
  130. // Do this late since we don't want to extract it since we already
  131. // did that, but it should be cached
  132. $data['globals']['wgAutoloadClasses'] += $data['autoload'];
  133. unset( $data['autoload'] );
  134. if ( !( $data['warnings'] && $wgDevelopmentWarnings ) ) {
  135. // If there were no warnings that were shown, cache it
  136. $cache->set( $key, $data, 60 * 60 * 24 );
  137. }
  138. }
  139. $this->queued = [];
  140. }
  141. /**
  142. * Get the current load queue. Not intended to be used
  143. * outside of the installer.
  144. *
  145. * @return array
  146. */
  147. public function getQueue() {
  148. return $this->queued;
  149. }
  150. /**
  151. * Clear the current load queue. Not intended to be used
  152. * outside of the installer.
  153. */
  154. public function clearQueue() {
  155. $this->queued = [];
  156. }
  157. /**
  158. * After this is called, no more extensions can be loaded
  159. *
  160. * @since 1.29
  161. */
  162. public function finish() {
  163. $this->finished = true;
  164. }
  165. /**
  166. * Process a queue of extensions and return their extracted data
  167. *
  168. * @param array $queue keys are filenames, values are ignored
  169. * @return array extracted info
  170. * @throws Exception
  171. */
  172. public function readFromQueue( array $queue ) {
  173. global $wgVersion;
  174. $autoloadClasses = [];
  175. $autoloaderPaths = [];
  176. $processor = new ExtensionProcessor();
  177. $versionChecker = new VersionChecker( $wgVersion );
  178. $extDependencies = [];
  179. $incompatible = [];
  180. $warnings = false;
  181. foreach ( $queue as $path => $mtime ) {
  182. $json = file_get_contents( $path );
  183. if ( $json === false ) {
  184. throw new Exception( "Unable to read $path, does it exist?" );
  185. }
  186. $info = json_decode( $json, /* $assoc = */ true );
  187. if ( !is_array( $info ) ) {
  188. throw new Exception( "$path is not a valid JSON file." );
  189. }
  190. if ( !isset( $info['manifest_version'] ) ) {
  191. wfDeprecated(
  192. "{$info['name']}'s extension.json or skin.json does not have manifest_version",
  193. '1.29'
  194. );
  195. $warnings = true;
  196. // For backwards-compatability, assume a version of 1
  197. $info['manifest_version'] = 1;
  198. }
  199. $version = $info['manifest_version'];
  200. if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) {
  201. $incompatible[] = "$path: unsupported manifest_version: {$version}";
  202. }
  203. $autoload = $this->processAutoLoader( dirname( $path ), $info );
  204. // Set up the autoloader now so custom processors will work
  205. $GLOBALS['wgAutoloadClasses'] += $autoload;
  206. $autoloadClasses += $autoload;
  207. // get all requirements/dependencies for this extension
  208. $requires = $processor->getRequirements( $info );
  209. // validate the information needed and add the requirements
  210. if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) {
  211. $extDependencies[$info['name']] = $requires;
  212. }
  213. // Get extra paths for later inclusion
  214. $autoloaderPaths = array_merge( $autoloaderPaths,
  215. $processor->getExtraAutoloaderPaths( dirname( $path ), $info ) );
  216. // Compatible, read and extract info
  217. $processor->extractInfo( $path, $info, $version );
  218. }
  219. $data = $processor->getExtractedInfo();
  220. $data['warnings'] = $warnings;
  221. // check for incompatible extensions
  222. $incompatible = array_merge(
  223. $incompatible,
  224. $versionChecker
  225. ->setLoadedExtensionsAndSkins( $data['credits'] )
  226. ->checkArray( $extDependencies )
  227. );
  228. if ( $incompatible ) {
  229. if ( count( $incompatible ) === 1 ) {
  230. throw new Exception( $incompatible[0] );
  231. } else {
  232. throw new Exception( implode( "\n", $incompatible ) );
  233. }
  234. }
  235. // Need to set this so we can += to it later
  236. $data['globals']['wgAutoloadClasses'] = [];
  237. $data['autoload'] = $autoloadClasses;
  238. $data['autoloaderPaths'] = $autoloaderPaths;
  239. return $data;
  240. }
  241. protected function exportExtractedData( array $info ) {
  242. foreach ( $info['globals'] as $key => $val ) {
  243. // If a merge strategy is set, read it and remove it from the value
  244. // so it doesn't accidentally end up getting set.
  245. if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) {
  246. $mergeStrategy = $val[self::MERGE_STRATEGY];
  247. unset( $val[self::MERGE_STRATEGY] );
  248. } else {
  249. $mergeStrategy = 'array_merge';
  250. }
  251. // Optimistic: If the global is not set, or is an empty array, replace it entirely.
  252. // Will be O(1) performance.
  253. if ( !isset( $GLOBALS[$key] ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) {
  254. $GLOBALS[$key] = $val;
  255. continue;
  256. }
  257. if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) {
  258. // config setting that has already been overridden, don't set it
  259. continue;
  260. }
  261. switch ( $mergeStrategy ) {
  262. case 'array_merge_recursive':
  263. $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val );
  264. break;
  265. case 'array_replace_recursive':
  266. $GLOBALS[$key] = array_replace_recursive( $GLOBALS[$key], $val );
  267. break;
  268. case 'array_plus_2d':
  269. $GLOBALS[$key] = wfArrayPlus2d( $GLOBALS[$key], $val );
  270. break;
  271. case 'array_plus':
  272. $GLOBALS[$key] += $val;
  273. break;
  274. case 'array_merge':
  275. $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] );
  276. break;
  277. default:
  278. throw new UnexpectedValueException( "Unknown merge strategy '$mergeStrategy'" );
  279. }
  280. }
  281. foreach ( $info['defines'] as $name => $val ) {
  282. define( $name, $val );
  283. }
  284. foreach ( $info['autoloaderPaths'] as $path ) {
  285. require_once $path;
  286. }
  287. $this->loaded += $info['credits'];
  288. if ( $info['attributes'] ) {
  289. if ( !$this->attributes ) {
  290. $this->attributes = $info['attributes'];
  291. } else {
  292. $this->attributes = array_merge_recursive( $this->attributes, $info['attributes'] );
  293. }
  294. }
  295. foreach ( $info['callbacks'] as $name => $cb ) {
  296. if ( !is_callable( $cb ) ) {
  297. if ( is_array( $cb ) ) {
  298. $cb = '[ ' . implode( ', ', $cb ) . ' ]';
  299. }
  300. throw new UnexpectedValueException( "callback '$cb' is not callable" );
  301. }
  302. call_user_func( $cb, $info['credits'][$name] );
  303. }
  304. }
  305. /**
  306. * Loads and processes the given JSON file without delay
  307. *
  308. * If some extensions are already queued, this will load
  309. * those as well.
  310. *
  311. * @param string $path Absolute path to the JSON file
  312. */
  313. public function load( $path ) {
  314. $this->loadFromQueue(); // First clear the queue
  315. $this->queue( $path );
  316. $this->loadFromQueue();
  317. }
  318. /**
  319. * Whether a thing has been loaded
  320. * @param string $name
  321. * @return bool
  322. */
  323. public function isLoaded( $name ) {
  324. return isset( $this->loaded[$name] );
  325. }
  326. /**
  327. * @param string $name
  328. * @return array
  329. */
  330. public function getAttribute( $name ) {
  331. if ( isset( $this->attributes[$name] ) ) {
  332. return $this->attributes[$name];
  333. } else {
  334. return [];
  335. }
  336. }
  337. /**
  338. * Get information about all things
  339. *
  340. * @return array
  341. */
  342. public function getAllThings() {
  343. return $this->loaded;
  344. }
  345. /**
  346. * Mark a thing as loaded
  347. *
  348. * @param string $name
  349. * @param array $credits
  350. */
  351. protected function markLoaded( $name, array $credits ) {
  352. $this->loaded[$name] = $credits;
  353. }
  354. /**
  355. * Register classes with the autoloader
  356. *
  357. * @param string $dir
  358. * @param array $info
  359. * @return array
  360. */
  361. protected function processAutoLoader( $dir, array $info ) {
  362. if ( isset( $info['AutoloadClasses'] ) ) {
  363. // Make paths absolute, relative to the JSON file
  364. return array_map( function ( $file ) use ( $dir ) {
  365. return "$dir/$file";
  366. }, $info['AutoloadClasses'] );
  367. } else {
  368. return [];
  369. }
  370. }
  371. }