Commit b5f7cccd authored by catch's avatar catch

Issue #1081266 by stefan.r, mikeytown2, jeroen.b, tsphethean, mfb,...

Issue #1081266 by stefan.r, mikeytown2, jeroen.b, tsphethean, mfb, joseph.olstad, marcelovani: Avoid re-scanning module directory when a filename or a module is missing
parent 2b49f7bb
......@@ -249,16 +249,9 @@ function drupal_get_filename($type, $name, $filename = NULL) {
if (!isset($files[$type][$name]) && \Drupal::hasService('state')) {
$files[$type] += \Drupal::state()->get('system.' . $type . '.files', array());
}
// If still unknown, perform a filesystem scan.
// If still unknown, create a user-level error message.
if (!isset($files[$type][$name])) {
$listing = new ExtensionDiscovery(DRUPAL_ROOT);
// Prevent an infinite recursion by this legacy function.
if ($original_type == 'profile') {
$listing->setProfileDirectories(array());
}
foreach ($listing->scan($original_type) as $extension_name => $file) {
$files[$type][$extension_name] = $file->getPathname();
}
trigger_error(SafeMarkup::format('The following @type is missing from the file system: @name', array('@type' => $type, '@name' => $name)), E_USER_WARNING);
}
}
......
......@@ -302,6 +302,12 @@ 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
......@@ -1029,6 +1035,11 @@ function install_base_system(&$install_state) {
// system.module in order to work.
file_ensure_htaccess();
// Prime the drupal_get_filename() static cache with the user module's
// exact location.
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_get_filename('module', 'user', 'core/modules/user/user.info.yml');
// Enable the user module so that sessions can be recorded during the
// upcoming bootstrap step.
\Drupal::service('module_installer')->install(array('user'), FALSE);
......
......@@ -556,7 +556,7 @@ function drupal_verify_profile($install_state) {
$info = $install_state['profile_info'];
// Get the list of available modules for the selected installation profile.
$listing = new ExtensionDiscovery(DRUPAL_ROOT);
$listing = new ExtensionDiscovery(\Drupal::root());
$present_modules = array();
foreach ($listing->scan('module') as $present_module) {
$present_modules[] = $present_module->getName();
......@@ -922,11 +922,20 @@ function drupal_check_profile($profile, array $install_state) {
// Collect requirement testing results.
$requirements = array();
// Performs an ExtensionDiscovery scan as the system module is unavailable and
// we don't yet know where all the modules are located.
// @todo Remove as part of https://www.drupal.org/node/2186491
$listing = new ExtensionDiscovery(\Drupal::root());
$module_list = $listing->scan('module');
foreach ($info['dependencies'] as $module) {
module_load_install($module);
$file = \Drupal::root() . '/' . $module_list[$module]->getPath() . "/$module.install";
if (is_file($file)) {
require_once $file;
}
$function = $module . '_requirements';
drupal_classloader_register($module, drupal_get_path('module', $module));
drupal_classloader_register($module, $module_list[$module]->getPath());
if (function_exists($function)) {
$requirements = array_merge($requirements, $function('install'));
}
......
......@@ -157,17 +157,23 @@ public function installDefaultConfig($type, $name) {
* {@inheritdoc}
*/
public function installOptionalConfig(StorageInterface $storage = NULL, $dependency = []) {
$profile = $this->drupalGetProfile();
if (!$storage) {
// Search the install profile's optional configuration too.
$storage = new ExtensionInstallStorage($this->getActiveStorages(StorageInterface::DEFAULT_COLLECTION), InstallStorage::CONFIG_OPTIONAL_DIRECTORY, StorageInterface::DEFAULT_COLLECTION, TRUE);
// The extension install storage ensures that overrides are used.
$profile_storage = NULL;
}
else {
elseif (isset($profile)) {
// Creates a profile storage to search for overrides.
$profile_install_path = $this->drupalGetPath('module', $this->drupalGetProfile()) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
$profile_install_path = $this->drupalGetPath('module', $profile) . '/' . InstallStorage::CONFIG_OPTIONAL_DIRECTORY;
$profile_storage = new FileStorage($profile_install_path, StorageInterface::DEFAULT_COLLECTION);
}
else {
// Profile has not been set yet. For example during the first steps of the
// installer or during unit tests.
$profile_storage = NULL;
}
$collection_info = $this->configManager->getConfigCollectionInfo();
$enabled_extensions = $this->getEnabledExtensions();
......
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Config;
use Drupal\Core\Site\Settings;
use Drupal\Core\Extension\ExtensionDiscovery;
/**
* Storage to access configuration and schema in enabled extensions.
......@@ -80,26 +81,57 @@ public function createCollection($collection) {
protected function getAllFolders() {
if (!isset($this->folders)) {
$this->folders = array();
$this->folders += $this->getComponentNames('core', array('core'));
$this->folders += $this->getCoreNames();
$install_profile = Settings::get('install_profile');
$profile = drupal_get_profile();
$extensions = $this->configStorage->read('core.extension');
// @todo Remove this scan as part of https://www.drupal.org/node/2186491
$listing = new ExtensionDiscovery(\Drupal::root());
if (!empty($extensions['module'])) {
$modules = $extensions['module'];
// Remove the install profile as this is handled later.
unset($modules[$install_profile]);
$this->folders += $this->getComponentNames('module', array_keys($modules));
$profile_list = $listing->scan('profile');
if ($profile && isset($profile_list[$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, $profile_list[$profile]->getPathname());
}
$module_list_scan = $listing->scan('module');
$module_list = array();
foreach (array_keys($modules) as $module) {
if (isset($module_list_scan[$module])) {
$module_list[$module] = $module_list_scan[$module];
}
}
$this->folders += $this->getComponentNames($module_list);
}
if (!empty($extensions['theme'])) {
$this->folders += $this->getComponentNames('theme', array_keys($extensions['theme']));
$theme_list_scan = $listing->scan('theme');
foreach (array_keys($extensions['theme']) as $theme) {
if (isset($theme_list_scan[$theme])) {
$theme_list[$theme] = $theme_list_scan[$theme];
}
}
$this->folders += $this->getComponentNames($theme_list);
}
if ($this->includeProfile) {
// The install profile can override module default configuration. We do
// this by replacing the config file path from the module/theme with the
// install profile version if there are any duplicates.
$profile_folders = $this->getComponentNames('profile', array(drupal_get_profile()));
$this->folders = $profile_folders + $this->folders;
if (isset($profile)) {
if (!isset($profile_list)) {
$profile_list = $listing->scan('profile');
}
if (isset($profile_list[$profile])) {
$profile_folders = $this->getComponentNames(array($profile_list[$profile]));
$this->folders = $profile_folders + $this->folders;
}
}
}
}
return $this->folders;
......
......@@ -8,6 +8,7 @@
namespace Drupal\Core\Config;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\Extension\Extension;
/**
* Storage used by the Drupal installer.
......@@ -157,14 +158,20 @@ public function listAll($prefix = '') {
protected function getAllFolders() {
if (!isset($this->folders)) {
$this->folders = array();
$this->folders += $this->getComponentNames('core', array('core'));
// @todo Refactor getComponentNames() to use the extension list directly.
$this->folders += $this->getCoreNames();
// Perform an ExtensionDiscovery scan as we cannot use drupal_get_path()
// yet because the system module may not yet be enabled during install.
// @todo Remove as part of https://www.drupal.org/node/2186491
$listing = new ExtensionDiscovery(\Drupal::root());
if ($profile = drupal_get_profile()) {
$this->folders += $this->getComponentNames('profile', array($profile));
$profile_list = $listing->scan('profile');
if (isset($profile_list[$profile])) {
$this->folders += $this->getComponentNames(array($profile_list[$profile]));
}
}
$listing = new ExtensionDiscovery(DRUPAL_ROOT);
$this->folders += $this->getComponentNames('module', array_keys($listing->scan('module')));
$this->folders += $this->getComponentNames('theme', array_keys($listing->scan('theme')));
// @todo Remove as part of https://www.drupal.org/node/2186491
$this->folders += $this->getComponentNames($listing->scan('module'));
$this->folders += $this->getComponentNames($listing->scan('theme'));
}
return $this->folders;
}
......@@ -172,21 +179,21 @@ protected function getAllFolders() {
/**
* Get all configuration names and folders for a list of modules or themes.
*
* @param string $type
* Type of components: 'module' | 'theme' | 'profile'
* @param array $list
* Array of theme or module names.
* @param \Drupal\Core\Extension\Extension[] $list
* An associative array of Extension objects, keyed by extension name.
*
* @return array
* Folders indexed by configuration name.
*/
public function getComponentNames($type, array $list) {
public function getComponentNames(array $list) {
$extension = '.' . $this->getFileExtension();
$folders = array();
foreach ($list as $name) {
$directory = $this->getComponentFolder($type, $name);
foreach ($list as $extension_object) {
// We don't have to use ExtensionDiscovery here because our list of
// extensions was already obtained through an ExtensionDiscovery scan.
$directory = $this->getComponentFolder($extension_object);
if (file_exists($directory)) {
$files = new \GlobIterator(DRUPAL_ROOT . '/' . $directory . '/*' . $extension);
$files = new \GlobIterator(\Drupal::root() . '/' . $directory . '/*' . $extension);
foreach ($files as $file) {
$folders[$file->getBasename($extension)] = $directory;
}
......@@ -195,19 +202,46 @@ public function getComponentNames($type, array $list) {
return $folders;
}
/**
* Get all configuration names and folders for Drupal core.
*
* @return array
* Folders indexed by configuration name.
*/
public function getCoreNames() {
$extension = '.' . $this->getFileExtension();
$folders = array();
$directory = $this->getCoreFolder();
if (file_exists($directory)) {
$files = new \GlobIterator(\Drupal::root() . '/' . $directory . '/*' . $extension);
foreach ($files as $file) {
$folders[$file->getBasename($extension)] = $directory;
}
}
return $folders;
}
/**
* Get folder inside each component that contains the files.
*
* @param string $type
* Component type: 'module' | 'theme' | 'profile'
* @param string $name
* Component name.
* @param \Drupal\Core\Extension\Extension $extension
* The Extension object for the component.
*
* @return string
* The configuration folder name for this component.
*/
protected function getComponentFolder($type, $name) {
return drupal_get_path($type, $name) . '/' . $this->getCollectionDirectory();
protected function getComponentFolder(Extension $extension) {
return $extension->getPath() . '/' . $this->getCollectionDirectory();
}
/**
* Get folder inside Drupal core that contains the files.
*
* @return string
* The configuration folder name for core.
*/
protected function getCoreFolder() {
return drupal_get_path('core', 'core') . '/' . $this->getCollectionDirectory();
}
/**
......
......@@ -134,8 +134,9 @@ public function __construct($root) {
*/
public function scan($type, $include_tests = NULL) {
// Determine the installation profile directories to scan for extensions,
// unless explicit profile directories have been set.
if (!isset($this->profileDirectories)) {
// unless explicit profile directories have been set. Exclude profiles as we
// cannot have profiles within profiles.
if (!isset($this->profileDirectories) && $type != 'profile') {
$this->setProfileDirectoriesFromSettings();
}
......
......@@ -18,7 +18,7 @@
/**
* Default implementation of the module installer.
*
* It registers the module in config, install its own configuration,
* It registers the module in config, installs its own configuration,
* installs the schema, updates the Drupal kernel and more.
*/
class ModuleInstaller implements ModuleInstallerInterface {
......
......@@ -469,7 +469,8 @@ public function rebuildThemeData() {
);
$sub_themes = array();
$files = array();
$files_theme = array();
$files_theme_engine = array();
// Read info files for each theme.
foreach ($themes as $key => $theme) {
// @todo Remove all code that relies on the $status property.
......@@ -498,6 +499,7 @@ public function rebuildThemeData() {
if (isset($engines[$engine])) {
$theme->owner = $engines[$engine]->getExtensionPathname();
$theme->prefix = $engines[$engine]->getName();
$files_theme_engine[$engine] = $engines[$engine]->getPathname();
}
// Prefix screenshot with theme path.
......@@ -505,7 +507,7 @@ public function rebuildThemeData() {
$theme->info['screenshot'] = $theme->getPath() . '/' . $theme->info['screenshot'];
}
$files[$key] = $theme->getPathname();
$files_theme[$key] = $theme->getPathname();
}
// Build dependencies.
// @todo Move into a generic ExtensionHandler base class.
......@@ -513,8 +515,10 @@ public function rebuildThemeData() {
$themes = $this->moduleHandler->buildModuleDependencies($themes);
// Store filenames to allow system_list() and drupal_get_filename() to
// retrieve them without having to scan the filesystem.
$this->state->set('system.theme.files', $files);
// retrieve them for themes and theme engines without having to scan the
// filesystem.
$this->state->set('system.theme.files', $files_theme);
$this->state->set('system.theme_engine.files', $files_theme_engine);
// After establishing the full list of available themes, fill in data for
// sub-themes.
......
......@@ -82,7 +82,7 @@ function _ckeditor_theme_css($theme = NULL) {
if (!isset($theme)) {
$theme = \Drupal::config('system.theme')->get('default');
}
if ($theme_path = drupal_get_path('theme', $theme)) {
if (isset($theme) && $theme_path = drupal_get_path('theme', $theme)) {
$info = system_get_info('theme', $theme);
if (isset($info['ckeditor_stylesheets'])) {
$css = $info['ckeditor_stylesheets'];
......
......@@ -27,7 +27,6 @@ class CommentStringIdEntitiesTest extends KernelTestBase {
'user',
'field',
'field_ui',
'entity',
'entity_test',
'text',
);
......
......@@ -184,6 +184,10 @@ public function testConfigEntityUninstall() {
)
);
$entity2->save();
// Perform a module rebuild so we can know where the node module is located
// and uninstall it.
// @todo Remove as part of https://www.drupal.org/node/2186491
system_rebuild_module_data();
// Test that doing a config uninstall of the node module deletes entity2
// since it is dependent on entity1 which is dependent on the node module.
$config_manager->uninstall('module', 'node');
......
......@@ -23,13 +23,12 @@ class TestInstallStorage extends InstallStorage {
*/
protected function getAllFolders() {
if (!isset($this->folders)) {
$this->folders = $this->getComponentNames('core', array('core'));
// @todo Refactor getComponentNames() to use the extension list directly.
$this->folders = $this->getCoreNames();
$listing = new ExtensionDiscovery(\Drupal::root());
$listing->setProfileDirectories(array());
$this->folders += $this->getComponentNames('profile', array_keys($listing->scan('profile')));
$this->folders += $this->getComponentNames('module', array_keys($listing->scan('module')));
$this->folders += $this->getComponentNames('theme', array_keys($listing->scan('theme')));
$this->folders += $this->getComponentNames($listing->scan('profile'));
$this->folders += $this->getComponentNames($listing->scan('module'));
$this->folders += $this->getComponentNames($listing->scan('theme'));
}
return $this->folders;
}
......
......@@ -77,7 +77,7 @@ function quickedit_library_info_alter(&$libraries, $extension) {
// First let the base theme modify the library, then the actual theme.
$alter_library = function(&$library, $theme) use (&$alter_library) {
if ($theme_path = drupal_get_path('theme', $theme)) {
if (isset($theme) && $theme_path = drupal_get_path('theme', $theme)) {
$info = system_get_info('theme', $theme);
// Recurse to process base theme(s) first.
if (isset($info['base theme'])) {
......
......@@ -13,6 +13,7 @@
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
use Drupal\Core\Language\Language;
use Drupal\Core\Site\Settings;
......@@ -483,6 +484,14 @@ protected function installEntitySchema($entity_type_id) {
* The new modules are only added to the active module list and loaded.
*/
protected function enableModules(array $modules) {
// Perform an ExtensionDiscovery scan as this function may receive a
// profile that is not the current profile, and we don't yet have a cached
// way to receive inactive profile information.
// @todo Remove as part of https://www.drupal.org/node/2186491
$listing = new ExtensionDiscovery(\Drupal::root());
$module_list = $listing->scan('module');
// In ModuleHandlerTest we pass in a profile as if it were a module.
$module_list += $listing->scan('profile');
// Set the list of modules in the extension handler.
$module_handler = $this->container->get('module_handler');
......@@ -492,7 +501,7 @@ protected function enableModules(array $modules) {
$extensions = $active_storage->read('core.extension');
foreach ($modules as $module) {
$module_handler->addModule($module, drupal_get_path('module', $module));
$module_handler->addModule($module, $module_list[$module]->getPath());
// Maintain the list of enabled modules in configuration.
$extensions['module'][$module] = 0;
}
......
......@@ -16,34 +16,6 @@
*/
class GetFilenameUnitTest extends KernelTestBase {
/**
* The container used by the test, moved out of the way.
*
* @var \Symfony\Component\DependencyInjection\ContainerInterface
*/
protected $previousContainer;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Store the previous container.
$this->previousContainer = $this->container;
$this->container = NULL;
\Drupal::unsetContainer();
}
/**
* {@inheritdoc}
*/
protected function tearDown() {
parent::tearDown();
// Restore the previous container.
$this->container = $this->previousContainer;
\Drupal::setContainer($this->previousContainer);
}
/**
* Tests that drupal_get_filename() works when the file is not in database.
*/
......@@ -53,24 +25,41 @@ function testDrupalGetFilename() {
global $install_state;
$install_state['parameters']['profile'] = 'testing';
// Assert that this test is meaningful.
$this->assertNull($this->container);
$this->assertFalse(\Drupal::hasContainer());
// 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.
$this->assertIdentical(drupal_get_filename('module', 'system'), 'core/modules/system/system.info.yml');
// Retrieving the location of a theme.
\Drupal::service('theme_handler')->install(array('stark'));
$this->assertIdentical(drupal_get_filename('theme', 'stark'), 'core/themes/stark/stark.info.yml');
// Retrieving the location of a theme engine.
$this->assertIdentical(drupal_get_filename('theme_engine', 'phptemplate'), 'core/themes/engines/phptemplate/phptemplate.info.yml');
$this->assertIdentical(drupal_get_filename('theme_engine', 'twig'), 'core/themes/engines/twig/twig.info.yml');
// Retrieving the location of a profile. Profiles are a special case with
// a fixed location and naming.
$this->assertIdentical(drupal_get_filename('profile', 'standard'), 'core/profiles/standard/standard.info.yml');
$this->assertIdentical(drupal_get_filename('profile', 'testing'), 'core/profiles/testing/testing.info.yml');
// Generate a non-existing module name.
$non_existing_module = uniqid("", TRUE);
// Searching for an item that does not exist returns NULL.
$this->assertNull(drupal_get_filename('module', uniqid("", TRUE)), 'Searching for an item that does not exist returns NULL.');
// Set a custom error handler so we can ignore the file not found error.
set_error_handler(function($severity, $message, $file, $line) {
// Skip error handling if this is a "file not found" error.
if (strstr($message, 'is missing from the file system:')) {
\Drupal::state()->set('get_filename_test_triggered_error', TRUE);
return;
}
throw new \ErrorException($message, 0, $severity, $file, $line);
});
$this->assertNull(drupal_get_filename('module', $non_existing_module), 'Searching for an item that does not exist returns NULL.');
$this->assertTrue(\Drupal::state()->get('get_filename_test_triggered_error'), 'Searching for an item that does not exist triggers an error.');
// Restore the original error handler.
restore_error_handler();
}
}
......@@ -74,7 +74,7 @@ function testDefault() {
* Tests non-existing libraries.
*/
function testLibraryUnknown() {
$build['#attached']['library'][] = 'unknown/unknown';
$build['#attached']['library'][] = 'core/unknown';
$assets = AttachedAssets::createFromRenderArray($build);
$this->assertIdentical([], $this->assetResolver->getJsAssets($assets, FALSE)[0], 'Unknown library was not added to the page.');
......
......@@ -23,13 +23,6 @@ class EntitySchemaTest extends EntityUnitTestBase {
*/
protected $database;
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('menu_link');
/**
* {@inheritdoc}
*/
......
......@@ -23,6 +23,14 @@ class ModuleHandlerTest extends KernelTestBase {
*/
public static $modules = array('system');
public function setUp() {
parent::setUp();
// Set up the state values so we know where to find the files when running
// drupal_get_filename().
// @todo Remove as part of https://www.drupal.org/node/2186491
system_rebuild_module_data();
}
/**
* {@inheritdoc}
*/
......@@ -37,6 +45,11 @@ public function containerBuild(ContainerBuilder $container) {
* The basic functionality of retrieving enabled modules.
*/
function testModuleList() {
// Prime the drupal_get_filename() static cache with the location of the
// testing profile as it is not the currently active profile and we don't
// yet have any cached way to retrieve its location.
// @todo Remove as part of https://www.drupal.org/node/2186491
drupal_get_filename('profile', 'testing', 'core/profiles/testing/testing.info.yml');
// Build a list of modules, sorted alphabetically.
$profile_info = install_profile_info('testing', 'en');
$module_list = $profile_info['dependencies'];
......@@ -184,6 +197,11 @@ function testUninstallProfileDependency() {
$profile = 'minimal';
$dependency = 'dblog';
$this->settingsSet('install_profile', $profile);
// Prime the drupal_get_filename() static cache with the location of the
// minimal profile as it is not the currently active profile and we don't