queued[$path] = $mtime; } /** * @throws MWException If the queue is already marked as finished (no further things should * be loaded then). */ public function loadFromQueue() { global $wgVersion, $wgDevelopmentWarnings; if ( !$this->queued ) { return; } if ( $this->finished ) { throw new MWException( "The following paths tried to load late: " . implode( ', ', array_keys( $this->queued ) ) ); } // A few more things to vary the cache on $versions = [ 'registration' => self::CACHE_VERSION, 'mediawiki' => $wgVersion ]; // We use a try/catch because we don't want to fail here // if $wgObjectCaches is not configured properly for APC setup try { $cache = MediaWikiServices::getInstance()->getLocalServerObjectCache(); } catch ( MWException $e ) { $cache = new EmptyBagOStuff(); } // See if this queue is in APC $key = $cache->makeKey( 'registration', md5( json_encode( $this->queued + $versions ) ) ); $data = $cache->get( $key ); if ( $data ) { $this->exportExtractedData( $data ); } else { $data = $this->readFromQueue( $this->queued ); $this->exportExtractedData( $data ); // Do this late since we don't want to extract it since we already // did that, but it should be cached $data['globals']['wgAutoloadClasses'] += $data['autoload']; unset( $data['autoload'] ); if ( !( $data['warnings'] && $wgDevelopmentWarnings ) ) { // If there were no warnings that were shown, cache it $cache->set( $key, $data, 60 * 60 * 24 ); } } $this->queued = []; } /** * Get the current load queue. Not intended to be used * outside of the installer. * * @return array */ public function getQueue() { return $this->queued; } /** * Clear the current load queue. Not intended to be used * outside of the installer. */ public function clearQueue() { $this->queued = []; } /** * After this is called, no more extensions can be loaded * * @since 1.29 */ public function finish() { $this->finished = true; } /** * Process a queue of extensions and return their extracted data * * @param array $queue keys are filenames, values are ignored * @return array extracted info * @throws Exception */ public function readFromQueue( array $queue ) { global $wgVersion; $autoloadClasses = []; $autoloadNamespaces = []; $autoloaderPaths = []; $processor = new ExtensionProcessor(); $versionChecker = new VersionChecker( $wgVersion ); $extDependencies = []; $incompatible = []; $warnings = false; foreach ( $queue as $path => $mtime ) { $json = file_get_contents( $path ); if ( $json === false ) { throw new Exception( "Unable to read $path, does it exist?" ); } $info = json_decode( $json, /* $assoc = */ true ); if ( !is_array( $info ) ) { throw new Exception( "$path is not a valid JSON file." ); } if ( !isset( $info['manifest_version'] ) ) { wfDeprecated( "{$info['name']}'s extension.json or skin.json does not have manifest_version", '1.29' ); $warnings = true; // For backwards-compatability, assume a version of 1 $info['manifest_version'] = 1; } $version = $info['manifest_version']; if ( $version < self::OLDEST_MANIFEST_VERSION || $version > self::MANIFEST_VERSION ) { $incompatible[] = "$path: unsupported manifest_version: {$version}"; } $dir = dirname( $path ); if ( isset( $info['AutoloadClasses'] ) ) { $autoload = $this->processAutoLoader( $dir, $info['AutoloadClasses'] ); $GLOBALS['wgAutoloadClasses'] += $autoload; $autoloadClasses += $autoload; } if ( isset( $info['AutoloadNamespaces'] ) ) { $autoloadNamespaces += $this->processAutoLoader( $dir, $info['AutoloadNamespaces'] ); } // get all requirements/dependencies for this extension $requires = $processor->getRequirements( $info ); // validate the information needed and add the requirements if ( is_array( $requires ) && $requires && isset( $info['name'] ) ) { $extDependencies[$info['name']] = $requires; } // Get extra paths for later inclusion $autoloaderPaths = array_merge( $autoloaderPaths, $processor->getExtraAutoloaderPaths( $dir, $info ) ); // Compatible, read and extract info $processor->extractInfo( $path, $info, $version ); } $data = $processor->getExtractedInfo(); $data['warnings'] = $warnings; // check for incompatible extensions $incompatible = array_merge( $incompatible, $versionChecker ->setLoadedExtensionsAndSkins( $data['credits'] ) ->checkArray( $extDependencies ) ); if ( $incompatible ) { if ( count( $incompatible ) === 1 ) { throw new Exception( $incompatible[0] ); } else { throw new Exception( implode( "\n", $incompatible ) ); } } // Need to set this so we can += to it later $data['globals']['wgAutoloadClasses'] = []; $data['autoload'] = $autoloadClasses; $data['autoloaderPaths'] = $autoloaderPaths; $data['autoloaderNS'] = $autoloadNamespaces; return $data; } protected function exportExtractedData( array $info ) { foreach ( $info['globals'] as $key => $val ) { // If a merge strategy is set, read it and remove it from the value // so it doesn't accidentally end up getting set. if ( is_array( $val ) && isset( $val[self::MERGE_STRATEGY] ) ) { $mergeStrategy = $val[self::MERGE_STRATEGY]; unset( $val[self::MERGE_STRATEGY] ); } else { $mergeStrategy = 'array_merge'; } // Optimistic: If the global is not set, or is an empty array, replace it entirely. // Will be O(1) performance. if ( !array_key_exists( $key, $GLOBALS ) || ( is_array( $GLOBALS[$key] ) && !$GLOBALS[$key] ) ) { $GLOBALS[$key] = $val; continue; } if ( !is_array( $GLOBALS[$key] ) || !is_array( $val ) ) { // config setting that has already been overridden, don't set it continue; } switch ( $mergeStrategy ) { case 'array_merge_recursive': $GLOBALS[$key] = array_merge_recursive( $GLOBALS[$key], $val ); break; case 'array_replace_recursive': $GLOBALS[$key] = array_replace_recursive( $GLOBALS[$key], $val ); break; case 'array_plus_2d': $GLOBALS[$key] = wfArrayPlus2d( $GLOBALS[$key], $val ); break; case 'array_plus': $GLOBALS[$key] += $val; break; case 'array_merge': $GLOBALS[$key] = array_merge( $val, $GLOBALS[$key] ); break; default: throw new UnexpectedValueException( "Unknown merge strategy '$mergeStrategy'" ); } } if ( isset( $info['autoloaderNS'] ) ) { AutoLoader::$psr4Namespaces += $info['autoloaderNS']; } foreach ( $info['defines'] as $name => $val ) { define( $name, $val ); } foreach ( $info['autoloaderPaths'] as $path ) { if ( file_exists( $path ) ) { require_once $path; } } $this->loaded += $info['credits']; if ( $info['attributes'] ) { if ( !$this->attributes ) { $this->attributes = $info['attributes']; } else { $this->attributes = array_merge_recursive( $this->attributes, $info['attributes'] ); } } foreach ( $info['callbacks'] as $name => $cb ) { if ( !is_callable( $cb ) ) { if ( is_array( $cb ) ) { $cb = '[ ' . implode( ', ', $cb ) . ' ]'; } throw new UnexpectedValueException( "callback '$cb' is not callable" ); } call_user_func( $cb, $info['credits'][$name] ); } } /** * Loads and processes the given JSON file without delay * * If some extensions are already queued, this will load * those as well. * * @param string $path Absolute path to the JSON file */ public function load( $path ) { $this->loadFromQueue(); // First clear the queue $this->queue( $path ); $this->loadFromQueue(); } /** * Whether a thing has been loaded * @param string $name * @return bool */ public function isLoaded( $name ) { return isset( $this->loaded[$name] ); } /** * @param string $name * @return array */ public function getAttribute( $name ) { if ( isset( $this->attributes[$name] ) ) { return $this->attributes[$name]; } else { return []; } } /** * Get information about all things * * @return array */ public function getAllThings() { return $this->loaded; } /** * Mark a thing as loaded * * @param string $name * @param array $credits */ protected function markLoaded( $name, array $credits ) { $this->loaded[$name] = $credits; } /** * Fully expand autoloader paths * * @param string $dir * @param array $files * @return array */ protected function processAutoLoader( $dir, array $files ) { // Make paths absolute, relative to the JSON file foreach ( $files as &$file ) { $file = "$dir/$file"; } return $files; } }