diff --git a/core/core.services.yml b/core/core.services.yml index 27e39a2..85b777d 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -470,6 +470,12 @@ services: - { name: service_collector, tag: 'module_install.uninstall_validator', call: addUninstallValidator } arguments: ['@app.root', '@module_handler', '@kernel'] lazy: true + module_listing: + class: Drupal\Core\Extension\ModuleExtensionList + arguments: ['@app.root', 'module', '@cache.default', '@info_parser', '@module_handler', '@config.factory', '@profile_listing'] + profile_listing: + class: Drupal\Core\Extension\ProfileExtensionList + arguments: ['@app.root', 'profile', '@cache.default', '@info_parser', '@module_handler'] content_uninstall_validator: class: Drupal\Core\Entity\ContentUninstallValidator tags: diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index 094feb6..e601559 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -199,11 +199,23 @@ function drupal_get_filename($type, $name, $filename = NULL) { return 'core/core.info.yml'; } - // Profiles are converted into modules in system_rebuild_module_data(). - // @todo Remove false-exposure of profiles as modules. - if ($type == 'profile') { - $type = 'module'; + if ($type === 'module' || $type === 'profile') { + $service = $type . '_listing'; + if (isset($filename)) { + // Manually add the info file path of an extension. + return \Drupal::service($service)->setFilename($name, $filename); + } + else { + try { + return \Drupal::service($service)->getFilename($name); + } + catch (\InvalidArgumentException $e) { + // Catch the exception and trigger error to maintain existing behavior. + trigger_error(SafeMarkup::format('The following @type is missing from the file system: @name', array('@type' => $type, '@name' => $name)), E_USER_WARNING); + } + } } + if (!isset($files[$type])) { $files[$type] = array(); } @@ -212,15 +224,6 @@ function drupal_get_filename($type, $name, $filename = NULL) { $files[$type][$name] = $filename; } elseif (!isset($files[$type][$name])) { - // If the pathname of the requested extension is not known, try to retrieve - // the list of extension pathnames from various providers, checking faster - // providers first. - // Retrieve the current module list (derived from the service container). - if ($type == 'module' && \Drupal::hasService('module_handler')) { - foreach (\Drupal::moduleHandler()->getModuleList() as $module_name => $module) { - $files[$type][$module_name] = $module->getPathname(); - } - } // If still unknown, retrieve the file list prepared in state by // system_rebuild_module_data() and // \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData(). diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index d5fcd9c..c6874cc 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -301,12 +301,6 @@ function install_begin_request($class_loader, &$install_state) { // Allow command line scripts to override server variables used by Drupal. require_once __DIR__ . '/bootstrap.inc'; - // Before having installed the system module and being able to do a module - // rebuild, prime the drupal_get_filename() static cache with the module's - // exact location. - // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_get_filename('module', 'system', 'core/modules/system/system.info.yml'); - // If the hash salt leaks, it becomes possible to forge a valid testing user // agent, install a new copy of Drupal, and take over the original site. // The user agent header is used to pass a database prefix in the request when diff --git a/core/includes/install.inc b/core/includes/install.inc index bae87d7..11f30a3 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -615,6 +615,11 @@ function drupal_install_system($install_state) { $kernel->rebuildContainer(FALSE); $kernel->prepareLegacyRequest($request); + // Before having installed the system module and being able to do a module + // rebuild, prime the ModuleExtensionList static cache with the module's + // exact location. + \Drupal::service('module_listing')->setFilename('system', 'core/modules/system/system.info.yml'); + // Install base system configuration. \Drupal::service('config.installer')->installDefaultConfig('core', 'core'); diff --git a/core/includes/module.inc b/core/includes/module.inc index bc50d2c..cd2575c 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -57,7 +57,7 @@ function system_list($type) { */ function system_list_reset() { drupal_static_reset('system_list'); - drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('module_listing')->reset(); \Drupal::cache('bootstrap')->delete('system_list'); } diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index d54f07c..b59f3b2 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -22,6 +22,7 @@ use Drupal\Core\File\MimeType\MimeTypeGuesser; use Drupal\Core\Http\TrustedHostsRequestFactory; use Drupal\Core\Language\Language; +use Drupal\Core\Session\AccountProxyInitializedInterface; use Drupal\Core\Site\Settings; use Symfony\Cmf\Component\Routing\RouteObjectInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -786,7 +787,14 @@ protected function initializeContainer() { if (isset($this->container)) { // Save the id of the currently logged in user. if ($this->container->initialized('current_user')) { - $current_user_id = $this->container->get('current_user')->id(); + $current_user = $this->container->get('current_user'); + // Ensure to not accidentally initialize the user. + if ($current_user instanceof AccountProxyInitializedInterface) { + $current_user_id = $current_user->accountIsInitialized() ? $current_user->id() : 0; + } + else { + $current_user_id = $current_user->id(); + } } // If there is a session, close and save it. diff --git a/core/lib/Drupal/Core/Extension/ExtensionList.php b/core/lib/Drupal/Core/Extension/ExtensionList.php new file mode 100644 index 0000000..ebc2425 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ExtensionList.php @@ -0,0 +1,449 @@ +root = $root; + $this->type = $type; + $this->cache = $cache; + $this->infoParser = $info_parser; + $this->moduleHandler = $module_handler; + $this->extensionDiscovery = $this->getExtensionDiscovery(); + } + + /** + * Returns the extension discovery. + * + * @return \Drupal\Core\Extension\ExtensionDiscovery + */ + protected function getExtensionDiscovery() { + return new ExtensionDiscovery($this->root); + } + + /** + * Resets the stored extension list. + */ + public function reset() { + $this->extensions = NULL; + $this->cache->delete($this->getCacheId()); + $this->extensionInfo = NULL; + $this->cache->delete("system.{$this->type}.info"); + $this->fileNames = NULL; + $this->cache->delete("system.{$this->type}.files"); + $this->addedFileNames = NULL; + return $this; + } + + /** + * Returns the used cache ID. + * + * @return string + */ + protected function getCacheId() { + return 'core.extension_listing.' . $this->type; + } + + /** + * Determines if an extension exists in the filesystem. + * + * @param string $name + * The machine name of the extension. + * + * @return bool + * true if the extension exists (whether installed or not) and false if not. + */ + public function extensionExists($name) { + $extensions = $this->listExtensions(); + return isset($extensions[$name]); + } + + /** + * Returns the human readable name of the extension. + * + * @param string $machine_name + * The extension name. + * + * @return string + * The human readable name of the extension. + * + * @throws \InvalidArgumentException + * If there is no extension with the supplied machine name. + */ + public function getName($machine_name) { + return $this->getExtension($machine_name)->info['name']; + } + + /** + * Returns a single extension. + * + * @param string $name + * The extension name. + * + * @return \Drupal\Core\Extension\Extension + * + * @throws \InvalidArgumentException + * If there is no extension with the supplied name. + */ + public function getExtension($name) { + $extensions = $this->listExtensions(); + if (isset($extensions[$name])) { + return $extensions[$name]; + } + + throw new \InvalidArgumentException("The {$this->type} $name does not exist."); + } + + /** + * Returns all available extensions. + * + * @return \Drupal\Core\Extension\Extension[] + */ + public function listExtensions() { + if (isset($this->extensions)) { + return $this->extensions; + } + if ($cache = $this->cache->get($this->getCacheId())) { + $this->extensions = $cache->data; + return $this->extensions; + } + $extensions = $this->doListExtensions(); + $this->cache->set($this->getCacheId(), $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[] + */ + protected function doScanExtensions() { + return $this->extensionDiscovery->scan($this->type); + } + + /** + * Build the actual list of extensions before caching it. + * + * @return \Drupal\Core\Extension\Extension[] + */ + protected function doListExtensions() { + // Find extensions. + $extensions = $this->doScanExtensions(); + + // Read info files for each extension. + foreach ($extensions as $name => $extension) { + // Look for the info file. + $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. + $extension->info['mtime'] = $extension->getMTime(); + + // Merge in defaults and save. + $extensions[$name]->info = $extension->info + $this->defaults; + + // 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', $extensions[$name]->info, $extensions[$name], $this->type); + } + + return $extensions; + } + + /** + * Returns information about a specified extension. + * + * This function returns the contents of the .info.yml file for the specified + * installed extension. + * + * @param string $name + * The name of an extension whose information shall be returned. If + * $name does not exist or is not enabled, an empty array will be returned. + * + * @return array + * An associative array of extension information. + * + * @throws \InvalidArgumentException + * If there is no extension with the supplied name. + */ + public function getInfo($name) { + // Ensure that $this->extensionInfo is primed. + $this->getAllInfo(); + if (isset($this->extensionInfo[$name])) { + return $this->extensionInfo[$name]; + } + throw new \InvalidArgumentException("The {$this->type} $name does not exist."); + } + + /** + * Returns an array of information about enabled modules or themes. + * + * This function returns the contents of the .info.yml file for each installed + * extension. + * + * @return array + * An associative array of extension information keyed by name. If no + * records are available, an empty array is returned. + */ + public function getAllInfo() { + if (!isset($this->extensionInfo)) { + if ($cache = $this->cache->get("system.{$this->type}.info")) { + $info = $cache->data; + } + else { + $info = $this->recalculateInfo(); + $this->cache->set("system.{$this->type}.info", $info); + } + $this->extensionInfo = $info; + } + return $this->extensionInfo; + } + + /** + * Generates the information from .info.yml files for extensions of this type. + * + * The information is placed in cache with the "system.{extension_type}.info" + * key. + * + * @return array + * An array of arrays of .info.yml entries keyed by the extension name. + */ + protected function recalculateInfo() { + $info = []; + foreach ($this->listExtensions() as $name => $extension) { + $info[$name] = $extension->info; + } + return $info; + } + + /** + * Returns a list of extension folder names keyed by extension name. + * + * @return string[] + */ + public function getFilenames() { + if (!isset($this->fileNames)) { + if ($cache = $this->cache->get("system.{$this->type}.files")) { + $file_names = $cache->data; + } + else { + $file_names = $this->recalculateFilenames(); + $this->cache->set("system.{$this->type}.files", $file_names); + } + $this->fileNames = $file_names; + } + return $this->fileNames; + } + + /** + * Generates a sorted list of .info.yml file locations for all extensions. + * + * The information is placed in cache with the "system.{extension_type}.files" + * key. + * + * @return string[] + * An array of .info.yml file locations keyed by the extension name. + */ + protected function recalculateFilenames() { + $file_names = []; + $extensions = $this->listExtensions(); + ksort($extensions); + foreach ($extensions as $name => $extension) { + $file_names[$name] = $extension->getPathname(); + } + return $file_names; + } + + /** + * Sets the filename 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 name of the extension for which the filename is requested. + * @param string $filename + * The filename of the extension which is to be set explicitly rather + * than by consulting the dynamic extension listing. + */ + public function setFilename($extension_name, $filename) { + $this->addedFileNames[$extension_name] = $filename; + } + + /** + * Gets the filename for a system resource. + * + * The filename, 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 three 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 similar 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 + * + * Calling ExtensionList::getFilename('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 name of the extension for which the filename is requested. + * + * @return string + * The filename of the requested extension's .info.yml file. + * + * @throws \InvalidArgumentException + * If there is no extension with the supplied name. + */ + public function getFilename($extension_name) { + // Ensure that $this->fileNames is primed. + $this->getFilenames(); + if (isset($this->addedFileNames[$extension_name])) { + return $this->addedFileNames[$extension_name]; + } + else if (isset($this->fileNames[$extension_name])) { + return $this->fileNames[$extension_name]; + } + throw new \InvalidArgumentException("The {$this->type} $extension_name does not exist."); + } + + + /** + * Gets the path to an extension of a specific type (module, theme, etc.). + * + * @param string $extension_name + * The name of the extension for which the path is requested. + * + * @return string + * The drupal-root-relative path to the specified extension. + * + * @throws \InvalidArgumentException + * If there is no extension with the supplied name. + */ + public function getPath($extension_name) { + return dirname($this->getFilename($extension_name)); + } + +} diff --git a/core/lib/Drupal/Core/Extension/ModuleExtensionList.php b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php new file mode 100644 index 0000000..516a550 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ModuleExtensionList.php @@ -0,0 +1,170 @@ + [], + 'description' => '', + 'package' => 'Other', + 'version' => NULL, + 'php' => DRUPAL_MINIMUM_PHP, + ]; + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The profile list needed by this module list. + * + * @var \Drupal\Core\Extension\ExtensionList + */ + protected $profileList; + + /** + * Constructs a new ModuleExtensionList instance. + * + * @param string $root + * The app root. + * @param string $type + * The extension type. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * The cache. + * @param \Drupal\Core\Extension\InfoParserInterface $info_parser + * The info parser. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + * @param \Drupal\Core\State\StateInterface $state + * The state service. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Extension\ExtensionList $profile_list + * The site profile listing. + */ + public function __construct($root, $type, CacheBackendInterface $cache, InfoParserInterface $info_parser, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, ExtensionList $profile_list) { + parent::__construct($root, $type, $cache, $info_parser, $module_handler); + + $this->configFactory = $config_factory; + $this->profileList = $profile_list; + } + + /** + * {@inheritdoc} + */ + protected function doScanExtensions() { + $extensions = parent::doScanExtensions(); + + // Find installation profiles. + $profiles = $this->profileList->listExtensions(); + + // Include the installation profile in modules that are loaded. + if ($profile = drupal_get_profile()) { + $extensions[$profile] = $profiles[$profile]; + // Installation profile hooks are always executed last. + $extensions[$profile]->weight = 1000; + } + + return $extensions; + } + + /** + * {@inheritdoc} + */ + protected function doListExtensions() { + // Find installation profiles. This needs to happen before performing a + // module scan as the module scan needs to know what the active profile is. + $profiles = $this->profileList->listExtensions(); + $profile = drupal_get_profile(); + if ($profile && isset($profiles[$profile])) { + // Set the profile in the ExtensionDiscovery so we can scan from the right + // profile directory. + $this->extensionDiscovery->setProfileDirectories([ + $profile => $profiles[$profile]->getPathname(), + ]); + } + + // Find modules. + $extensions = parent::doListExtensions(); + // It is possible that a module was marked as required by + // hook_system_info_alter() and modules that it depends on are not required. + foreach ($extensions as $extension) { + $this->ensureRequiredDependencies($extension, $extensions); + } + + if ($profile) { + // Installation profiles are hidden by default, unless explicitly + // specified otherwise in the .info.yml file. + if (!isset($extensions[$profile]->info['hidden'])) { + $extensions[$profile]->info['hidden'] = TRUE; + } + + if (isset($extensions[$profile])) { + // The installation profile is required, if it's a valid module. + $extensions[$profile]->info['required'] = TRUE; + // Add a default distribution name if the profile did not provide one. + // @see install_profile_info() + // @see drupal_install_profile_distribution_name() + if (!isset($extensions[$profile]->info['distribution']['name'])) { + $extensions[$profile]->info['distribution']['name'] = 'Drupal'; + } + } + } + + // Add status, weight, and schema version. + $installed_modules = $this->configFactory->get('core.extension')->get('module') ?: []; + foreach ($extensions as $name => $module) { + $module->weight = isset($installed_modules[$name]) ? $installed_modules[$name] : 0; + $module->status = (int) isset($installed_modules[$name]); + $module->schema_version = SCHEMA_UNINSTALLED; + } + $extensions = $this->moduleHandler->buildModuleDependencies($extensions); + + return $extensions; + } + + /** + * Ensures that dependencies of required modules are also required. + * + * @param \Drupal\Core\Extension\Extension $module + * The module info. + * @param \Drupal\Core\Extension\Extension[] $modules + * The array of all module info. + */ + protected function ensureRequiredDependencies(Extension $module, array $modules = []) { + if (!empty($module->info['required'])) { + foreach ($module->info['dependencies'] as $dependency) { + $dependency_name = ModuleHandler::parseDependency($dependency)['name']; + if (!isset($modules[$dependency_name]->info['required'])) { + $modules[$dependency_name]->info['required'] = TRUE; + $modules[$dependency_name]->info['explanation'] = $this->t('Dependency of required module @module', array('@module' => $module->info['name'])); + // Ensure any dependencies it has are required. + $this->ensureRequiredDependencies($modules[$dependency_name], $modules); + } + } + } + } + +} diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index 05f1e4f..2b3ae21 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -722,8 +722,7 @@ public function getModuleDirectories() { * {@inheritdoc} */ public function getName($module) { - $info = system_get_info('module', $module); - return isset($info['name']) ? $info['name'] : $module; + return \Drupal::service('module_listing')->getName($module); } } diff --git a/core/lib/Drupal/Core/Extension/ModuleInstaller.php b/core/lib/Drupal/Core/Extension/ModuleInstaller.php index f8300cb..678611b 100644 --- a/core/lib/Drupal/Core/Extension/ModuleInstaller.php +++ b/core/lib/Drupal/Core/Extension/ModuleInstaller.php @@ -171,7 +171,7 @@ public function install(array $module_list, $enable_dependencies = TRUE) { $module_filenames[$name] = $current_module_filenames[$name]; } else { - $module_path = drupal_get_path('module', $name); + $module_path = \Drupal::service('module_listing')->getPath($name); $pathname = "$module_path/$name.info.yml"; $filename = file_exists($module_path . "/$name.module") ? "$name.module" : NULL; $module_filenames[$name] = new Extension($this->root, 'module', $pathname, $filename); @@ -187,10 +187,10 @@ public function install(array $module_list, $enable_dependencies = TRUE) { $this->moduleHandler->load($module); module_load_install($module); - // Clear the static cache of system_rebuild_module_data() to pick up the + // Clear the static cache of ModuleExtensionList to pick up the // new module, since it merges the installation status of modules into // its statically cached list. - drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('module_listing')->reset(); // Update the kernel to include it. $this->updateKernel($module_filenames); @@ -426,10 +426,10 @@ public function uninstall(array $module_list, $uninstall_dependents = TRUE) { // Remove any potential cache bins provided by the module. $this->removeCacheBins($module); - // Clear the static cache of system_rebuild_module_data() to pick up the + // Clear the static cache of ModuleExtensionList to pick up the // new module, since it merges the installation status of modules into // its statically cached list. - drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('module_listing')->reset(); // Clear plugin manager caches. \Drupal::getContainer()->get('plugin.cache_clearer')->clearCachedDefinitions(); diff --git a/core/lib/Drupal/Core/Extension/ProfileExtensionList.php b/core/lib/Drupal/Core/Extension/ProfileExtensionList.php new file mode 100644 index 0000000..8bae168 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ProfileExtensionList.php @@ -0,0 +1,18 @@ + [], + 'description' => '', + 'package' => 'Other', + 'version' => NULL, + 'php' => DRUPAL_MINIMUM_PHP, + ]; + +} diff --git a/core/lib/Drupal/Core/Session/AccountProxy.php b/core/lib/Drupal/Core/Session/AccountProxy.php index 9bc8c1b..f606104 100644 --- a/core/lib/Drupal/Core/Session/AccountProxy.php +++ b/core/lib/Drupal/Core/Session/AccountProxy.php @@ -18,7 +18,7 @@ * allows legacy code to change the current user where the user cannot be * directly injected into dependent code. */ -class AccountProxy implements AccountProxyInterface { +class AccountProxy implements AccountProxyInterface, AccountProxyInitializedInterface { /** * The instantiated account. @@ -50,6 +50,14 @@ public function setAccount(AccountInterface $account) { /** * {@inheritdoc} */ + public function accountIsInitialized() { + return isset($this->account); + } + + + /** + * {@inheritdoc} + */ public function getAccount() { if (!isset($this->account)) { if ($this->initialAccountId) { diff --git a/core/lib/Drupal/Core/Session/AccountProxyInitializedInterface.php b/core/lib/Drupal/Core/Session/AccountProxyInitializedInterface.php new file mode 100644 index 0000000..11a0644 --- /dev/null +++ b/core/lib/Drupal/Core/Session/AccountProxyInitializedInterface.php @@ -0,0 +1,19 @@ +get('system.module.files', array()); - return isset($modules[$this->name]); + return \Drupal::service('module_listing')->extensionExists($this->name); } /** diff --git a/core/modules/book/src/Tests/BookUninstallTest.php b/core/modules/book/src/Tests/BookUninstallTest.php index 0a18fb1..45dac11 100644 --- a/core/modules/book/src/Tests/BookUninstallTest.php +++ b/core/modules/book/src/Tests/BookUninstallTest.php @@ -81,7 +81,7 @@ public function testBookUninstall() { $book_node->delete(); // No nodes exist therefore the book module is not required. - $module_data = _system_rebuild_module_data(); + $module_data = \Drupal::service('module_listing')->reset()->listExtensions(); $this->assertFalse(isset($module_data['book']->info['required']), 'The book module is not required.'); $node = Node::create(array('title' => $this->randomString(), 'type' => $content_type->id())); diff --git a/core/modules/config/src/Tests/ConfigImportAllTest.php b/core/modules/config/src/Tests/ConfigImportAllTest.php index d496973..9fda5fd 100644 --- a/core/modules/config/src/Tests/ConfigImportAllTest.php +++ b/core/modules/config/src/Tests/ConfigImportAllTest.php @@ -102,7 +102,7 @@ public function testInstallUninstall() { // Ensure that only core required modules and the install profile can not be uninstalled. $validation_reasons = \Drupal::service('module_installer')->validateUninstall(array_keys($all_modules)); - $this->assertEqual(['standard', 'system', 'user'], array_keys($validation_reasons)); + $this->assertEqual(['system', 'user', 'standard'], array_keys($validation_reasons)); $modules_to_uninstall = array_filter($all_modules, function ($module) use ($validation_reasons) { // Filter required and not enabled modules. diff --git a/core/modules/filter/src/Tests/FilterAPITest.php b/core/modules/filter/src/Tests/FilterAPITest.php index a6a3d56..48bd3d5 100644 --- a/core/modules/filter/src/Tests/FilterAPITest.php +++ b/core/modules/filter/src/Tests/FilterAPITest.php @@ -490,7 +490,7 @@ public function testDependencyRemoval() { drupal_static_reset('filter_formats'); \Drupal::entityManager()->getStorage('filter_format')->resetCache(); - $module_data = _system_rebuild_module_data(); + $module_data =\Drupal::service('module_listing')->reset()->listExtensions(); $this->assertFalse(isset($module_data['filter_test']->info['required']), 'The filter_test module is required.'); // Verify that a dependency exists on the module that provides the filter diff --git a/core/modules/simpletest/src/KernelTestBase.php b/core/modules/simpletest/src/KernelTestBase.php index a987d13..3e04583 100644 --- a/core/modules/simpletest/src/KernelTestBase.php +++ b/core/modules/simpletest/src/KernelTestBase.php @@ -105,7 +105,7 @@ function __construct($test_id = NULL) { protected function beforePrepareEnvironment() { // Copy/prime extension file lists once to avoid filesystem scans. if (!isset($this->moduleFiles)) { - $this->moduleFiles = \Drupal::state()->get('system.module.files') ?: array(); + $this->moduleFiles = \Drupal::cache('default')->get('system.module.files') ?: array(); $this->themeFiles = \Drupal::state()->get('system.theme.files') ?: array(); } } @@ -227,7 +227,7 @@ protected function setUp() { // Re-inject extension file listings into state, unless the key/value // service was overridden (in which case its storage does not exist yet). if ($this->container->get('keyvalue') instanceof KeyValueMemoryFactory) { - $this->container->get('state')->set('system.module.files', $this->moduleFiles); + $this->container->get('cache.default')->set('system.module.files', $this->moduleFiles); $this->container->get('state')->set('system.theme.files', $this->themeFiles); } diff --git a/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php b/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php index 27ca162..dc6763a 100644 --- a/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php +++ b/core/modules/system/src/Tests/Bootstrap/GetFilenameUnitTest.php @@ -26,8 +26,6 @@ function testDrupalGetFilename() { $install_state['parameters']['profile'] = 'testing'; // Rebuild system.module.files state data. - // @todo Remove as part of https://www.drupal.org/node/2186491 - drupal_static_reset('system_rebuild_module_data'); system_rebuild_module_data(); // Retrieving the location of a module. diff --git a/core/modules/system/system.module b/core/modules/system/system.module index 2572a39..0156eb8 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -889,27 +889,15 @@ function system_check_directory($form_element, FormStateInterface $form_state) { */ function system_get_info($type, $name = NULL) { if ($type == 'module') { - $info = &drupal_static(__FUNCTION__); - if (!isset($info)) { - if ($cache = \Drupal::cache()->get('system.module.info')) { - $info = $cache->data; - } - else { - $data = system_rebuild_module_data(); - foreach (\Drupal::moduleHandler()->getModuleList() as $module => $filename) { - if (isset($data[$module])) { - $info[$module] = $data[$module]->info; - } - } - // Store the module information in cache. This cache is cleared by - // calling system_rebuild_module_data(), for example, when listing - // modules, (un)installing modules, importing configuration, updating - // the site and when flushing all the caches. - \Drupal::cache()->set('system.module.info', $info); - } + if (isset($name)) { + return \Drupal::service('module_listing')->getInfo($name); + } + else { + return \Drupal::service('module_listing')->getAllInfo(); } } else { + // @todo move into ThemeExtensionList https://www.drupal.org/node/2659940 $info = array(); $list = system_list($type); foreach ($list as $shortname => $item) { @@ -917,11 +905,11 @@ function system_get_info($type, $name = NULL) { $info[$shortname] = $item->info; } } + if (isset($name)) { + return isset($info[$name]) ? $info[$name] : array(); + } + return $info; } - if (isset($name)) { - return isset($info[$name]) ? $info[$name] : array(); - } - return $info; } /** @@ -931,105 +919,7 @@ function system_get_info($type, $name = NULL) { * An associative array of module information. */ function _system_rebuild_module_data() { - $listing = new ExtensionDiscovery(\Drupal::root()); - - // Find installation profiles. This needs to happen before performing a - // module scan as the module scan requires knowing what the active profile is. - // @todo Remove as part of https://www.drupal.org/node/2186491. - $profiles = $listing->scan('profile'); - $profile = drupal_get_profile(); - if ($profile && isset($profiles[$profile])) { - // Prime the drupal_get_filename() static cache with the profile info file - // location so we can use drupal_get_path() on the active profile during - // the module scan. - // @todo Remove as part of https://www.drupal.org/node/2186491. - drupal_get_filename('profile', $profile, $profiles[$profile]->getPathname()); - } - - // Find modules. - $modules = $listing->scan('module'); - // Include the installation profile in modules that are loaded. - if ($profile) { - $modules[$profile] = $profiles[$profile]; - // Installation profile hooks are always executed last. - $modules[$profile]->weight = 1000; - } - - // Set defaults for module info. - $defaults = array( - 'dependencies' => array(), - 'description' => '', - 'package' => 'Other', - 'version' => NULL, - 'php' => DRUPAL_MINIMUM_PHP, - ); - - // Read info files for each module. - foreach ($modules as $key => $module) { - // Look for the info file. - $module->info = \Drupal::service('info_parser')->parse($module->getPathname()); - - // Add the info file modification time, so it becomes available for - // contributed modules to use for ordering module lists. - $module->info['mtime'] = $module->getMTime(); - - // Merge in defaults and save. - $modules[$key]->info = $module->info + $defaults; - - // Installation profiles are hidden by default, unless explicitly specified - // otherwise in the .info.yml file. - if ($key == $profile && !isset($modules[$key]->info['hidden'])) { - $modules[$key]->info['hidden'] = TRUE; - } - - // Invoke hook_system_info_alter() to give installed modules a chance to - // modify the data in the .info.yml files if necessary. - // @todo Remove $type argument, obsolete with $module->getType(). - $type = 'module'; - \Drupal::moduleHandler()->alter('system_info', $modules[$key]->info, $modules[$key], $type); - } - - // It is possible that a module was marked as required by - // hook_system_info_alter() and modules that it depends on are not required. - foreach ($modules as $module) { - _system_rebuild_module_data_ensure_required($module, $modules); - } - - - if ($profile && isset($modules[$profile])) { - // The installation profile is required, if it's a valid module. - $modules[$profile]->info['required'] = TRUE; - // Add a default distribution name if the profile did not provide one. - // @see install_profile_info() - // @see drupal_install_profile_distribution_name() - if (!isset($modules[$profile]->info['distribution']['name'])) { - $modules[$profile]->info['distribution']['name'] = 'Drupal'; - } - } - - return $modules; -} - -/** - * Ensures that dependencies of required modules are also required. - * - * @param \Drupal\Core\Extension\Extension $module - * The module info. - * @param \Drupal\Core\Extension\Extension[] $modules - * The array of all module info. - */ -function _system_rebuild_module_data_ensure_required($module, &$modules) { - if (!empty($module->info['required'])) { - foreach ($module->info['dependencies'] as $dependency) { - $dependency_name = ModuleHandler::parseDependency($dependency)['name']; - if (!isset($modules[$dependency_name]->info['required'])) { - $modules[$dependency_name]->info['required'] = TRUE; - $modules[$dependency_name]->info['explanation'] = t('Dependency of required module @module', array('@module' => $module->info['name'])); - // Ensure any dependencies it has are required. - _system_rebuild_module_data_ensure_required($modules[$dependency_name], $modules); - } - } - } + return \Drupal::service('module_listing')->reset()->listExtensions(); } /** @@ -1039,33 +929,7 @@ function _system_rebuild_module_data_ensure_required($module, &$modules) { * Array of all available modules and their data. */ function system_rebuild_module_data() { - $modules_cache = &drupal_static(__FUNCTION__); - // Only rebuild once per request. $modules and $modules_cache cannot be - // combined into one variable, because the $modules_cache variable is reset by - // reference from system_list_reset() during the rebuild. - if (!isset($modules_cache)) { - $modules = _system_rebuild_module_data(); - $files = array(); - ksort($modules); - // Add status, weight, and schema version. - $installed_modules = \Drupal::config('core.extension')->get('module') ?: array(); - foreach ($modules as $name => $module) { - $module->weight = isset($installed_modules[$name]) ? $installed_modules[$name] : 0; - $module->status = (int) isset($installed_modules[$name]); - $module->schema_version = SCHEMA_UNINSTALLED; - $files[$name] = $module->getPathname(); - } - $modules = \Drupal::moduleHandler()->buildModuleDependencies($modules); - $modules_cache = $modules; - - // Store filenames to allow drupal_get_filename() to retrieve them without - // having to rebuild or scan the filesystem. - \Drupal::state()->set('system.module.files', $files); - // Clear the module info cache. - \Drupal::cache()->delete('system.module.info'); - drupal_static_reset('system_get_info'); - } - return $modules_cache; + return \Drupal::service('module_listing')->reset()->listExtensions(); } /** diff --git a/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php index 24b4d54..5515706 100644 --- a/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php +++ b/core/modules/system/tests/src/Kernel/Extension/ModuleHandlerTest.php @@ -120,7 +120,7 @@ function testDependencyResolution() { // Color will depend on Config, which depends on a non-existing module Foo. // Nothing should be installed. \Drupal::state()->set('module_test.dependency', 'missing dependency'); - drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('module_listing')->reset(); try { $result = $this->moduleInstaller()->install(array('color')); @@ -135,7 +135,7 @@ function testDependencyResolution() { // Fix the missing dependency. // Color module depends on Config. Config depends on Help module. \Drupal::state()->set('module_test.dependency', 'dependency'); - drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('module_listing')->reset(); $result = $this->moduleInstaller()->install(array('color')); $this->assertTrue($result, 'ModuleHandler::install() returns the correct value.'); @@ -167,7 +167,7 @@ function testDependencyResolution() { // dependency on a specific version of Help module in its info file. Make // sure that Drupal\Core\Extension\ModuleHandler::install() still works. \Drupal::state()->set('module_test.dependency', 'version dependency'); - drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('module_listing')->reset(); $result = $this->moduleInstaller()->install(array('color')); $this->assertTrue($result, 'ModuleHandler::install() returns the correct value.'); @@ -197,8 +197,7 @@ function testUninstallProfileDependency() { drupal_get_filename('profile', $profile, 'core/profiles/' . $profile . '/' . $profile . '.info.yml'); $this->enableModules(array('module_test', $profile)); - drupal_static_reset('system_rebuild_module_data'); - $data = system_rebuild_module_data(); + $data = \Drupal::service('module_listing')->reset()->listExtensions(); $this->assertTrue(isset($data[$profile]->requires[$dependency])); $this->moduleInstaller()->install(array($dependency)); @@ -236,7 +235,7 @@ function testUninstallContentDependency() { // entity_test will depend on help. This way help can not be uninstalled // when there is test content preventing entity_test from being uninstalled. \Drupal::state()->set('module_test.dependency', 'dependency'); - drupal_static_reset('system_rebuild_module_data'); + \Drupal::service('module_listing')->reset(); // Create an entity so that the modules can not be disabled. $entity = EntityTest::create(array('name' => $this->randomString())); diff --git a/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php b/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php new file mode 100644 index 0000000..e644aa1 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Extension/ExtensionListTest.php @@ -0,0 +1,209 @@ +getMocks(); + $test_extension_list = new TestExtension($this->root, 'test_extension', $cache->reveal(), $info_parser->reveal(), $module_handler->reveal()); + + $extension_discovery = $this->prophesize(ExtensionDiscovery::class); + $extension_discovery->scan('test_extension')->willReturn([]); + $test_extension_list->setExtensionDiscovery($extension_discovery->reveal()); + + $test_extension_list->getName('test_name'); + } + + /** + * @covers ::getName + */ + public function testGetName() { + $test_extension_list = $this->setupTestExtensionList(); + + $this->assertEquals('test name', $test_extension_list->getName('test_name')); + } + + /** + * @covers ::getExtension + * @expectedException \InvalidArgumentException + */ + public function testGetExtensionWithNonExistingExtension() { + list($cache, $info_parser, $module_handler) = $this->getMocks(); + $test_extension_list = new TestExtension($this->root, 'test_extension', $cache->reveal(), $info_parser->reveal(), $module_handler->reveal()); + + $extension_discovery = $this->prophesize(ExtensionDiscovery::class); + $extension_discovery->scan('test_extension')->willReturn([]); + $test_extension_list->setExtensionDiscovery($extension_discovery->reveal()); + + $test_extension_list->getExtension('test_name'); + } + + /** + * @covers ::getExtension + */ + public function testGetExtension() { + $test_extension_list = $this->setupTestExtensionList(); + + $extension = $test_extension_list->getExtension('test_name'); + $this->assertInstanceOf(Extension::class, $extension); + $this->assertEquals('test_name', $extension->getName()); + } + + /** + * @covers ::listExtensions + */ + public function testListExtensions() { + $test_extension_list = $this->setupTestExtensionList(); + + $extensions = $test_extension_list->listExtensions(); + $this->assertCount(1, $extensions); + $this->assertEquals('test_name', $extensions['test_name']->getName()); + } + + /** + * @covers ::getInfo + * @covers ::getAllInfo + */ + public function testGetInfo() { + $test_extension_list = $this->setupTestExtensionList(); + + $info = $test_extension_list->getInfo('test_name'); + $this->assertEquals([ + 'type' => 'test_extension', + 'core' => '8.x', + 'name' => 'test name', + 'mtime' => 123456789, + ], $info); + } + + /** + * @covers ::getAllInfo + */ + public function testGetAllInfo() { + $test_extension_list = $this->setupTestExtensionList(); + + $infos = $test_extension_list->getAllInfo(); + $this->assertEquals(['test_name' => [ + 'type' => 'test_extension', + 'core' => '8.x', + 'name' => 'test name', + 'mtime' => 123456789, + ]], $infos); + } + + /** + * @covers ::getFilenames + */ + public function testGetFilenames() { + $test_extension_list = $this->setupTestExtensionList(); + + $filenames = $test_extension_list->getFilenames(); + $this->assertEquals([ + 'test_name' => 'vfs://drupal_root/example/test_name/test_name.info.yml', + ], $filenames); + } + + /** + * @covers ::getFilename + */ + public function testGetFilename() { + $test_extension_list = $this->setupTestExtensionList(); + + $filename = $test_extension_list->getFilename('test_name'); + $this->assertEquals('vfs://drupal_root/example/test_name/test_name.info.yml', $filename); + } + + + /** + * @covers ::setFilename + * @covers ::getFilename + */ + public function testSetFilename() { + $test_extension_list = $this->setupTestExtensionList(); + + $test_extension_list->setFilename('test_name', 'vfs://drupal_root/example2/test_name/test_name.info.yml'); + $this->assertEquals('vfs://drupal_root/example2/test_name/test_name.info.yml', $test_extension_list->getFilename('test_name')); + } + + /** + * @covers ::getPath + */ + public function testGetPath() { + $test_extension_list = $this->setupTestExtensionList(); + + $path = $test_extension_list->getPath('test_name'); + $this->assertEquals('vfs://drupal_root/example/test_name', $path); + } + + /** + * @return \Drupal\Tests\Core\Extension\TestExtension + */ + protected function setupTestExtensionList() { + vfsStream::setup('drupal_root'); + vfsStream::create([ + 'example' => [ + 'test_name' => [ + 'test_name.info.yml' => Yaml::encode([ + 'name' => 'test name', + 'type' => 'test_extension', + 'core' => '8.x', + ]), + ], + ], + ]); + touch('vfs://drupal_root/example/test_name/test_name.info.yml', 123456789); + + list($cache, $info_parser, $module_handler) = $this->getMocks(); + $info_parser->parse(Argument::any())->will(function($args) { + return Yaml::decode(file_get_contents($args[0])); + }); + + $test_extension_list = new TestExtension('vfs://drupal_root', 'test_extension', $cache->reveal(), $info_parser->reveal(), $module_handler->reveal()); + + $extension_discovery = $this->prophesize(ExtensionDiscovery::class); + $extension_discovery->scan('test_extension')->willReturn(['test_name' => new Extension($this->root, 'test_extension', 'vfs://drupal_root/example/test_name/test_name.info.yml')]); + $test_extension_list->setExtensionDiscovery($extension_discovery->reveal()); + return $test_extension_list; + } + + protected function getMocks() { + $cache = $this->prophesize(CacheBackendInterface::class); + $info_parser = $this->prophesize(InfoParserInterface::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + return [$cache, $info_parser, $module_handler]; + } + +} + +class TestExtension extends ExtensionList { + + public function setExtensionDiscovery(ExtensionDiscovery $extension_discovery) { + $this->extensionDiscovery = $extension_discovery; + } + +}