root = $root; $this->type = $type; $this->cache = $cache; $this->infoParser = $info_parser; $this->moduleHandler = $module_handler; $this->state = $state; $this->installProfile = $install_profile; } /** * Returns the extension discovery. * * @return \Drupal\Core\Extension\ExtensionDiscovery */ protected function getExtensionDiscovery() { return new ExtensionDiscovery($this->root); } /** * Resets the stored extension list. * * We don't reset statically added filenames, as it is a static cache which * logically can't change. This is done for performance reasons of the * installer. * * @return $this */ public function reset() { $this->extensions = NULL; $this->cache->delete($this->getListCacheId()); $this->extensionInfo = NULL; $this->cache->delete($this->getInfoCacheId()); $this->pathNames = NULL; try { $this->state->delete($this->getPathNamesCacheId()); } catch (DatabaseExceptionWrapper $e) { // Ignore exceptions caused by a non existing {key_value} table in the // early installer. } // @todo In the long run it would be great to add the reset, but the early // installer fails due to that. https://www.drupal.org/node/2719315 could // help to resolve with that. return $this; } /** * Returns the extension list cache ID. * * @return string * The list cache ID. */ protected function getListCacheId() { return 'core.extension.list.' . $this->type; } /** * Returns the extension info cache ID. * * @return string * The info cache ID. */ protected function getInfoCacheId() { return "system.{$this->type}.info"; } /** * Returns the extension filenames cache ID. * * @return string * The filename cache ID. */ protected function getPathNamesCacheId() { return "system.{$this->type}.files"; } /** * Determines if an extension exists in the filesystem. * * @param string $extension_name * The machine name of the extension. * * @return bool * TRUE if the extension exists (regardless installed or not) and FALSE if * not. */ public function exists($extension_name) { $extensions = $this->getList(); return isset($extensions[$extension_name]); } /** * Returns the human-readable name of the extension. * * @param string $extension_name * The machine name of the extension. * * @return string * The human-readable name of the extension. * * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException * If there is no extension with the supplied machine name. */ public function getName($extension_name) { return $this->get($extension_name)->info['name']; } /** * Returns a single extension. * * @param string $extension_name * The machine name of the extension. * * @return \Drupal\Core\Extension\Extension * A processed extension object for the extension with the specified machine * name. * * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException * If there is no extension with the supplied name. */ public function get($extension_name) { $extensions = $this->getList(); if (isset($extensions[$extension_name])) { return $extensions[$extension_name]; } throw new UnknownExtensionException("The {$this->type} $extension_name does not exist."); } /** * Returns all available extensions. * * @return \Drupal\Core\Extension\Extension[] * Processed extension objects, keyed by machine name. */ public function getList() { if ($this->extensions !== NULL) { return $this->extensions; } if ($cache = $this->cache->get($this->getListCacheId())) { $this->extensions = $cache->data; return $this->extensions; } $extensions = $this->doList(); $this->cache->set($this->getListCacheId(), $extensions); $this->extensions = $extensions; return $this->extensions; } /** * Scans the available extensions. * * Overriding this method gives other code the chance to add additional * extensions to this raw listing. * * @return \Drupal\Core\Extension\Extension[] * Unprocessed extension objects, keyed by machine name. */ protected function doScanExtensions() { return $this->getExtensionDiscovery()->scan($this->type); } /** * Builds the list of extensions. * * @return \Drupal\Core\Extension\Extension[] * Processed extension objects, keyed by machine name. * * @throws \Drupal\Core\Extension\InfoParserException * If one of the .info.yml files is incomplete, or causes a parsing error. */ protected function doList() { // Find extensions. $extensions = $this->doScanExtensions(); // Read info files for each extension. foreach ($extensions as $extension) { $extension->info = $this->createExtensionInfo($extension); // Invoke hook_system_info_alter() to give installed modules a chance to // modify the data in the .info.yml files if necessary. $this->moduleHandler->alter('system_info', $extension->info, $extension, $this->type); } return $extensions; } /** * Returns information about a specified extension. * * This function returns the contents of the .info.yml file for the specified * extension. * * @param string $extension_name * The name of an extension whose information shall be returned. * * @return mixed[] * An associative array of extension information. * * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException * If there is no extension with the supplied name. */ public function getExtensionInfo($extension_name) { $all_info = $this->getAllInstalledInfo(); if (isset($all_info[$extension_name])) { return $all_info[$extension_name]; } throw new UnknownExtensionException("The {$this->type} $extension_name does not exist or is not installed."); } /** * Returns an array of info files information of available extensions. * * This function returns the processed contents (with added defaults) of the * .info.yml files. * * @return array[] * An associative array of extension information arrays, keyed by extension * name. */ public function getAllAvailableInfo() { if ($this->extensionInfo === NULL) { $cache_id = $this->getInfoCacheId(); if ($cache = $this->cache->get($cache_id)) { $info = $cache->data; } else { $info = $this->recalculateInfo(); $this->cache->set($cache_id, $info); } $this->extensionInfo = $info; } return $this->extensionInfo; } /** * Returns a list of machine names of installed extensions. * * @return string[] * The machine names of all installed extensions of this type. */ abstract protected function getInstalledExtensionNames(); /** * Returns an array of info files information of installed extensions. * * This function returns the processed contents (with added defaults) of the * .info.yml files. * * @return array[] * An associative array of extension information arrays, keyed by extension * name. */ public function getAllInstalledInfo() { return array_intersect_key($this->getAllAvailableInfo(), array_flip($this->getInstalledExtensionNames())); } /** * Generates the information from .info.yml files for extensions of this type. * * @return array[] * An array of arrays of .info.yml entries keyed by the machine name. */ protected function recalculateInfo() { return array_map(function (Extension $extension) { return $extension->info; }, $this->getList()); } /** * Returns a list of extension file paths keyed by machine name. * * @return string[] */ public function getPathNames() { if ($this->pathNames === NULL) { $cache_id = $this->getPathNamesCacheId(); $this->pathNames = $this->state->get($cache_id); if ($this->pathNames === NULL) { $this->pathNames = $this->recalculatePathNames(); // Store filenames to allow static::getPathname() to retrieve them // without having to rebuild or scan the filesystem. $this->state->set($cache_id, $this->pathNames); } } return $this->pathNames; } /** * Generates a sorted list of .info.yml file locations for all extensions. * * @return string[] * An array of .info.yml file locations keyed by the extension machine name. */ protected function recalculatePathNames() { $extensions = $this->getList(); ksort($extensions); return array_map(function (Extension $extension) { return $extension->getPathname(); }, $extensions); } /** * Sets the pathname for an extension. * * This method is used in the Drupal bootstrapping phase, when the extension * system is not fully initialized, to manually set locations of modules and * profiles needed to complete bootstrapping. * * It is not recommended to call this method except in those rare cases. * * @param string $extension_name * The machine name of the extension. * @param string $pathname * The pathname of the extension which is to be set explicitly rather * than by consulting the dynamic extension listing. * * @internal * * @see ::getPathname */ public function setPathname($extension_name, $pathname) { $this->addedPathNames[$extension_name] = $pathname; } /** * Gets the info file path for an extension. * * The info path, whether provided, cached, or retrieved from the database, is * only returned if the file exists. * * This function plays a key role in allowing Drupal's extensions (modules, * themes, profiles, theme_engines, etc.) to be located in different places * depending on a site's configuration. For example, a module 'foo' may * legally be located in any of these four places: * * - core/modules/foo/foo.info.yml * - modules/foo/foo.info.yml * - sites/all/modules/foo/foo.info.yml * - sites/example.com/modules/foo/foo.info.yml * * while a theme 'bar' may be located in any of the following four places: * * - core/themes/bar/bar.info.yml * - themes/bar/bar.info.yml * - sites/all/themes/bar/bar.info.yml * - sites/example.com/themes/bar/bar.info.yml * * An installation profile maybe be located in any of the following places: * * - core/profiles/baz/baz.info.yml * - profiles/baz/baz.info.yml * * Calling ExtensionList::getPathname('foo') will give you one of the above, * depending on where the extension is located and what type it is. * * @param string $extension_name * The machine name of the extension for which the pathname is requested. * * @return string * The drupal-root relative filename and path of the requested extension's * .info.yml file. * * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException * If there is no extension with the supplied machine name. */ public function getPathname($extension_name) { if (isset($this->addedPathNames[$extension_name])) { return $this->addedPathNames[$extension_name]; } elseif (isset($this->pathNames[$extension_name])) { return $this->pathNames[$extension_name]; } elseif (($path_names = $this->getPathNames()) && isset($path_names[$extension_name])) { return $path_names[$extension_name]; } throw new UnknownExtensionException("The {$this->type} $extension_name does not exist."); } /** * Gets the path to an extension of a specific type (module, theme, etc.). * * The path is the directory in which the .info file is located. This name is * coming from \SplFileInfo. * * @param string $extension_name * The machine name of the extension for which the path is requested. * * @return string * The Drupal-root-relative path to the specified extension. * * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException * If there is no extension with the supplied name. */ public function getPath($extension_name) { return dirname($this->getPathname($extension_name)); } /** * Creates the info value for an extension object. * * @param \Drupal\Core\Extension\Extension $extension * The extension whose info is to be altered. * * @return array * The extension info array. */ protected function createExtensionInfo(Extension $extension) { $info = $this->infoParser->parse($extension->getPathname()); // Add the info file modification time, so it becomes available for // contributed extensions to use for ordering extension lists. $info['mtime'] = $extension->getFileInfo()->getMTime(); // Merge extension type-specific defaults, making sure to replace NULL // values. foreach ($this->defaults as $key => $default_value) { if (!isset($info[$key])) { $info[$key] = $default_value; } } return $info; } /** * Tests the compatibility of an extension. * * @param string $name * The extension name to check. * * @return bool * TRUE if the extension is incompatible and FALSE if not. * * @throws \Drupal\Core\Extension\Exception\UnknownExtensionException * If there is no extension with the supplied name. */ public function checkIncompatibility($name) { $extension = $this->get($name); return $extension->info['core_incompatible'] || (isset($extension->info['php']) && version_compare(phpversion(), $extension->info['php']) < 0); } /** * Array sorting callback; sorts extensions by their name. * * @param \Drupal\Core\Extension\Extension $a * The first extension to compare. * @param \Drupal\Core\Extension\Extension $b * The second extension to compare. * * @return int * Less than 0 if $a is less than $b, more than 0 if $a is greater than $b, * and 0 if they are equal. */ public static function sortByName(Extension $a, Extension $b): int { return strcasecmp($a->info['name'], $b->info['name']); } }