diff --git a/core/includes/bootstrap.inc b/core/includes/bootstrap.inc index a46b7bf..e6b7cc3 100644 --- a/core/includes/bootstrap.inc +++ b/core/includes/bootstrap.inc @@ -10,7 +10,7 @@ use Drupal\Core\DrupalKernel; use Drupal\Core\Database\Database; use Drupal\Core\DependencyInjection\ContainerBuilder; -use Drupal\Core\SystemListing; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Utility\Title; use Drupal\Core\Utility\Error; use Symfony\Component\ClassLoader\ApcClassLoader; @@ -754,8 +754,8 @@ function drupal_get_filename($type, $name, $filename = NULL) { // 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. - $listing = new SystemListing(); - $matches = $listing->scan("/^" . 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/install.core.inc b/core/includes/install.core.inc index 63c22d0..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\SystemListing; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Reference; @@ -501,8 +501,8 @@ function install_begin_request(&$install_state) { $module_handler->loadAll(); // Add list of all available profiles to the installation state. - $listing = new SystemListing(); - $install_state['profiles'] += $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles'); + $listing = new ExtensionDiscovery(); + $install_state['profiles'] += $listing->scan('profiles'); // Prime drupal_get_filename()'s static cache. foreach ($install_state['profiles'] as $name => $profile) { diff --git a/core/includes/install.inc b/core/includes/install.inc index ff000ac..ab9a640 100644 --- a/core/includes/install.inc +++ b/core/includes/install.inc @@ -9,7 +9,7 @@ use Drupal\Component\Utility\Crypt; use Drupal\Core\Database\Database; use Drupal\Core\DrupalKernel; -use Drupal\Core\SystemListing; +use Drupal\Core\Extension\ExtensionDiscovery; /** * Requirement severity -- Informational message only. @@ -566,9 +566,9 @@ function drupal_verify_profile($install_state) { $info = $install_state['profile_info']; // Get the list of available modules for the selected installation profile. - $listing = new SystemListing(); + $listing = new ExtensionDiscovery(); $present_modules = array(); - foreach ($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules') as $present_module) { + foreach ($listing->scan('modules') as $present_module) { $present_modules[] = $present_module->name; } @@ -951,9 +951,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 dce73de..96bfbf0 100644 --- a/core/includes/module.inc +++ b/core/includes/module.inc @@ -5,7 +5,7 @@ * API for loading and interacting with Drupal modules. */ -use Drupal\Core\SystemListing; +use Drupal\Core\Extension\ExtensionDiscovery; /** * Builds a list of bootstrap modules and enabled modules and themes. @@ -286,8 +286,8 @@ function module_uninstall($module_list = array(), $uninstall_dependents = TRUE) * Returns an array of modules required by core. */ function drupal_required_modules() { - $listing = new SystemListing(); - $files = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules'); + $listing = new ExtensionDiscovery(); + $files = $listing->scan('modules'); $required = array(); // Unless called by the installer, an installation profile is required and @@ -298,11 +298,7 @@ function drupal_required_modules() { } foreach ($files as $name => $file) { - $info_file = dirname($file->uri) . '/' . $file->name . '.info.yml'; - if (!file_exists($info_file)) { - continue; - } - $info = \Drupal::service('info_parser')->parse($info_file); + $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 2b4f0ad..5d05790 100644 --- a/core/lib/Drupal/Core/Config/InstallStorage.php +++ b/core/lib/Drupal/Core/Config/InstallStorage.php @@ -7,7 +7,7 @@ namespace Drupal\Core\Config; -use Drupal\Core\SystemListing; +use Drupal\Core\Extension\ExtensionDiscovery; /** * Storage controller used by the Drupal installer. @@ -116,9 +116,9 @@ protected function getAllFolders() { if ($profile = drupal_get_profile()) { $this->folders += $this->getComponentNames('profile', array($profile)); } - $listing = new SystemListing(); - $this->folders += $this->getComponentNames('module', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules'))); - $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info\.yml$/', 'themes'))); + $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 ecc8881..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 - * SystemListing but only the uri property is used. + * ExtensionDiscovery but only the uri property is used. * * @var array */ @@ -297,8 +298,8 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ protected function moduleData($module) { if (!$this->moduleData) { // First, find profiles. - $listing = new SystemListing(); - $all_profiles = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', '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 @@ -317,7 +318,7 @@ protected function moduleData($module) { $listing->setProfileDirectories($profile_directories); // Now find modules. - $this->moduleData = $all_profiles + $listing->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..be2051a --- /dev/null +++ b/core/lib/Drupal/Core/Extension/Discovery/RecursiveExtensionFilterIterator.php @@ -0,0 +1,65 @@ +acceptTests = $flag; + } + if (!$this->acceptTests) { + $this->skipDirs[] = 'tests'; + } + } + + public function getChildren() { + $filter = parent::getChildren(); + $filter->acceptTests($this->acceptTests); + return $filter; + } + + public function accept() { + // Should normally never be TRUE, since ExtensionDiscovery passes the + // FilesystemIterator::SKIP_DOTS flag to RecursiveDirectoryIterator, but + // in case this filter is instantiated from elsewhere, ensure that we are + // not recursing into parent directories. + if ($this->isDot()) { + return FALSE; + } + if ($this->isDir()) { + return !in_array($this->current()->getFilename(), $this->skipDirs, TRUE); + } + else { + return substr($this->current()->getFilename(), -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..6e12d2e --- /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); + } + + $site = conf_path(); + + // Installation profile directories can only be scanned for extensions when + // not scanning for installation profiles themselves. + $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; + } + } + + // Search for the directory in core. + $searchdirs = array(); + $searchdirs[] = 'core/' . $directory; + foreach ($profileDirectories as $profile) { + $searchdirs[] = $profile . '/' . $directory; + } + // 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. + $searchdirs[] = $directory; + $searchdirs[] = 'sites/all/' . $directory; + $searchdirs[] = "$site/$directory"; + + $files = array(); + foreach ($searchdirs as $dir) { + if ($include_tests) { + $directory_extensions = $this->scanDirectory($dir, $include_tests); + } + else { + if (!isset(static::$files[$dir])) { + static::$files[$dir] = $this->scanDirectory($dir); + } + $directory_extensions = static::$files[$dir]; + } + if (isset($directory_extensions[$type])) { + $files = array_merge($files, $this->process($files, $directory_extensions[$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/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 26cb679..446079a 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\SystemListing; +use Drupal\Core\Extension\ExtensionDiscovery; /** * Default theme handler using the config system for enabled/disabled themes. @@ -89,7 +89,7 @@ class ThemeHandler implements ThemeHandlerInterface { /** * A system listing instance. * - * @var \Drupal\Core\SystemListing + * @var \Drupal\Core\Extension\ExtensionDiscovery */ protected $systemListing; @@ -110,10 +110,10 @@ 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\SystemListing $system_list + * @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, SystemListing $system_list = 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; @@ -245,11 +245,11 @@ public function reset() { public function rebuildThemeData() { // Find themes. $listing = $this->getSystemListing(); - $themes = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info\.yml$/', 'themes'); + $themes = $listing->scan('themes'); // Allow modules to add further themes. if ($module_themes = $this->moduleHandler->invokeAll('system_theme_info')) { foreach ($module_themes as $name => $uri) { - // @see \Drupal\Core\SystemListing + // @see \Drupal\Core\Extension\ExtensionDiscovery $themes[$name] = (object) array( 'uri' => $uri, 'filename' => pathinfo($uri, PATHINFO_FILENAME), @@ -259,7 +259,7 @@ public function rebuildThemeData() { } // Find theme engines. - $engines = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.engine$/', 'themes/engines'); + $engines = $listing->scan('themes/engines'); // Set defaults for theme info. $defaults = array( @@ -286,23 +286,24 @@ 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; - } - + // The theme data is serialized into State. But RecursiveDirectoryIterator + // objects are not serializable, so replace the item with a stdClass. + $themes[$key] = (object) array( + 'uri' => $theme->uri, + // @todo Filename hold filename, not URI. + 'filename' => $theme->uri, + 'name' => $theme->name, + 'info' => $this->infoParser->parse($theme->uri) + $defaults, + ); // 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); + $themes[$key]->info['mtime'] = $theme->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', $themes[$key]->info, $theme, $type); if (!empty($themes[$key]->info['base theme'])) { $sub_themes[] = $key; @@ -317,8 +318,8 @@ public function rebuildThemeData() { // 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); + $themes[$key]->info['stylesheets'] = $this->themeInfoPrefixPath($themes[$key]->info['stylesheets'], $path); + $themes[$key]->info['scripts'] = $this->themeInfoPrefixPath($themes[$key]->info['scripts'], $path); // Give the screenshot proper path information. if (!empty($themes[$key]->info['screenshot'])) { @@ -435,7 +436,7 @@ 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; @@ -444,12 +445,12 @@ protected function doGetBaseThemes(array $themes, $theme, $used_themes = array() /** * Returns a system listing object. * - * @return \Drupal\Core\SystemListing + * @return \Drupal\Core\Extension\ExtensionDiscovery * The system listing object. */ protected function getSystemListing() { if (!isset($this->systemListing)) { - $this->systemListing = new SystemListing(); + $this->systemListing = new ExtensionDiscovery(); } return $this->systemListing; } diff --git a/core/lib/Drupal/Core/SystemListing.php b/core/lib/Drupal/Core/SystemListing.php deleted file mode 100644 index 7c8c051..0000000 --- a/core/lib/Drupal/Core/SystemListing.php +++ /dev/null @@ -1,304 +0,0 @@ -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. - */ - public function scan($mask, $directory, $key = 'name') { - if (!in_array($key, array('uri', 'filename', 'name'))) { - $key = 'uri'; - } - $site = conf_path(); - - // Installation profile directories can only be scanned for extensions when - // not scanning for installation profiles themselves. - $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; - } - if (isset($profileDirectories[0]) && $profileDirectories[0] === '') { - throw new \RuntimeException('drupal_get_profile() can return an empty string. Please adjust your code.'); - } - } - - // Check static cache. - // The static cache is valid unless the dynamic directories have changed. - $dynamic_dirs_hash = implode('|', array($site => $site) + $profileDirectories); - if (isset(static::$files[$dynamic_dirs_hash][$directory])) { - return static::$files[$dynamic_dirs_hash][$directory]; - } - - // Search for the directory in core. - $searchdir = array('core/' . $directory); - foreach ($profileDirectories as $profile) { - $searchdir[] = $profile . '/' . $directory; - } - - // 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; - $searchdir[] = "$site/$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))); - } - static::$files[$dynamic_dirs_hash][$directory] = $files; - return $files; - } - - /** - * Returns additional installation profile directories to be scanned. - * - * @return array - * A list of installation profile directory paths relative to system root. - */ - protected 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) { - // 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 = $this->getInfoParser()->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; - } - - /** - * Recursively scans a base directory for the requested extension type. - * - * @param string $dir - * The base directory or URI to scan, without trailing slash. - * @param string $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. - * - 'name' for the name of the file without the extension. - * @param string $mask - * The preg_match() regular expression of the files to find. - * @param string $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) { - $file->name = basename($file->name, '.info'); - } - - /** - * 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/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php index a4dcda9..4782304 100644 --- a/core/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php +++ b/core/modules/config/tests/config_test/lib/Drupal/config_test/TestInstallStorage.php @@ -8,7 +8,7 @@ namespace Drupal\config_test; use Drupal\Core\Config\InstallStorage; -use Drupal\Core\SystemListing; +use Drupal\Core\Extension\ExtensionDiscovery; /** * Tests configuration of profiles, modules and themes. @@ -23,10 +23,10 @@ class TestInstallStorage extends InstallStorage { */ protected function getAllFolders() { if (!isset($this->folders)) { - $listing = new SystemListing(); - $this->folders = $this->getComponentNames('profile', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles'))); - $this->folders += $this->getComponentNames('module', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules'))); - $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('/^' . 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 8c86525..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,7 +8,7 @@ namespace Drupal\config_test; use Drupal\Core\Config\Schema\SchemaStorage; -use Drupal\Core\SystemListing; +use Drupal\Core\Extension\ExtensionDiscovery; /** * Tests configuration schemas of profiles, modules and themes. @@ -30,11 +30,11 @@ public function __construct() { */ protected function getAllFolders() { if (!isset($this->folders)) { - $listing = new SystemListing(); + $listing = new ExtensionDiscovery(); $this->folders = $this->getBaseDataTypeSchema(); - $this->folders += $this->getComponentNames('profile', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles'))); - $this->folders += $this->getComponentNames('module', array_keys($listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules'))); - $this->folders += $this->getComponentNames('theme', array_keys($listing->scan('/^' . 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/simpletest/lib/Drupal/simpletest/WebTestBase.php b/core/modules/simpletest/lib/Drupal/simpletest/WebTestBase.php index 1ab4418..c4eb298 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\Core\SystemListing::getProfileDirectories() + // @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 aa00dc1..b00f11c 100644 --- a/core/modules/simpletest/simpletest.module +++ b/core/modules/simpletest/simpletest.module @@ -2,7 +2,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\Page\HtmlPage; -use Drupal\Core\SystemListing; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\simpletest\TestBase; use Symfony\Component\Process\PhpExecutableFinder; @@ -461,8 +461,8 @@ function simpletest_test_get_all($module = NULL) { } else { // Select all PSR-0 classes in the Tests namespace of all modules. - $listing = new SystemListing(); - $all_data = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules'); + $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( @@ -470,8 +470,8 @@ function simpletest_test_get_all($module = NULL) { ); } else { - $all_data += $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles'); - $all_data += $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.info\.yml$/', 'themes'); + $all_data += $listing->scan('profiles', TRUE); + $all_data += $listing->scan('themes', TRUE); } $classes = array(); foreach ($all_data as $name => $data) { @@ -553,20 +553,20 @@ function simpletest_classloader_register() { $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\.yml'), - 'profile' => array('dir' => 'profiles', 'extension' => 'profile'), + 'theme_engine' => 'themes/engines', + 'module' => 'modules', + 'theme' => 'themes', + 'profile' => 'profiles', ); if ($cache = cache()->get($cid)) { $extensions = $cache->data; } else { - $listing = new SystemListing(); + $listing = new ExtensionDiscovery(); $extensions = array(); - foreach ($types as $type => $info) { - $extensions[$type] = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.' . $info['extension'] . '$/', $info['dir']); + foreach ($types as $type => $dir) { + $extensions[$type] = $listing->scan($dir, TRUE); foreach ($extensions[$type] as $name => $file) { $extensions[$type][$name] = $file->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 ae9af4e..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,7 +7,7 @@ namespace Drupal\system\Tests\Common; -use Drupal\Core\SystemListing; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\simpletest\WebTestBase; /** @@ -56,8 +56,8 @@ function testDirectoryPrecedence() { // Now scan the directories and check that the files take precedence as // expected. - $listing = new SystemListing(); - $files = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.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/system.module b/core/modules/system/system.module index d5f7453..7f3704b 100644 --- a/core/modules/system/system.module +++ b/core/modules/system/system.module @@ -8,7 +8,7 @@ use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\Cache; use Drupal\Core\Language\Language; -use Drupal\Core\SystemListing; +use Drupal\Core\Extension\ExtensionDiscovery; use Drupal\Core\Utility\ModuleInfo; use Drupal\menu_link\MenuLinkInterface; use Drupal\user\UserInterface; @@ -2580,12 +2580,12 @@ function system_get_module_info($property) { * An associative array of module information. */ function _system_rebuild_module_data() { - $listing = new SystemListing(); + $listing = new ExtensionDiscovery(); // Find modules - $modules = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.module$/', 'modules'); + $modules = $listing->scan('modules'); // Find installation profiles. - $profiles = $listing->scan('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles'); + $profiles = $listing->scan('profiles'); // Include the installation profile in modules that are loaded. if ($profile = drupal_get_profile()) { diff --git a/core/modules/update/update.module b/core/modules/update/update.module index e9ab3aa..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\Core\SystemListing::process() + * @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 4aeed08..9d93491 100644 --- a/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php +++ b/core/tests/Drupal/Tests/Core/Extension/ThemeHandlerTest.php @@ -67,7 +67,7 @@ class ThemeHandlerTest extends UnitTestCase { /** * The system listing info. * - * @var \Drupal\Core\SystemListing|\PHPUnit_Framework_MockObject_MockObject + * @var \Drupal\Core\Extension\ExtensionDiscovery|\PHPUnit_Framework_MockObject_MockObject */ protected $systemListing; @@ -101,7 +101,7 @@ protected function setUp() { $this->routeBuilder = $this->getMockBuilder('Drupal\Core\Routing\RouteBuilder') ->disableOriginalConstructor() ->getMock(); - $this->systemListing = $this->getMockBuilder('Drupal\Core\SystemListing') + $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->systemListing); 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