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