diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index be8c695..e6b7cc3 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -10,6 +10,7 @@ use Drupal\Core\DrupalKernel; use Drupal\Core\Database\Database; use Drupal\Core\DependencyInjection\ContainerBuilder; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Utility\Title; use Drupal\Core\Utility\Error; use Symfony\Component\ClassLoader\ApcClassLoader; @@ -700,10 +701,7 @@ function drupal_get_filename($type, $name, $filename = NULL) { if (!empty($filename)) { $files[$type][$name] = $filename; } - elseif (isset($files[$type][$name])) { - // nothing - } - else { + elseif (!isset($files[$type][$name])) { // Verify that we have an keyvalue service before using it. This is required // because this function is called during installation. // @todo Inject database connection into KeyValueStore\DatabaseStorage. @@ -752,14 +750,12 @@ function drupal_get_filename($type, $name, $filename = NULL) { if (!isset($dirs[$dir][$extension])) { $dirs[$dir][$extension] = TRUE; - if (!function_exists('drupal_system_listing')) { - require_once __DIR__ . '/common.inc'; - } // Scan the appropriate directories for all files with the requested // extension, not just the file we are currently looking for. This // prevents unnecessary scans from being repeated when this function is // called more than once in the same page request. - $matches = drupal_system_listing("/^" . DRUPAL_PHP_FUNCTION_PATTERN . "\.$extension$/", $dir); + $listing = new ExtensionDiscovery(); + $matches = $listing->scan($dir); foreach ($matches as $matched_name => $file) { $files[$type][$matched_name] = $file->uri; } diff --git a/core/includes/common.inc b/core/includes/common.inc index b97cc5e..f9cea6e 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -17,7 +17,6 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Datetime\DrupalDateTime; use Drupal\Core\Routing\GeneratorNotInitializedException; -use Drupal\Core\SystemListingInfo; use Drupal\Core\Template\Attribute; use Drupal\Core\Render\Element; @@ -3177,19 +3176,6 @@ function drupal_page_set_cache(Response $response, Request $request) { } /** - * This function is kept only for backward compatibility. - * - * @see \Drupal\Core\SystemListing::scan(). - */ -function drupal_system_listing($mask, $directory, $key = 'name', $min_depth = 1) { - // As SystemListing is required to build a dependency injection container - // from scratch and SystemListingInfo only extends SystemLising, this - // class needs to be hardwired. - $listing = new SystemListingInfo(); - return $listing->scan($mask, $directory, $key, $min_depth); -} - -/** * Sets the main page content value for later use. * * Given the nature of the Drupal page handling, this will be called once with diff --git a/core/includes/install.core.inc b/core/includes/install.core.inc index 0a57f16..fcc01f8 100644 --- a/core/includes/install.core.inc +++ b/core/includes/install.core.inc @@ -13,7 +13,7 @@ use Drupal\Core\Language\Language; use Drupal\Core\Language\LanguageManager; use Drupal\Core\StringTranslation\Translator\FileTranslation; - +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -494,7 +494,20 @@ function install_begin_request(&$install_state) { // Override the module list with a minimal set of modules. $module_handler->setModuleList(array('system' => 'core/modules/system/system.module')); } - $module_handler->load('system'); + // After setting up a custom and finite module list in a custom low-level + // bootstrap like here, ensure to use ModuleHandler::loadAll() so that + // ModuleHandler::isLoaded() returns TRUE, since that is a condition being + // checked by other subsystems (e.g., the theme system). + $module_handler->loadAll(); + + // Add list of all available profiles to the installation state. + $listing = new ExtensionDiscovery(); + $install_state['profiles'] += $listing->scan('profiles'); + + // Prime drupal_get_filename()'s static cache. + foreach ($install_state['profiles'] as $name => $profile) { + drupal_get_filename('profile', $name, $profile->uri); + } // Prepare for themed output. We need to run this at the beginning of the // page request to avoid a different theme accidentally getting set. (We also @@ -528,9 +541,6 @@ function install_begin_request(&$install_state) { // Modify the installation state as appropriate. $install_state['completed_task'] = $task; - - // Add the list of available profiles to the installation state. - $install_state['profiles'] += drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles'); } /** diff --git a/core/includes/install.inc b/core/includes/install.inc index f3fbdf0..ef86da9 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -9,6 +9,7 @@ use Drupal\Component\Utility\Crypt; use Drupal\Core\Database\Database; use Drupal\Core\DrupalKernel; +use Drupal\Core\Extension\ExtensionDiscovery; /** * Requirement severity -- Informational message only. @@ -122,22 +123,17 @@ function drupal_detect_database_types() { } /** - * Returns all supported database installer objects that are compiled into PHP. + * Returns all supported database driver installer objects. * - * @return - * An array of database installer objects compiled into PHP. + * @return \Drupal\Core\Database\Install\Tasks[] + * An array of available database driver installer objects. */ function drupal_get_database_types() { $databases = array(); $drivers = array(); - // We define a driver as a directory in /core/includes/database that in turn - // contains a database.inc file. That allows us to drop in additional drivers - // without modifying the installer. - require_once __DIR__ . '/database.inc'; - // Allow any valid PHP identifier. - // @see http://www.php.net/manual/en/language.variables.basics.php. - $mask = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/'; + // The internal database driver name is any valid PHP identifier. + $mask = '/^' . DRUPAL_PHP_FUNCTION_PATTERN . '$/'; $files = file_scan_directory(DRUPAL_ROOT . '/core/lib/Drupal/Core/Database/Driver', $mask, array('recurse' => FALSE)); if (is_dir(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database')) { $files += file_scan_directory(DRUPAL_ROOT . '/drivers/lib/Drupal/Driver/Database/', $mask, array('recurse' => FALSE)); @@ -569,15 +565,16 @@ function drupal_verify_profile($install_state) { } $info = $install_state['profile_info']; - // Get a list of modules that exist in Drupal's assorted subdirectories. + // Get the list of available modules for the selected installation profile. + $listing = new ExtensionDiscovery(); $present_modules = array(); - foreach (drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules') as $present_module) { + foreach ($listing->scan('modules') as $present_module) { $present_modules[] = $present_module->name; } // The installation profile is also a module, which needs to be installed // after all the other dependencies have been installed. - $present_modules[] = drupal_get_profile(); + $present_modules[] = $profile; // Verify that all of the profile's required modules are present. $missing_modules = array_diff($info['dependencies'], $present_modules); @@ -960,9 +957,7 @@ function drupal_requirements_url($severity) { function drupal_check_profile($profile, array $install_state) { include_once __DIR__ . '/file.inc'; - $profile_file = $install_state['profiles'][$profile]->uri; - - if (!isset($profile) || !file_exists($profile_file)) { + if (!isset($profile) || !isset($install_state['profiles'][$profile])) { throw new Exception(install_no_profile_error()); } diff --git a/core/includes/module.inc b/core/includes/module.inc index 429f5db..96bfbf0 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -5,6 +5,7 @@ * API for loading and interacting with Drupal modules. */ +use Drupal\Core\Extension\ExtensionDiscovery; /** * Builds a list of bootstrap modules and enabled modules and themes. @@ -285,14 +286,19 @@ function module_uninstall($module_list = array(), $uninstall_dependents = TRUE) * Returns an array of modules required by core. */ function drupal_required_modules() { - $files = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'modules'); + $listing = new ExtensionDiscovery(); + $files = $listing->scan('modules'); $required = array(); - // An installation profile is required and one must always be loaded. - $required[] = drupal_get_profile(); + // Unless called by the installer, an installation profile is required and + // must always be loaded. drupal_get_profile() also returns the installation + // profile in the installer, but only after it has been selected. + if ($profile = drupal_get_profile()) { + $required[] = $profile; + } foreach ($files as $name => $file) { - $info = \Drupal::service('info_parser')->parse($file->uri); + $info = \Drupal::service('info_parser')->parse($file->infoSubPathname); if (!empty($info) && !empty($info['required']) && $info['required']) { $required[] = $name; } diff --git a/core/lib/Drupal/Core/Config/InstallStorage.php b/core/lib/Drupal/Core/Config/InstallStorage.php index 16bcc1c..5d05790 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -7,6 +7,8 @@ namespace Drupal\Core\Config; +use Drupal\Core\Extension\ExtensionDiscovery; + /** * Storage controller used by the Drupal installer. * @@ -110,9 +112,13 @@ public function listAll($prefix = '') { */ protected function getAllFolders() { if (!isset($this->folders)) { - $this->folders = $this->getComponentNames('profile', array(drupal_get_profile())); - $this->folders += $this->getComponentNames('module', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0))); - $this->folders += $this->getComponentNames('theme', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes'))); + $this->folders = array(); + if ($profile = drupal_get_profile()) { + $this->folders += $this->getComponentNames('profile', array($profile)); + } + $listing = new ExtensionDiscovery(); + $this->folders += $this->getComponentNames('module', array_keys($listing->scan('modules'))); + $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('themes'))); } return $this->folders; } diff --git a/core/lib/Drupal/Core/DrupalKernel.php b/core/lib/Drupal/Core/DrupalKernel.php index 46a7fee..7fa4a36 100644 --- a/core/lib/Drupal/Core/DrupalKernel.php +++ b/core/lib/Drupal/Core/DrupalKernel.php @@ -13,6 +13,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\DependencyInjection\ServiceProviderInterface; use Drupal\Core\DependencyInjection\YamlFileLoader; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Language\Language; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; @@ -83,7 +84,7 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface { * An array of module data objects. * * The data objects have the same data structure as returned by - * file_scan_directory() but only the uri property is used. + * ExtensionDiscovery but only the uri property is used. * * @var array */ @@ -297,19 +298,27 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ protected function moduleData($module) { if (!$this->moduleData) { // First, find profiles. - $profiles_scanner = new SystemListing(); - $all_profiles = $profiles_scanner->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles'); - $profiles = array_keys(array_intersect_key($this->moduleList, $all_profiles)); + $listing = new ExtensionDiscovery(); + $all_profiles = $listing->scan('profiles'); + $profiles = array_intersect_key($all_profiles, $this->moduleList); + // If a module is within a profile directory but specifies another // profile for testing, it needs to be found in the parent profile. - if (($parent_profile_config = $this->configStorage->read('simpletest.settings')) && isset($parent_profile_config['parent_profile']) && $parent_profile_config['parent_profile'] != $profiles[0]) { + $settings = $this->configStorage->read('simpletest.settings'); + $parent_profile = !empty($settings['parent_profile']) ? $settings['parent_profile'] : FALSE; + if ($parent_profile && !isset($profiles[$parent_profile])) { // In case both profile directories contain the same extension, the // actual profile always has precedence. - array_unshift($profiles, $parent_profile_config['parent_profile']); + $profiles = array($parent_profile => $all_profiles[$parent_profile]) + $profiles; } + + $profile_directories = array_map(function ($profile) { + return dirname($profile->uri); + }, $profiles); + $listing->setProfileDirectories($profile_directories); + // Now find modules. - $modules_scanner = new SystemListing($profiles); - $this->moduleData = $all_profiles + $modules_scanner->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules'); + $this->moduleData = $profiles + $listing->scan('modules'); } return isset($this->moduleData[$module]) ? $this->moduleData[$module] : FALSE; } diff --git a/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php new file mode 100644 index 0000000..e8d757d --- /dev/null +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php @@ -0,0 +1,77 @@ +acceptTests = $flag; + } + if (!$this->acceptTests) { + $this->skipDirs[] = 'tests'; + } + } + + public function getChildren() { + $filter = parent::getChildren(); + $filter->acceptTests($this->acceptTests); + return $filter; + } + + public function accept() { + $name = $this->current()->getFilename(); + // The FilesystemIterator::SKIP_DOTS flag only skips '.' and '..', but not + // hidden directories (like '.git'). + if ($name[0] == '.') { + return FALSE; + } + if ($this->isDir()) { + // 'config' directories are special-cased here, because every extension + // contains one. However, those default configuration directories cannot + // contain extensions. The directory name cannot be globally skipped, + // because core happens to have a directory of an actual module that is + // named 'config'. By explicitly testing for that case, we can skip all + // other config directories, and at the same time, still allow the core + // config module to be overridden/replaced in a profile/site directory + // (whereas it must be located directly in a modules directory). + if ($name == 'config') { + return substr($this->current()->getPathname(), -14) == 'modules/config'; + } + return !in_array($name, $this->skipDirs, TRUE); + } + else { + return substr($name, -9) == '.info.yml'; + } + } + +} diff --git a/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php new file mode 100644 index 0000000..32f4fe9 --- /dev/null +++ b/core/lib/Drupal/Core/Extension/ExtensionDiscovery.php @@ -0,0 +1,311 @@ +scan('modules'); + * @endcode + * + * The following directories will be searched (in the order stated): + * - the core modules directory; i.e., /core/modules + * - the profiles directories; e.g., /core/profiles/standard + * - the site-wide modules directory; i.e., /modules + * - the all-sites directory; i.e., /sites/all/modules + * - the site-specific directory; i.e., /sites/example.com/modules + * + * The information is returned in an associative array, keyed by the extension + * name (without .info.yml extension). Extensions found later in the search + * will take precedence over extensions found earlier - unless they are not + * compatible with the current version of Drupal core. + * + * @param string $directory + * The subdirectory name in which to search for extensions. For example, + * 'modules' will recursively search all 'modules' directories for + * extensions of type 'module'. + * @param bool $include_tests + * (optional) Whether to include test extensions. Defaults to FALSE; i.e., + * all 'tests' directories are excluded in the search. + * + * @return RecursiveDirectoryIterator[] + * An associative array of RecursiveDirectoryIterator (SplFileInfo) objects, + * keyed by extension name. + */ + public function scan($directory, $include_tests = FALSE) { + // @todo Fix callers to pass $type. + // @see drupal_get_filename() + if ($directory == 'themes/engines') { + $type = 'theme_engine'; + } + else { + $type = substr($directory, 0, -1); + } + + // Installation profile directories can only be scanned for extensions when + // not scanning for installation profiles themselves. + // @todo Obsolete? + $profileDirectories = array(); + if ($directory != 'profiles') { + // Determine the installation profile directories to scan for extensions, + // unless explicit profile directories have been preset. + if (!isset($this->profileDirectories)) { + $profileDirectories = $this->getProfileDirectories(); + } + else { + $profileDirectories = $this->profileDirectories; + } + } + + $searchdirs = array(); + $searchdirs[] = 'core'; + // Search installation profile directories. + foreach ($profileDirectories as $profile) { + $searchdirs[] = $profile; + } + // Search for contributed and custom extensions in top-level directories + // (and the sites/all directory for legacy/BC reasons) as well as the + // site-specific directory. + $searchdirs[] = $directory; + $searchdirs[] = 'sites/all'; + $searchdirs[] = conf_path(); + + // Unless explicitly requested by Simpletest, manually check whether we are + // are in a test environment, in which case test extensions must be included. + if (!$include_tests) { + // @todo Replace with drupal_valid_test_ua() once avaiable. + // @see https://drupal.org/node/2171683 + $include_tests = !empty($GLOBALS['drupal_test_info']['test_run_id']); + } + + $files = array(); + foreach ($searchdirs as $dir) { + if (!isset(static::$files[$dir][$include_tests])) { + static::$files[$dir][$include_tests] = $this->scanDirectory($dir, $include_tests); + } + if (isset(static::$files[$dir][$include_tests][$type])) { + $files = array_merge($files, $this->process($files, static::$files[$dir][$include_tests][$type])); + } + } + return $files; + } + + /** + * Returns additional installation profile directories to be scanned. + * + * @return array + * A list of installation profile directory paths relative to system root. + */ + public function getProfileDirectories() { + $searchdir = array(); + $profile = drupal_get_profile(); + // For SimpleTest to be able to test modules packaged together with a + // distribution we need to include the profile of the parent site (in + // which test runs are triggered). + if (drupal_valid_test_ua() && !drupal_installation_attempted()) { + $testing_profile = \Drupal::config('simpletest.settings')->get('parent_profile'); + if ($testing_profile && $testing_profile != $profile) { + $searchdir[] = drupal_get_path('profile', $testing_profile); + } + } + // In case both profile directories contain the same extension, the actual + // profile always has precedence. + if ($profile) { + $searchdir[] = drupal_get_path('profile', $profile); + } + return $searchdir; + } + + /** + * Sets explicit profile directories to scan. + * + * @param array $profileDirectories + * A list of installation profile directories to search for extensions. + * + * @return $this + */ + public function setProfileDirectories(array $paths) { + $this->profileDirectories = $paths; + return $this; + } + + /** + * Process the files to add before adding them. + * + * @param array $files + * Every file found so far. + * @param array $files_to_add + * The files found in a single directory. + * + * @return array + * The processed list of file objects. Extensions discovered in later search + * paths and which are not not compatible with the current core version are + * skipped. + */ + protected function process(array $files, array $files_to_add) { + // Duplicate files found in later search directories take precedence over + // earlier ones, so we want them to overwrite keys in our resulting + // $files array. + // The exception to this is if the later file is from a module or theme not + // compatible with Drupal core. This may occur during upgrades of Drupal + // core when new modules exist in core while older contrib modules with the + // same name exist in a directory such as /modules. + foreach (array_intersect_key($files_to_add, $files) as $file_key => $file) { + // Parse the extension info. + $info = $this->getInfoParser()->parse($file->infoSubPathname); + + // If the module or theme is incompatible with Drupal core, remove it + // from the array for the current search directory, so it is not + // overwritten when merged with the $files array. + if (isset($info['core']) && $info['core'] != \Drupal::CORE_COMPATIBILITY) { + unset($files_to_add[$file_key]); + } + } + return $files_to_add; + } + + /** + * Recursively scans a base directory for the requested extension type. + * + * @param string $dir + * The base directory to scan, without trailing slash. + * @param bool $include_tests + * (optional) Whether to include test extensions. Defaults to FALSE; i.e., + * all 'tests' directories are excluded in the search. + * + * @return array + * An associative array whose keys are extension types and whose values are + * associative arrays of RecursiveDirectoryIterator (SplFileInfo) objects, + * keyed by extension name. + */ + protected function scanDirectory($dir, $include_tests = FALSE) { + $files = array(); + if (!is_dir($dir)) { + return $files; + } + $flags = \FilesystemIterator::UNIX_PATHS; + $flags |= \FilesystemIterator::SKIP_DOTS; + $flags |= \FilesystemIterator::CURRENT_AS_SELF; + $flags |= \FilesystemIterator::KEY_AS_FILENAME; + $directory_iterator = new \RecursiveDirectoryIterator($dir, $flags); + + $filter = new RecursiveExtensionFilterIterator($directory_iterator); + $filter->acceptTests($include_tests); + + $iterator = new \RecursiveIteratorIterator($filter, \RecursiveIteratorIterator::LEAVES_ONLY, \RecursiveIteratorIterator::CATCH_GET_CHILD); + + foreach ($iterator as $fileinfo) { + // All extension names in Drupal have to be valid PHP function names due + // to the module hook architecture. + if (!preg_match(static::PHP_FUNCTION_PATTERN, $fileinfo->getBasename('.info.yml'))) { + continue; + } + // Determine extension type from info file. + // @todo Direct parsing for performance. Though it might be likely that + // InfoParser will be invoked anyway on all files; needs investigation. + // One typical exception would be Simpletest, which just scans for + // extensions, doesn't care for info. + $file = $fileinfo->openFile('r'); + while (!isset($fileinfo->type) && !$file->eof()) { + preg_match('@type: ?(.+)@', $file->fgets(), $matches); + if (isset($matches[1])) { + $fileinfo->type = $matches[1]; + } + } + if (empty($fileinfo->type)) { + continue; + } + // Supply public properties used throughout Drupal. + // @todo Consider to update all code to use the built-in + // RecursiveDirectoryIterator (SplFileInfo) methods. + $fileinfo->name = $fileinfo->getBasename('.info.yml'); + + $fileinfo->infoFilename = $fileinfo->getFilename(); + // @todo The name infoSubPathname stems from the original implementation + // idea of scanning the *entire* Drupal filesystem once. Now that + // individual directories are scanned again (to apply the override logic), + // getSubPathname() only returns the path name relative to the directory + // being scanned. That's neither useful nor appropriate naming. Thus, + // either rename to 'infoUri' or get rid of the 'uri' override below. + $fileinfo->infoSubPathname = $dir . '/' . $fileinfo->getSubPathname(); + + // For themes, the filename and URI is the info file itself. + if ($fileinfo->type == 'theme') { + $fileinfo->filename = $fileinfo->getFilename(); + $fileinfo->uri = $dir . '/' . $fileinfo->getSubPathname(); + } + // For theme engines, the suffix is .engine. + elseif ($fileinfo->type == 'theme_engine') { + $fileinfo->filename = $fileinfo->name . '.engine'; + $fileinfo->uri = $dir . '/' . $fileinfo->getSubPath() . '/' . $fileinfo->filename; + } + // For all other types, it is $name.$type (e.g., node.module). + else { + $fileinfo->filename = $fileinfo->name . '.' . $fileinfo->type; + $fileinfo->uri = $dir . '/' . $fileinfo->getSubPath() . '/' . $fileinfo->filename; + } + + $files[$fileinfo->type][$fileinfo->name] = $fileinfo; + } + return $files; + } + + /** + * Returns a parser for parsing .info.yml files. + * + * @return \Drupal\Core\Extension\InfoParser + * The InfoParser instance. + */ + protected function getInfoParser() { + if (!isset($this->infoParser)) { + $this->infoParser = new InfoParser(); + } + return $this->infoParser; + } + +} diff --git a/core/lib/Drupal/Core/Extension/InfoParser.php b/core/lib/Drupal/Core/Extension/InfoParser.php index 5bb4841..593b34d 100644 --- a/core/lib/Drupal/Core/Extension/InfoParser.php +++ b/core/lib/Drupal/Core/Extension/InfoParser.php @@ -22,7 +22,7 @@ class InfoParser implements InfoParserInterface { * * @var array */ - protected $parsedInfos = array(); + protected static $parsedInfos = array(); /** * Symfony YAML parser object. @@ -35,29 +35,29 @@ class InfoParser implements InfoParserInterface { * {@inheritdoc} */ public function parse($filename) { - if (!isset($this->parsedInfos[$filename])) { + if (!isset(static::$parsedInfos[$filename])) { if (!file_exists($filename)) { - $this->parsedInfos[$filename] = array(); + static::$parsedInfos[$filename] = array(); } else { try { - $this->parsedInfos[$filename] = $this->getParser()->parse(file_get_contents($filename)); + static::$parsedInfos[$filename] = $this->getParser()->parse(file_get_contents($filename)); } catch (ParseException $e) { $message = String::format("Unable to parse !file. Parser error !error.", array('!file' => $filename, '!error' => $e->getMessage())); throw new InfoParserException($message, $filename); } - $missing_keys = array_diff($this->getRequiredKeys(), array_keys($this->parsedInfos[$filename])); + $missing_keys = array_diff($this->getRequiredKeys(), array_keys(static::$parsedInfos[$filename])); if (!empty($missing_keys)) { $message = format_plural(count($missing_keys), 'Missing required key (!missing_keys) in !file.', 'Missing required keys (!missing_keys) in !file.', array('!missing_keys' => implode(', ', $missing_keys), '!file' => $filename)); throw new InfoParserException($message, $filename); } - if (isset($this->parsedInfos[$filename]['version']) && $this->parsedInfos[$filename]['version'] === 'VERSION') { - $this->parsedInfos[$filename]['version'] = \Drupal::VERSION; + if (isset(static::$parsedInfos[$filename]['version']) && static::$parsedInfos[$filename]['version'] === 'VERSION') { + static::$parsedInfos[$filename]['version'] = \Drupal::VERSION; } } } - return $this->parsedInfos[$filename]; + return static::$parsedInfos[$filename]; } /** @@ -80,7 +80,7 @@ protected function getParser() { * An array of required keys. */ protected function getRequiredKeys() { - return array('name', 'type'); + return array('type', 'core', 'name'); } } diff --git a/core/lib/Drupal/Core/Extension/ModuleHandler.php b/core/lib/Drupal/Core/Extension/ModuleHandler.php index ef7b5e7..a2ad92f 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php @@ -599,10 +599,6 @@ public function install(array $module_list, $enable_dependencies = TRUE) { $this->load($module); module_load_install($module); - // Flush theme info caches, since (testing) modules can implement - // hook_system_theme_info() to register additional themes. - system_list_reset(); - // Update the kernel to include it. // This reboots the kernel to register the module's bundle and its // services in the service container. The $module_filenames argument is diff --git a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php index a74f9cd..024eeb6 100644 --- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php +++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php @@ -71,7 +71,7 @@ public function setModuleList(array $module_list = array()); * * @param array $modules * An array of module objects keyed by module name. Each object contains - * information discovered during a Drupal\Core\SystemListing scan. + * information discovered during a Drupal\Core\Extension\ExtensionDiscovery scan. * * @return * The same array with the new keys for each module: @@ -80,7 +80,7 @@ public function setModuleList(array $module_list = array()); * - required_by: An array with the keys being the modules that will not work * without this module. * - * @see \Drupal\Core\SystemListing + * @see \Drupal\Core\Extension\ExtensionDiscovery */ public function buildModuleDependencies(array $modules); diff --git a/core/lib/Drupal/Core/Extension/ThemeHandler.php b/core/lib/Drupal/Core/Extension/ThemeHandler.php index 7c71eba..bbae941 100644 --- a/core/lib/Drupal/Core/Extension/ThemeHandler.php +++ b/core/lib/Drupal/Core/Extension/ThemeHandler.php @@ -13,7 +13,7 @@ use Drupal\Core\Config\ConfigFactory; use Drupal\Core\Config\ConfigInstallerInterface; use Drupal\Core\Routing\RouteBuilder; -use Drupal\Core\SystemListingInfo; +use Drupal\Core\Extension\ExtensionDiscovery; /** * Default theme handler using the config system for enabled/disabled themes. @@ -87,11 +87,11 @@ class ThemeHandler implements ThemeHandlerInterface { protected $routeBuilder; /** - * The system listing info + * A system listing instance. * - * @var \Drupal\Core\SystemListingInfo + * @var \Drupal\Core\Extension\ExtensionDiscovery */ - protected $systemListingInfo; + protected $systemListing; /** * Constructs a new ThemeHandler. @@ -110,17 +110,17 @@ class ThemeHandler implements ThemeHandlerInterface { * database. * @param \Drupal\Core\Routing\RouteBuilder $route_builder * (optional) The route builder to rebuild the routes if a theme is enabled. - * @param \Drupal\Core\SystemListingInfo $system_list_info - * (optional) The system listing info. + * @param \Drupal\Core\Extension\ExtensionDiscovery $system_list + * (optional) A system listing instance. */ - public function __construct(ConfigFactory $config_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, InfoParserInterface $info_parser, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, SystemListingInfo $system_list_info = NULL) { + public function __construct(ConfigFactory $config_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, InfoParserInterface $info_parser, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, ExtensionDiscovery $system_list = NULL) { $this->configFactory = $config_factory; $this->moduleHandler = $module_handler; $this->cacheBackend = $cache_backend; $this->infoParser = $info_parser; $this->configInstaller = $config_installer; $this->routeBuilder = $route_builder; - $this->systemListingInfo = $system_list_info; + $this->systemListing = $system_list; } /** @@ -243,24 +243,9 @@ public function reset() { * {@inheritdoc} */ public function rebuildThemeData() { - // Find themes. - $listing = $this->getSystemListingInfo(); - $themes = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes', 'name', 1); - // Allow modules to add further themes. - if ($module_themes = $this->moduleHandler->invokeAll('system_theme_info')) { - foreach ($module_themes as $name => $uri) { - // @see file_scan_directory() - $themes[$name] = (object) array( - 'uri' => $uri, - 'filename' => pathinfo($uri, PATHINFO_FILENAME), - 'name' => $name, - ); - } - } - - // Find theme engines. - $listing = $this->getSystemListingInfo(); - $engines = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.engine$/', 'themes/engines', 'name', 1); + $listing = $this->getSystemListing(); + $themes = $listing->scan('themes'); + $engines = $listing->scan('themes/engines'); // Set defaults for theme info. $defaults = array( @@ -286,44 +271,44 @@ public function rebuildThemeData() { $sub_themes = array(); // Read info files for each theme. - foreach ($themes as $key => $theme) { - $themes[$key]->filename = $theme->uri; - $themes[$key]->info = $this->infoParser->parse($theme->uri) + $defaults; - - // Skip this extension if its type is not theme. - if (!isset($themes[$key]->info['type']) || $themes[$key]->info['type'] != 'theme') { - unset($themes[$key]); - continue; - } - + foreach ($themes as $key => $fileinfo) { + // The theme data is serialized into State. But RecursiveDirectoryIterator + // objects are not serializable, so replace the item with a stdClass. + $themes[$key] = $theme = (object) array( + 'uri' => $fileinfo->uri, + // @todo Filename should hold filename, not URI. + 'filename' => $fileinfo->uri, + 'name' => $fileinfo->name, + 'info' => $this->infoParser->parse($fileinfo->uri) + $defaults, + ); + $info = &$theme->info; // Add the info file modification time, so it becomes available for // contributed modules to use for ordering theme lists. - $themes[$key]->info['mtime'] = filemtime($theme->uri); + $info['mtime'] = $fileinfo->getMTime(); // 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 $theme->type. $type = 'theme'; - $this->moduleHandler->alter('system_info', $themes[$key]->info, $themes[$key], $type); + $this->moduleHandler->alter('system_info', $info, $fileinfo, $type); - if (!empty($themes[$key]->info['base theme'])) { + if (!empty($info['base theme'])) { $sub_themes[] = $key; } - $engine = $themes[$key]->info['engine']; + $engine = $info['engine']; if (isset($engines[$engine])) { - $themes[$key]->owner = $engines[$engine]->uri; - $themes[$key]->prefix = $engines[$engine]->name; - $themes[$key]->template = TRUE; + $theme->owner = $engines[$engine]->uri; + $theme->prefix = $engines[$engine]->name; + $theme->template = TRUE; } - // Prefix stylesheets and scripts with module path. - $path = dirname($theme->uri); - $theme->info['stylesheets'] = $this->themeInfoPrefixPath($theme->info['stylesheets'], $path); - $theme->info['scripts'] = $this->themeInfoPrefixPath($theme->info['scripts'], $path); - - // Give the screenshot proper path information. - if (!empty($themes[$key]->info['screenshot'])) { - $themes[$key]->info['screenshot'] = $path . '/' . $themes[$key]->info['screenshot']; + // Prefix stylesheets, scripts, and screenshot with theme path. + $path = dirname($fileinfo->uri); + $info['stylesheets'] = $this->themeInfoPrefixPath($info['stylesheets'], $path); + $info['scripts'] = $this->themeInfoPrefixPath($info['scripts'], $path); + if (!empty($info['screenshot'])) { + $info['screenshot'] = $path . '/' . $info['screenshot']; } } @@ -436,23 +421,23 @@ protected function doGetBaseThemes(array $themes, $theme, $used_themes = array() return array($base_key => NULL); } $used_themes[$base_key] = TRUE; - return $this->getBaseThemes($themes, $base_key, $used_themes) + $current_base_theme; + return $this->doGetBaseThemes($themes, $base_key, $used_themes) + $current_base_theme; } // If we get here, then this is our parent theme. return $current_base_theme; } /** - * Returns a system listing info object. + * Returns a system listing object. * - * @return \Drupal\Core\SystemListingInfo + * @return \Drupal\Core\Extension\ExtensionDiscovery * The system listing object. */ - protected function getSystemListingInfo() { - if (!isset($this->systemListingInfo)) { - $this->systemListingInfo = new SystemListingInfo(); + protected function getSystemListing() { + if (!isset($this->systemListing)) { + $this->systemListing = new ExtensionDiscovery(); } - return $this->systemListingInfo; + return $this->systemListing; } /** diff --git a/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php b/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php index a8334b8..7f514a7 100644 --- a/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php +++ b/core/lib/Drupal/Core/Extension/UpdateModuleHandler.php @@ -46,8 +46,6 @@ public function getImplementations($hook) { case 'stream_wrappers': return array('system'); - // This is called during rebuild to find testing themes. - case 'system_theme_info': // Those are needed by user_access() to check access on update.php. case 'entity_info': case 'entity_load': diff --git a/core/lib/Drupal/Core/SystemListing.php b/core/lib/Drupal/Core/SystemListing.php deleted file mode 100644 index 20d95cb..0000000 --- a/core/lib/Drupal/Core/SystemListing.php +++ /dev/null @@ -1,208 +0,0 @@ -profiles = $profiles; - } - - /** - * Returns information about system object files (modules, themes, etc.). - * - * This function is used to find all or some system object files (module - * files, theme files, etc.) that exist on the site. It searches in several - * locations, depending on what type of object you are looking for. For - * instance, if you are looking for modules and call: - * @code - * $scanner = new SystemListing(); - * $all_modules = $scanner->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules'); - * @endcode - * this function will search: - * - the core modules directory; i.e., /core/modules - * - the profiles directories as defined by the profiles() method. - * - the site-wide modules directory; i.e., /modules - * - the all-sites directory; i.e., /sites/all/modules - * - the site-specific directory; i.e., /sites/example.com/modules - * in that order, and return information about all of the files ending in - * .module in those directories. - * - * The information is returned in an associative array, which can be keyed - * on the file name ($key = 'filename'), the file name without the extension - * ($key = 'name'), or the full file stream URI ($key = 'uri'). If you use a - * key of 'filename' or 'name', files found later in the search will take - * precedence over files found earlier (unless they belong to a module or - * theme not compatible with Drupal core); if you choose a key of 'uri', - * you will get all files found. - * - * @param string $mask - * The preg_match() regular expression for the files to find. The - * expression must be anchored and use DRUPAL_PHP_FUNCTION_PATTERN for the - * file name part before the extension, since the results could contain - * matches that do not present valid Drupal extensions otherwise. - * @param string $directory - * The subdirectory name in which the files are found. For example, - * 'modules' will search all 'modules' directories and their - * sub-directories as explained above. - * @param string $key - * (optional) The key to be used for the associative array returned. - * Possible values are: - * - 'uri' for the file's URI. - * - 'filename' for the basename of the file. - * - 'name' for the name of the file without the extension. - * For 'name' and 'filename' only the highest-precedence file is returned. - * Defaults to 'name'. - * - * @return array - * An associative array of file objects, keyed on the chosen key. Each - * element in the array is an object containing file information, with - * properties: - * - 'uri': Full URI of the file. - * - 'filename': File name. - * - 'name': Name of file without the extension. - */ - function scan($mask, $directory, $key = 'name') { - if (!in_array($key, array('uri', 'filename', 'name'))) { - $key = 'uri'; - } - $config = conf_path(); - - // Search for the directory in core. - $searchdir = array('core/' . $directory); - foreach ($this->profiles($directory) as $profile) { - $searchdir[] = $profile; - } - - // Always search for contributed and custom extensions in top-level - // directories as well as sites/all/* directories. If the same extension is - // located in both directories, then the latter wins for legacy/historical - // reasons. - $searchdir[] = $directory; - $searchdir[] = 'sites/all/' . $directory; - - if (file_exists("$config/$directory")) { - $searchdir[] = "$config/$directory"; - } - // @todo Find a way to skip ./config directories (but not modules/config). - $nomask = '/^(CVS|lib|templates|css|js)$/'; - $files = array(); - // Get current list of items. - foreach ($searchdir as $dir) { - $files = array_merge($files, $this->process($files, $this->scanDirectory($dir, $key, $mask, $nomask))); - } - return $files; - } - - /** - * List the profiles for this directory. - * - * This version only returns those passed to the constructor. - * - * @param string $directory - * The current search directory like 'modules' or 'themes'. - * - * @return array - * A list of profiles. - */ - protected function profiles($directory) { - return $this->profiles; - } - - /** - * Process the files to add before adding them. - * - * @param array $files - * Every file found so far. - * @param array $files_to_add - * The files found in a single directory. - * - * @return array - * The processed list of file objects. For example, the SystemListingInfo - * class removes files not compatible with the current core version. - */ - protected function process(array $files, array $files_to_add) { - return $files_to_add; - } - - /** - * Abbreviated version of file_scan_directory(). - * - * @param $dir - * The base directory or URI to scan, without trailing slash. - * @param $key - * The key to be used for the returned associative array of files. - * Possible values are 'uri', for the file's URI; 'filename', for the - * basename of the file; and 'name' for the name of the file without the - * extension. - * @param $mask - * The preg_match() regular expression of the files to find. - * @param $nomask - * The preg_match() regular expression of the files to ignore. - * - * @return array - * An associative array (keyed on the chosen key) of objects with 'uri', - * 'filename', and 'name' members corresponding to the matching files. - */ - protected function scanDirectory($dir, $key, $mask, $nomask) { - $files = array(); - if (is_dir($dir)) { - // Avoid warnings when opendir does not have the permissions to open a - // directory. - if ($handle = @opendir($dir)) { - while (FALSE !== ($filename = readdir($handle))) { - // Skip this file if it matches the nomask or starts with a dot. - if ($filename[0] != '.' && !preg_match($nomask, $filename)) { - $uri = "$dir/$filename"; - if (is_dir($uri)) { - // Give priority to files in this folder by merging them in after - // any subdirectory files. - $files = array_merge($this->scanDirectory($uri, $key, $mask, $nomask), $files); - } - elseif (preg_match($mask, $filename)) { - // Always use this match over anything already set in $files with - // the same $options['key']. - $file = new \stdClass(); - $file->uri = $uri; - $file->filename = $filename; - $file->name = pathinfo($filename, PATHINFO_FILENAME); - $this->processFile($file); - $files[$file->$key] = $file; - } - } - } - closedir($handle); - } - } - return $files; - } - - /** - * Process each file object as it is found by scanDirectory(). - * - * @param $file - * A file object. - */ - protected function processFile($file) { - } - -} diff --git a/core/lib/Drupal/Core/SystemListingInfo.php b/core/lib/Drupal/Core/SystemListingInfo.php deleted file mode 100644 index d302699..0000000 --- a/core/lib/Drupal/Core/SystemListingInfo.php +++ /dev/null @@ -1,79 +0,0 @@ - - // directories. - $profile = drupal_get_profile(); - // For SimpleTest to be able to test modules packaged together with a - // distribution we need to include the profile of the parent site (in - // which test runs are triggered). - if (drupal_valid_test_ua() && !drupal_installation_attempted()) { - $testing_profile = \Drupal::config('simpletest.settings')->get('parent_profile'); - if ($testing_profile && $testing_profile != $profile) { - $searchdir[] = drupal_get_path('profile', $testing_profile) . '/' . $directory; - } - } - // In case both profile directories contain the same extension, the actual - // profile always has precedence. - $searchdir[] = drupal_get_path('profile', $profile) . '/' . $directory; - return $searchdir; - } - - /** - * Overrides Drupal\Core\SystemListing::process(). - */ - protected function process(array $files, array $files_to_add) { - // Duplicate files found in later search directories take precedence over - // earlier ones, so we want them to overwrite keys in our resulting - // $files array. - // The exception to this is if the later file is from a module or theme not - // compatible with Drupal core. This may occur during upgrades of Drupal - // core when new modules exist in core while older contrib modules with the - // same name exist in a directory such as /modules. - foreach (array_intersect_key($files_to_add, $files) as $file_key => $file) { - // If it has no info file, then we just behave liberally and accept the - // new resource on the list for merging. - if (file_exists($info_file = dirname($file->uri) . '/' . $file->name . '.info.yml')) { - // Get the .info.yml file for the module or theme this file belongs to. - $info = \Drupal::service('info_parser')->parse($info_file); - - // If the module or theme is incompatible with Drupal core, remove it - // from the array for the current search directory, so it is not - // overwritten when merged with the $files array. - if (isset($info['core']) && $info['core'] != \Drupal::CORE_COMPATIBILITY) { - unset($files_to_add[$file_key]); - } - } - } - return $files_to_add; - } - - /** - * Overrides Drupal\Core\SystemListing::processFile(). - */ - protected function processFile($file) { - $file->name = basename($file->name, '.info'); - } - -} diff --git a/core/modules/block/tests/modules/block_test/block_test.module b/core/modules/block/tests/modules/block_test/block_test.module index 4370202..1530f2c 100644 --- a/core/modules/block/tests/modules/block_test/block_test.module +++ b/core/modules/block/tests/modules/block_test/block_test.module @@ -6,14 +6,6 @@ */ /** - * Implements hook_system_theme_info(). - */ -function block_test_system_theme_info() { - $themes['block_test_theme'] = drupal_get_path('module', 'block_test') . '/themes/block_test_theme/block_test_theme.info.yml'; - return $themes; -} - -/** * Implements hook_block_alter(). */ function block_test_block_alter(&$block_info) { diff --git a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php index ed88881..153df41 100644 --- a/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php +++ b/core/modules/breakpoint/lib/Drupal/breakpoint/Tests/BreakpointThemeTest.php @@ -15,13 +15,6 @@ */ class BreakpointThemeTest extends BreakpointGroupTestBase { - /** - * Modules to enable. - * - * @var array - */ - public static $modules = array('breakpoint_theme_test'); - public static function getInfo() { return array( 'name' => 'Breakpoint theme functionality', diff --git a/core/modules/breakpoint/tests/breakpoint_theme_test.info.yml b/core/modules/breakpoint/tests/breakpoint_theme_test.info.yml deleted file mode 100644 index f035360..0000000 --- a/core/modules/breakpoint/tests/breakpoint_theme_test.info.yml +++ /dev/null @@ -1,9 +0,0 @@ -name: 'Breakpoint theme test' -type: module -description: 'Test breakpoints provided by themes' -package: Other -version: VERSION -core: 8.x -hidden: true -dependencies: - - breakpoint diff --git a/core/modules/breakpoint/tests/breakpoint_theme_test.module b/core/modules/breakpoint/tests/breakpoint_theme_test.module deleted file mode 100644 index 765cc50..0000000 --- a/core/modules/breakpoint/tests/breakpoint_theme_test.module +++ /dev/null @@ -1,13 +0,0 @@ -folders)) { - $this->folders = $this->getComponentNames('profile', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles'))); - $this->folders += $this->getComponentNames('module', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0))); - $this->folders += $this->getComponentNames('theme', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes'))); + $listing = new ExtensionDiscovery(); + $this->folders = $this->getComponentNames('profile', array_keys($listing->scan('profiles'))); + $this->folders += $this->getComponentNames('module', array_keys($listing->scan('modules'))); + $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('themes'))); } return $this->folders; } diff --git a/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php index 8d240b0..bf5a6fc 100644 --- a/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestSchemaStorage.php @@ -8,6 +8,7 @@ namespace Drupal\config_test; use Drupal\Core\Config\Schema\SchemaStorage; +use Drupal\Core\Extension\ExtensionDiscovery; /** * Tests configuration schemas of profiles, modules and themes. @@ -29,10 +30,11 @@ public function __construct() { */ protected function getAllFolders() { if (!isset($this->folders)) { + $listing = new ExtensionDiscovery(); $this->folders = $this->getBaseDataTypeSchema(); - $this->folders += $this->getComponentNames('profile', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles'))); - $this->folders += $this->getComponentNames('module', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0))); - $this->folders += $this->getComponentNames('theme', array_keys(drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info.yml$/', 'themes'))); + $this->folders += $this->getComponentNames('profile', array_keys($listing->scan('profiles'))); + $this->folders += $this->getComponentNames('module', array_keys($listing->scan('modules'))); + $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('themes'))); } return $this->folders; } diff --git a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module index ac6fb3f..1e04a19 100644 --- a/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module +++ b/core/modules/config_translation/tests/modules/config_translation_test/config_translation_test.module @@ -70,11 +70,3 @@ function config_translation_test_form_config_translation_edit_form_alter(&$form, $form['#altered'] = TRUE; } } - -/** - * Implements hook_system_theme_info(). - */ -function config_translation_test_system_theme_info() { - $themes['config_translation_test_theme'] = drupal_get_path('module', 'config_translation') . '/tests/themes/config_translation_test_theme/config_translation_test_theme.info.yml'; - return $themes; -} diff --git a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index fff0ace..13bd091 100644 --- a/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php +++ b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php @@ -777,7 +777,7 @@ protected function setUp() { // Set 'parent_profile' of simpletest to add the parent profile's // search path to the child site's search paths. - // @see drupal_system_listing() + // @see \Drupal\Core\Extension\ExtensionDiscovery::getProfileDirectories() \Drupal::config('simpletest.settings')->set('parent_profile', $this->originalProfile)->save(); // Collect modules to install. diff --git a/core/modules/simpletest/simpletest.module b/core/modules/simpletest/simpletest.module index c6b53bf..b00f11c 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -2,6 +2,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\Page\HtmlPage; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\simpletest\TestBase; use Symfony\Component\Process\PhpExecutableFinder; @@ -444,14 +445,13 @@ function simpletest_log_read($test_id, $prefix, $test_class, $during_test = FALS * @endcode */ function simpletest_test_get_all($module = NULL) { - $all_groups = &drupal_static(__FUNCTION__); + static $all_groups = array(); $cid = "simpletest:$module"; if (!isset($all_groups[$cid])) { $all_groups[$cid] = array(); $groups = &$all_groups[$cid]; - // Make sure that namespaces for disabled modules are registered so that the - // checks below will find them. + // Register namespaces (extensions are not necessarily enabled). simpletest_classloader_register(); // Load test information from cache if available, otherwise retrieve the @@ -461,22 +461,25 @@ function simpletest_test_get_all($module = NULL) { } else { // Select all PSR-0 classes in the Tests namespace of all modules. - $classes = array(); - $module_data = system_rebuild_module_data(); - $all_data = $module_data + system_rebuild_theme_data(); - $all_data += drupal_system_listing('/\.profile$/', 'profiles', 'name'); + $listing = new ExtensionDiscovery(); + $all_data = $listing->scan('modules', TRUE); // If module is set then we keep only that one module. if (isset($module)) { $all_data = array( $module => $all_data[$module], ); } + else { + $all_data += $listing->scan('profiles', TRUE); + $all_data += $listing->scan('themes', TRUE); + } + $classes = array(); foreach ($all_data as $name => $data) { // Build directory in which the test files would reside. $tests_dir = DRUPAL_ROOT . '/' . dirname($data->uri) . '/lib/Drupal/' . $name . '/Tests'; // Scan it for test files if it exists. if (is_dir($tests_dir)) { - $files = file_scan_directory($tests_dir, '/.*\.php/'); + $files = file_scan_directory($tests_dir, '/\.php$/'); if (!empty($files)) { $basedir = DRUPAL_ROOT . '/' . dirname($data->uri) . '/lib/'; foreach ($files as $file) { @@ -546,23 +549,38 @@ function simpletest_test_get_all($module = NULL) { * Registers namespaces for disabled modules. */ function simpletest_classloader_register() { + // Use the same cache prefix as simpletest_test_get_all(). + $cid = "simpletest::all"; // @see drupal_get_filename() $types = array( - 'theme_engine' => array('dir' => 'themes/engines', 'extension' => 'engine'), - 'module' => array('dir' => 'modules', 'extension' => 'module'), - 'theme' => array('dir' => 'themes', 'extension' => 'info'), - 'profile' => array('dir' => 'profiles', 'extension' => 'profile'), + 'theme_engine' => 'themes/engines', + 'module' => 'modules', + 'theme' => 'themes', + 'profile' => 'profiles', ); - $classloader = drupal_classloader(); + if ($cache = cache()->get($cid)) { + $extensions = $cache->data; + } + else { + $listing = new ExtensionDiscovery(); + $extensions = array(); + foreach ($types as $type => $dir) { + $extensions[$type] = $listing->scan($dir, TRUE); + foreach ($extensions[$type] as $name => $file) { + $extensions[$type][$name] = $file->uri; + } + } + cache()->set($cid, $extensions); + } + $classloader = drupal_classloader(); foreach ($types as $type => $info) { - $matches = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.' . $info['extension'] . '$/', $info['dir']); - foreach ($matches as $name => $file) { - drupal_classloader_register($name, dirname($file->uri)); - $classloader->add('Drupal\\' . $name . '\\Tests', DRUPAL_ROOT . '/' . dirname($file->uri) . '/tests'); + foreach ($extensions[$type] as $name => $uri) { + drupal_classloader_register($name, dirname($uri)); + $classloader->add('Drupal\\' . $name . '\\Tests', DRUPAL_ROOT . '/' . dirname($uri) . '/tests'); // While being there, prime drupal_get_filename(). - drupal_get_filename($type, $name, $file->uri); + drupal_get_filename($type, $name, $uri); } } diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php index ee1f12b..937d7f8 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/SystemListingTest.php @@ -7,6 +7,7 @@ namespace Drupal\system\Tests\Common; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\simpletest\WebTestBase; /** @@ -55,7 +56,8 @@ function testDirectoryPrecedence() { // Now scan the directories and check that the files take precedence as // expected. - $files = drupal_system_listing('/\.module$/', 'modules'); + $listing = new ExtensionDiscovery(); + $files = $listing->scan('modules'); foreach ($expected_directories as $module => $directories) { $expected_directory = array_shift($directories); $expected_filename = "$expected_directory/$module/$module.module"; diff --git a/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php b/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php index e9cf688..8be4af9 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Extension/InfoParserUnitTest.php @@ -72,7 +72,7 @@ public function testInfoParser() { $this->fail('Expected InfoParserException not thrown when reading missing_keys.info.txt'); } catch (InfoParserException $e) { - $expected_message = 'Missing required keys (name, type) in core/modules/system/tests/fixtures/missing_keys.info.txt.'; + $expected_message = "Missing required keys (type, core, name) in $filename."; $this->assertEqual($e->getMessage(), $expected_message); } @@ -83,7 +83,7 @@ public function testInfoParser() { $this->fail('Expected InfoParserException not thrown when reading missing_key.info.txt'); } catch (InfoParserException $e) { - $expected_message = 'Missing required key (type) in core/modules/system/tests/fixtures/missing_key.info.txt.'; + $expected_message = "Missing required key (type) in $filename."; $this->assertEqual($e->getMessage(), $expected_message); } diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php index 0d9815c..26cfc0d 100644 --- a/core/modules/system/system.api.php +++ b/core/modules/system/system.api.php @@ -1192,25 +1192,6 @@ function hook_system_breadcrumb_alter(array &$breadcrumb, array $attributes, arr } /** - * Return additional themes provided by modules. - * - * Only use this hook for testing purposes. Use a hidden MYMODULE_test.module - * to implement this hook. Testing themes should be hidden, too. - * - * This hook is invoked from _system_rebuild_theme_data() and allows modules to - * register additional themes outside of the regular 'themes' directories of a - * Drupal installation. - * - * @return - * An associative array. Each key is the system name of a theme and each value - * is the corresponding path to the theme's .info.yml file. - */ -function hook_system_theme_info() { - $themes['mymodule_test_theme'] = drupal_get_path('module', 'mymodule') . '/mymodule_test_theme/mymodule_test_theme.info.yml'; - return $themes; -} - -/** * Alter the information parsed from module and theme .info.yml files * * This hook is invoked in _system_rebuild_module_data() and in diff --git a/core/modules/system/system.module b/core/modules/system/system.module index cc647a2..7f3704b 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -8,6 +8,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\Cache; use Drupal\Core\Language\Language; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Utility\ModuleInfo; use Drupal\menu_link\MenuLinkInterface; use Drupal\user\UserInterface; @@ -2579,18 +2580,19 @@ function system_get_module_info($property) { * An associative array of module information. */ function _system_rebuild_module_data() { + $listing = new ExtensionDiscovery(); // Find modules - $modules = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules', 'name', 0); + $modules = $listing->scan('modules'); // Find installation profiles. - $profiles = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles', 'name', 0); + $profiles = $listing->scan('profiles'); // Include the installation profile in modules that are loaded. - $profile = drupal_get_profile(); - $modules[$profile] = $profiles[$profile]; - - // Installation profile hooks are always executed last. - $modules[$profile]->weight = 1000; + if ($profile = drupal_get_profile()) { + $modules[$profile] = $profiles[$profile]; + // Installation profile hooks are always executed last. + $modules[$profile]->weight = 1000; + } // Set defaults for module info. $defaults = array( @@ -2643,7 +2645,7 @@ function _system_rebuild_module_data() { } - if (isset($modules[$profile])) { + 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. This diff --git a/core/modules/system/tests/fixtures/common_test.info.txt b/core/modules/system/tests/fixtures/common_test.info.txt index ae97b16..7e57dfe 100644 --- a/core/modules/system/tests/fixtures/common_test.info.txt +++ b/core/modules/system/tests/fixtures/common_test.info.txt @@ -1,3 +1,4 @@ +core: 8.x name: common_test type: module description: 'testing info file parsing' diff --git a/core/modules/system/tests/fixtures/missing_keys.info.txt b/core/modules/system/tests/fixtures/missing_keys.info.txt index 56c6411..c04f34d 100644 --- a/core/modules/system/tests/fixtures/missing_keys.info.txt +++ b/core/modules/system/tests/fixtures/missing_keys.info.txt @@ -1,6 +1,5 @@ # info.yml for testing missing name, description, and type keys. package: Core version: VERSION -core: 8.x dependencies: - field diff --git a/core/modules/system/tests/modules/ajax_test/ajax_test.module b/core/modules/system/tests/modules/ajax_test/ajax_test.module index 18a002a..9eb9178 100644 --- a/core/modules/system/tests/modules/ajax_test/ajax_test.module +++ b/core/modules/system/tests/modules/ajax_test/ajax_test.module @@ -13,14 +13,6 @@ use Drupal\Core\Ajax\HtmlCommand; /** - * Implements hook_system_theme_info(). - */ -function ajax_test_system_theme_info() { - $themes['test_theme'] = drupal_get_path('module', 'system') . '/tests/themes/test_theme/test_theme.info.yml'; - return $themes; -} - -/** * Menu callback: Returns an element suitable for use by * \Drupal\Core\Ajax\AjaxResponse::ajaxRender(). * diff --git a/core/modules/system/tests/modules/theme_page_test/theme_page_test.module b/core/modules/system/tests/modules/theme_page_test/theme_page_test.module index 6fdd7bd..1180d64 100644 --- a/core/modules/system/tests/modules/theme_page_test/theme_page_test.module +++ b/core/modules/system/tests/modules/theme_page_test/theme_page_test.module @@ -9,13 +9,3 @@ function theme_page_test_system_info_alter(&$info, $file, $type) { unset($info['hidden']); } } - - -/** - * Implements hook_system_theme_info(). - */ -function theme_page_test_system_theme_info() { - $themes['test_invalid_basetheme'] = drupal_get_path('module', 'system') . '/tests/themes/test_invalid_basetheme/test_invalid_basetheme.info.yml'; - $themes['test_invalid_engine'] = drupal_get_path('module', 'system') . '/tests/themes/test_invalid_engine/test_invalid_engine.info.yml'; - return $themes; -} diff --git a/core/modules/system/tests/modules/theme_test/theme_test.module b/core/modules/system/tests/modules/theme_test/theme_test.module index bdb1404..cd4c646 100644 --- a/core/modules/system/tests/modules/theme_test/theme_test.module +++ b/core/modules/system/tests/modules/theme_test/theme_test.module @@ -53,17 +53,6 @@ function theme_test_theme($existing, $type, $theme, $path) { } /** - * Implements hook_system_theme_info(). - */ -function theme_test_system_theme_info() { - $themes['test_theme'] = drupal_get_path('module', 'system') . '/tests/themes/test_theme/test_theme.info.yml'; - $themes['test_basetheme'] = drupal_get_path('module', 'system') . '/tests/themes/test_basetheme/test_basetheme.info.yml'; - $themes['test_subtheme'] = drupal_get_path('module', 'system') . '/tests/themes/test_subtheme/test_subtheme.info.yml'; - $themes['test_theme_phptemplate'] = drupal_get_path('module', 'system') . '/tests/themes/test_theme_phptemplate/test_theme_phptemplate.info.yml'; - return $themes; -} - -/** * Implements hook_menu(). */ function theme_test_menu() { diff --git a/core/modules/update/tests/modules/update_test/update_test.module b/core/modules/update/tests/modules/update_test/update_test.module index 249ad83..ed5e008 100644 --- a/core/modules/update/tests/modules/update_test/update_test.module +++ b/core/modules/update/tests/modules/update_test/update_test.module @@ -9,15 +9,6 @@ */ /** - * Implements hook_system_theme_info(). - */ -function update_test_system_theme_info() { - $themes['update_test_basetheme'] = drupal_get_path('module', 'update') . '/tests/themes/update_test_basetheme/update_test_basetheme.info.yml'; - $themes['update_test_subtheme'] = drupal_get_path('module', 'update') . '/tests/themes/update_test_subtheme/update_test_subtheme.info.yml'; - return $themes; -} - -/** * Implements hook_system_info_alter(). * * Checks the 'update_test.settings:system_info' configuration and sees if we diff --git a/core/modules/update/update.module b/core/modules/update/update.module index adf6ba3..aedccd5 100644 --- a/core/modules/update/update.module +++ b/core/modules/update/update.module @@ -629,7 +629,7 @@ function theme_update_last_check($variables) { * an .info.yml file which claims that the code is compatible with the current * version of Drupal core. * - * @see drupal_system_listing() + * @see \Drupal\Core\Extension\ExtensionDiscovery::process() * @see _system_rebuild_module_data() */ function update_verify_update_archive($project, $archive_file, $directory) { diff --git a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php index 25901c1..5fc60da 100644 --- a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php @@ -67,9 +67,9 @@ class ThemeHandlerTest extends UnitTestCase { /** * The system listing info. * - * @var \Drupal\Core\SystemListingInfo|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Extension\ExtensionDiscovery|\PHPUnit_Framework_MockObject_MockObject */ - protected $systemListingInfo; + protected $systemListing; /** * The tested theme handler. @@ -101,10 +101,10 @@ protected function setUp() { $this->routeBuilder = $this->getMockBuilder('Drupal\Core\Routing\RouteBuilder') ->disableOriginalConstructor() ->getMock(); - $this->systemListingInfo = $this->getMockBuilder('Drupal\Core\SystemListingInfo') + $this->systemListing = $this->getMockBuilder('Drupal\Core\Extension\ExtensionDiscovery') ->disableOriginalConstructor() ->getMock(); - $this->themeHandler = new TestThemeHandler($this->configFactory, $this->moduleHandler, $this->cacheBackend, $this->infoParser, $this->configInstaller, $this->routeBuilder, $this->systemListingInfo); + $this->themeHandler = new TestThemeHandler($this->configFactory, $this->moduleHandler, $this->cacheBackend, $this->infoParser, $this->configInstaller, $this->routeBuilder, $this->systemListing); $container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); $container->expects($this->any()) @@ -154,16 +154,11 @@ public function testEnableSingleTheme() { ->expects($this->once()) ->method('save'); - $this->systemListingInfo->expects($this->any()) + $this->systemListing->expects($this->any()) ->method('scan') ->will($this->returnValue(array())); // Ensure that the themes_enabled hook is fired. - $this->moduleHandler->expects($this->at(0)) - ->method('invokeAll') - ->with('system_theme_info') - ->will($this->returnValue(array())); - $this->moduleHandler->expects($this->at(1)) ->method('invokeAll') ->with('themes_enabled', array($theme_list)); @@ -195,7 +190,7 @@ public function testEnableAndListInfo() { ->method('clear') ->will($this->returnSelf()); - $this->systemListingInfo->expects($this->any()) + $this->systemListing->expects($this->any()) ->method('scan') ->will($this->returnValue(array())); @@ -255,9 +250,9 @@ public function testEnableAndListInfo() { * @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData() */ public function testRebuildThemeData() { - $this->systemListingInfo->expects($this->at(0)) + $this->systemListing->expects($this->at(0)) ->method('scan') - ->with($this->anything(), 'themes', 'name', 1) + ->with($this->anything(), 'themes', 'name') ->will($this->returnValue(array( 'seven' => (object) array( 'name' => 'seven', diff --git a/core/themes/engines/phptemplate/phptemplate.info.yml b/core/themes/engines/phptemplate/phptemplate.info.yml new file mode 100644 index 0000000..2bea7a0 --- /dev/null +++ b/core/themes/engines/phptemplate/phptemplate.info.yml @@ -0,0 +1,5 @@ +type: theme_engine +name: PHPTemplate +core: 8.x +version: VERSION +package: Core diff --git a/core/themes/engines/twig/twig.info.yml b/core/themes/engines/twig/twig.info.yml new file mode 100644 index 0000000..1767384 --- /dev/null +++ b/core/themes/engines/twig/twig.info.yml @@ -0,0 +1,5 @@ +type: theme_engine +name: Twig +core: 8.x +version: VERSION +package: Core