Issue by sun, Berdir: Extension System, Part II: ExtensionDiscovery.

8.0.x
Nathaniel Catchpole 2014-02-27 13:26:50 +00:00
parent 438494d0f1
commit ae8be3cc96
49 changed files with 1085 additions and 751 deletions

View File

@ -10,6 +10,7 @@ use Drupal\Component\Utility\Url;
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;
@ -655,7 +656,7 @@ function _drupal_request_initialize() {
function drupal_get_filename($type, $name, $filename = NULL) {
// The location of files will not change during the request, so do not use
// drupal_static().
static $files = array(), $dirs = array();
static $files = array();
// Profiles are converted into modules in system_rebuild_module_data().
// @todo Remove false-exposure of profiles as modules.
@ -667,72 +668,31 @@ function drupal_get_filename($type, $name, $filename = NULL) {
$files[$type] = array();
}
if (!empty($filename)) {
if (isset($filename)) {
$files[$type][$name] = $filename;
}
elseif (isset($files[$type][$name])) {
// nothing
}
else {
// 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.
if (\Drupal::hasService('keyvalue') && function_exists('db_query')) {
if ($type == 'module') {
if (empty($files[$type])) {
$files[$type] = \Drupal::moduleHandler()->getModuleList();
}
if (isset($files[$type][$name])) {
return $files[$type][$name];
}
}
try {
$file_list = \Drupal::state()->get('system.' . $type . '.files');
if ($file_list && isset($file_list[$name]) && file_exists(DRUPAL_ROOT . '/' . $file_list[$name])) {
$files[$type][$name] = $file_list[$name];
}
}
catch (Exception $e) {
// The keyvalue service raised an exception because the backend might
// be down. We have a fallback for this case so we hide the error
// completely.
}
elseif (!isset($files[$type][$name])) {
// If the pathname of the requested extension is not known, try to retrieve
// the list of extension pathnames from various providers, checking faster
// providers first.
// Retrieve the current module list (derived from the service container).
if ($type == 'module' && \Drupal::hasService('module_handler')) {
$files[$type] += \Drupal::moduleHandler()->getModuleList();
}
// Fallback to searching the filesystem if the database could not find the
// file or the file returned by the database is not found.
// If still unknown, retrieve the file list prepared in state by
// system_rebuild_module_data() and system_rebuild_theme_data().
if (!isset($files[$type][$name]) && \Drupal::hasService('state')) {
$files[$type] += \Drupal::state()->get('system.' . $type . '.files', array());
}
// If still unknown, perform a filesystem scan.
if (!isset($files[$type][$name])) {
// We have consistent directory naming: modules, themes...
$dir = $type . 's';
if ($type == 'theme_engine') {
$dir = 'themes/engines';
$extension = 'engine';
$listing = new ExtensionDiscovery();
// Prevent an infinite recursion by this legacy function.
if ($original_type == 'profile') {
$listing->setProfileDirectories(array());
}
elseif ($type == 'theme') {
$extension = 'info.yml';
}
// Profiles are converted into modules in system_rebuild_module_data().
// @todo Remove false-exposure of profiles as modules.
elseif ($original_type == 'profile') {
$dir = 'profiles';
$extension = 'profile';
}
else {
$extension = $type;
}
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);
foreach ($matches as $matched_name => $file) {
$files[$type][$matched_name] = $file->uri;
}
foreach ($listing->scan($original_type) as $extension_name => $file) {
$files[$type][$extension_name] = $file->uri;
}
}
}

View File

@ -20,7 +20,6 @@ use Drupal\Component\Utility\MapArray;
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;
@ -3303,19 +3302,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.
*

View File

@ -13,7 +13,7 @@ use Drupal\Core\Database\Install\TaskException;
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,21 @@ 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();
$listing->setProfileDirectories(array());
$install_state['profiles'] += $listing->scan('profile');
// 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 +542,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');
}
/**

View File

@ -10,6 +10,7 @@ use Drupal\Component\Utility\Crypt;
use Drupal\Component\Utility\Settings;
use Drupal\Core\Database\Database;
use Drupal\Core\DrupalKernel;
use Drupal\Core\Extension\ExtensionDiscovery;
/**
* Requirement severity -- Informational message only.
@ -123,22 +124,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));
@ -584,15 +580,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('module') 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);
@ -976,9 +973,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());
}

View File

@ -6,6 +6,7 @@
*/
use Drupal\Core\Cache\Cache;
use Drupal\Core\Extension\ExtensionDiscovery;
/**
* Builds a list of bootstrap modules and enabled modules and themes.
@ -298,14 +299,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('module');
$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->getPathname());
if (!empty($info) && !empty($info['required']) && $info['required']) {
$required[] = $name;
}

View File

@ -12,6 +12,7 @@ use Drupal\Component\Utility\String;
use Drupal\Component\Utility\Url;
use Drupal\Core\Config\Config;
use Drupal\Core\Language\Language;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\ExtensionNameLengthException;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Template\RenderWrapper;
@ -65,10 +66,10 @@ const RESPONSIVE_PRIORITY_LOW = 'priority-low';
/**
* Determines if a theme is available to use.
*
* @param $theme
* @param string|\Drupal\Core\Extension\Extension $theme
* Either the name of a theme or a full theme object.
*
* @return
* @return bool
* Boolean TRUE if the theme is enabled or is the site administration theme;
* FALSE otherwise.
*
@ -78,7 +79,7 @@ const RESPONSIVE_PRIORITY_LOW = 'priority-low';
* @see \Drupal\Core\Theme\ThemeAccessCheck::checkAccess().
*/
function drupal_theme_access($theme) {
if (is_object($theme)) {
if ($theme instanceof Extension) {
$theme = $theme->name;
}
return \Drupal::service('access_check.theme')->checkAccess($theme);
@ -121,19 +122,9 @@ function drupal_theme_initialize() {
*
* This function is useful to initialize a theme when no database is present.
*
* @param $theme
* An object with the following information:
* filename
* The .info.yml file for this theme. The 'path' to
* the theme will be in this file's directory. (Required)
* owner
* The path to the .theme file or the .engine file to load for
* the theme. (Required)
* stylesheet
* The primary stylesheet for the theme. (Optional)
* engine
* The name of theme engine to use. (Optional)
* @param $base_theme
* @param \Drupal\Core\Extension\Extension $theme
* The theme extension object.
* @param \Drupal\Core\Extension\Extension[] $base_theme
* An optional array of objects that represent the 'base theme' if the
* theme is meant to be derivative of another theme. It requires
* the same information as the $theme object. It should be in

View File

@ -7,6 +7,8 @@
namespace Drupal\Core\Config;
use Drupal\Core\Extension\ExtensionDiscovery;
/**
* Storage controller used by the Drupal installer.
*
@ -110,9 +112,14 @@ class InstallStorage extends FileStorage {
*/
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();
// @todo Refactor getComponentNames() to use the extension list directly.
if ($profile = drupal_get_profile()) {
$this->folders += $this->getComponentNames('profile', array($profile));
}
$listing = new ExtensionDiscovery();
$this->folders += $this->getComponentNames('module', array_keys($listing->scan('module')));
$this->folders += $this->getComponentNames('theme', array_keys($listing->scan('theme')));
}
return $this->folders;
}

View File

@ -13,6 +13,7 @@ use Drupal\Core\CoreServiceProvider;
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,28 @@ class DrupalKernel implements DrupalKernelInterface, TerminableInterface {
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();
$listing->setProfileDirectories(array());
$all_profiles = $listing->scan('profile');
$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'] : NULL;
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 $profile->getPath();
}, $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('module');
}
return isset($this->moduleData[$module]) ? $this->moduleData[$module] : FALSE;
}

View File

@ -0,0 +1,154 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator.
*/
namespace Drupal\Core\Extension\Discovery;
/**
* Filters a RecursiveDirectoryIterator to discover extensions.
*
* To ensure the best possible performance for extension discovery, this
* filter implementation hard-codes a range of assumptions about directories
* in which Drupal extensions may appear and in which not. Every unnecessary
* subdirectory tree recursion is avoided.
*
* The list of globally ignored directory names is defined in the
* RecursiveExtensionFilterIterator::$blacklist property.
*
* In addition, all 'config' directories are skipped, unless the directory path
* ends with 'modules/config', so as to still find the config module provided by
* Drupal core and still allow that module to be overridden with a custom config
* module.
*
* Lastly, ExtensionDiscovery instructs this filter to additionally skip all
* 'tests' directories at regular runtime, since just with Drupal core only, the
* discovery process yields 4x more extensions when tests are not ignored.
*
* @see ExtensionDiscovery::scan()
* @see ExtensionDiscovery::scanDirectory()
*
* @todo Use RecursiveCallbackFilterIterator instead of the $acceptTests
* parameter forwarding once PHP 5.4 is available.
*/
class RecursiveExtensionFilterIterator extends \RecursiveFilterIterator {
/**
* List of base extension type directory names to scan.
*
* Only these directory names are considered when starting a filesystem
* recursion in a search path.
*
* @var array
*/
protected $whitelist = array(
'profiles',
'modules',
'themes',
);
/**
* List of directory names to skip when recursing.
*
* These directories are globally ignored in the recursive filesystem scan;
* i.e., extensions (of all types) are not able to use any of these names,
* because their directory names will be skipped.
*
* @var array
*/
protected $blacklist = array(
// Object-oriented code subdirectories.
'src',
'lib',
'vendor',
// Front-end.
'assets',
'css',
'files',
'images',
'js',
'misc',
'templates',
// Legacy subdirectories.
'includes',
// Test subdirectories.
'fixtures',
// @todo ./tests/Drupal should be ./tests/src/Drupal
'Drupal',
);
/**
* Whether to include test directories when recursing.
*
* @var bool
*/
protected $acceptTests = FALSE;
/**
* Controls whether test directories will be scanned.
*
* @param bool $flag
* Pass FALSE to skip all test directories in the discovery. If TRUE,
* extensions in test directories will be discovered and only the global
* directory blacklist in RecursiveExtensionFilterIterator::$blacklist is
* applied.
*/
public function acceptTests($flag = FALSE) {
$this->acceptTests = $flag;
if (!$this->acceptTests) {
$this->blacklist[] = 'tests';
}
}
/**
* Overrides \RecursiveFilterIterator::getChildren().
*/
public function getChildren() {
$filter = parent::getChildren();
// Pass the $acceptTests flag forward to child iterators.
$filter->acceptTests($this->acceptTests);
return $filter;
}
/**
* Implements \FilterIterator::accept().
*/
public function accept() {
$name = $this->current()->getFilename();
// FilesystemIterator::SKIP_DOTS only skips '.' and '..', but not hidden
// directories (like '.git').
if ($name[0] == '.') {
return FALSE;
}
if ($this->isDir()) {
// If this is a subdirectory of a base search path, only recurse into the
// fixed list of expected extension type directory names. Required for
// scanning the top-level/root directory; without this condition, we would
// recurse into the whole filesystem tree that possibly contains other
// files aside from Drupal.
if ($this->current()->getSubPath() == '') {
return in_array($name, $this->whitelist, TRUE);
}
// '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';
}
// Accept the directory unless the name is blacklisted.
return !in_array($name, $this->blacklist, TRUE);
}
else {
// Only accept extension info files.
return substr($name, -9) == '.info.yml';
}
}
}

View File

@ -0,0 +1,223 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\Extension.
*/
namespace Drupal\Core\Extension;
/**
* Defines an extension (file) object.
*/
class Extension implements \Serializable {
/**
* The type of the extension (e.g., 'module').
*
* @todo Replace all uses of $type with getType() method.
*
* @var string
*/
public $type;
/**
* The relative pathname of the extension (e.g., 'core/modules/node/node.info.yml').
*
* @var string
*/
protected $pathname;
/**
* The internal name of the extension (e.g., 'node').
*
* @todo Replace all uses of $name with getName() method.
*
* @var string
*/
public $name;
/**
* The relative pathname of the main extension file (e.g., 'core/modules/node/node.module').
*
* @todo Remove this property and do not require .module/.profile files.
* @see https://drupal.org/node/340723
*
* @var string
*/
public $uri;
/**
* The filename of the main extension file (e.g., 'node.module').
*
* Note that this is not necessarily a filename but a pathname and also not
* necessarily the filename of the info file. Due to legacy code and property
* value overloading, it is either the filename of the main extension file or
* the relative pathname of the main extension file (== $uri), depending on
* whether the object has been post-processed or not.
*
* @see _system_rebuild_module_data()
* @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData()
*
* @todo Remove this property and do not require .module/.profile files.
* @see https://drupal.org/node/340723
*
* @var string
*/
public $filename;
/**
* An SplFileInfo instance for the extension's info file.
*
* Note that SplFileInfo is a PHP resource and resources cannot be serialized.
*
* @var \SplFileInfo
*/
protected $splFileInfo;
/**
* Constructs a new Extension object.
*
* @param string $type
* The type of the extension; e.g., 'module'.
* @param string $pathname
* The relative path and filename of the extension's info file; e.g.,
* 'core/modules/node/node.info.yml'.
* @param string $filename
* The filename of the main extension file; e.g., 'node.module'.
*/
public function __construct($type, $pathname, $filename) {
$this->type = $type;
$this->pathname = $pathname;
// Set legacy public properties.
$this->name = basename($pathname, '.info.yml');
$this->filename = $filename;
$this->uri = dirname($pathname) . '/' . $filename;
}
/**
* Returns the type of the extension.
*
* @return string
*/
public function getType() {
return $this->type;
}
/**
* Returns the internal name of the extension.
*
* @return string
*/
public function getName() {
return basename($this->pathname, '.info.yml');
}
/**
* Returns the relative path of the extension.
*
* @return string
*/
public function getPath() {
return dirname($this->pathname);
}
/**
* Returns the relative path and filename of the extension's info file.
*
* @return string
*/
public function getPathname() {
return $this->pathname;
}
/**
* Returns the filename of the extension's info file.
*
* @return string
*/
public function getFilename() {
return basename($this->pathname);
}
/**
* Re-routes method calls to SplFileInfo.
*
* Offers all SplFileInfo methods to consumers; e.g., $extension->getMTime().
*/
public function __call($method, array $args) {
if (!isset($this->splFileInfo)) {
$this->splFileInfo = new \SplFileInfo($this->pathname);
}
return call_user_func_array(array($this->splFileInfo, $method), $args);
}
/**
* Sets an explicit SplFileInfo object for the extension's info file.
*
* Used by ExtensionDiscovery::scanDirectory() to avoid creating additional
* PHP resources.
*
* @param \SplFileInfo $fileinfo
* A file info instance to set.
*
* @return $this
*/
public function setSplFileInfo(\SplFileInfo $fileinfo) {
$this->splFileInfo = $fileinfo;
return $this;
}
/**
* Implements Serializable::serialize().
*
* Serializes the Extension object in the most optimized way.
*/
public function serialize() {
$data = array(
'type' => $this->type,
'pathname' => $this->pathname,
);
// Include legacy public properties.
// @todo Remove this property and do not require .module/.profile files.
// @see https://drupal.org/node/340723
// @see Extension::$filename
$data['filename'] = basename($this->uri);
// @todo ThemeHandler::listInfo(), ThemeHandler::rebuildThemeData(), and
// system_list() are adding custom properties to the Extension object.
$info = new \ReflectionObject($this);
foreach ($info->getProperties(\ReflectionProperty::IS_PUBLIC) as $property) {
$data[$property->getName()] = $property->getValue($this);
}
return serialize($data);
}
/**
* Implements Serializable::unserialize().
*/
public function unserialize($data) {
$data = unserialize($data);
$this->type = $data['type'];
$this->pathname = $data['pathname'];
// Restore legacy public properties.
// @todo Remove these properties and do not require .module/.profile files.
// @see https://drupal.org/node/340723
// @see Extension::$filename
$this->name = basename($data['pathname'], '.info.yml');
$this->uri = dirname($data['pathname']) . '/' . $data['filename'];
$this->filename = $data['filename'];
// @todo ThemeHandler::listInfo(), ThemeHandler::rebuildThemeData(), and
// system_list() are adding custom properties to the Extension object.
foreach ($data as $property => $value) {
if (!isset($this->$property)) {
$this->$property = $value;
}
}
}
}

View File

@ -0,0 +1,406 @@
<?php
/**
* @file
* Contains \Drupal\Core\Extension\ExtensionDiscovery.
*/
namespace Drupal\Core\Extension;
use Drupal\Component\Utility\Settings;
use Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator;
/**
* Discovers available extensions in the filesystem.
*/
class ExtensionDiscovery {
/**
* Origin directory weight: Core.
*/
const ORIGIN_CORE = 0;
/**
* Origin directory weight: Installation profile.
*/
const ORIGIN_PROFILE = 1;
/**
* Origin directory weight: sites/all.
*/
const ORIGIN_SITES_ALL = 2;
/**
* Origin directory weight: Site-wide directory.
*/
const ORIGIN_ROOT = 3;
/**
* Origin directory weight: Parent site directory of a test site environment.
*/
const ORIGIN_PARENT_SITE = 4;
/**
* Origin directory weight: Site-specific directory.
*/
const ORIGIN_SITE = 5;
/**
* Regular expression to match PHP function names.
*
* @see http://php.net/manual/functions.user-defined.php
*/
const PHP_FUNCTION_PATTERN = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/';
/**
* InfoParser instance for parsing .info.yml files.
*
* @var \Drupal\Core\Extension\InfoParser
*/
protected $infoParser;
/**
* Previously discovered files keyed by origin directory and extension type.
*
* @var array
*/
protected static $files = array();
/**
* List of installation profile directories to additionally scan.
*
* @var array
*/
protected $profileDirectories;
/**
* Discovers available extensions of a given type.
*
* Finds all extensions (modules, themes, etc) that exist on the site. It
* searches in several locations. For instance, to discover all available
* modules:
* @code
* $listing = new ExtensionDiscovery();
* $modules = $listing->scan('module');
* @endcode
*
* The following directories will be searched (in the order stated):
* - the core directory; i.e., /core
* - the installation profile directory; e.g., /core/profiles/standard
* - the legacy site-wide directory; i.e., /sites/all
* - the site-wide directory; i.e., /
* - the site-specific directory; e.g., /sites/example.com
*
* 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 $type
* The extension type to search for. One of 'profile', 'module', 'theme', or
* 'theme_engine'.
* @param bool $include_tests
* (optional) Whether to explicitly include or exclude test extensions. By
* default, test extensions are only discovered when in a test environment.
*
* @return \Drupal\Core\Extension\Extension[]
* An associative array of Extension objects, keyed by extension name.
*/
public function scan($type, $include_tests = NULL) {
// Determine the installation profile directories to scan for extensions,
// unless explicit profile directories have been set.
if (!isset($this->profileDirectories)) {
$this->setProfileDirectoriesFromSettings();
}
// Search the core directory.
$searchdirs[static::ORIGIN_CORE] = 'core';
// Search the legacy sites/all directory.
$searchdirs[static::ORIGIN_SITES_ALL] = 'sites/all';
// Search for contributed and custom extensions in top-level directories.
// The scan uses a whitelist to limit recursion to the expected extension
// type specific directory names only.
$searchdirs[static::ORIGIN_ROOT] = '';
// Simpletest uses the regular built-in multi-site functionality of Drupal
// for running web tests. As a consequence, extensions of the parent site
// located in a different site-specific directory are not discovered in a
// test site environment, because the site directories are not the same.
// Therefore, add the site directory of the parent site to the search paths,
// so that contained extensions are still discovered.
// @see \Drupal\simpletest\WebTestBase::setUp()
if ($parent_site = Settings::getSingleton()->get('test_parent_site')) {
$searchdirs[static::ORIGIN_PARENT_SITE] = $parent_site;
}
// Search the site-specific directory.
$searchdirs[static::ORIGIN_SITE] = conf_path();
// Unless an explicit value has been passed, manually check whether we are
// in a test environment, in which case test extensions must be included.
if (!isset($include_tests)) {
$include_tests = (bool) drupal_valid_test_ua();
}
$files = array();
foreach ($searchdirs as $dir) {
// Discover all extensions in the directory, unless we did already.
if (!isset(static::$files[$dir][$include_tests])) {
static::$files[$dir][$include_tests] = $this->scanDirectory($dir, $include_tests);
}
// Only return extensions of the requested type.
if (isset(static::$files[$dir][$include_tests][$type])) {
$files += static::$files[$dir][$include_tests][$type];
}
}
// Sort the discovered extensions by their originating directories and,
// if applicable, filter out extensions that do not belong to the current
// installation profiles.
$origin_weights = array_flip($searchdirs);
$origins = array();
$profiles = array();
foreach ($files as $key => $file) {
// If the extension does not belong to a profile, just apply the weight
// of the originating directory.
if (strpos($file->getSubPath(), 'profiles') !== 0) {
$origins[$key] = $origin_weights[$file->origin];
$profiles[$key] = NULL;
}
// If the extension belongs to a profile but no profile directories are
// defined, then we are scanning for installation profiles themselves.
// In this case, profiles are sorted by origin only.
elseif (empty($this->profileDirectories)) {
$origins[$key] = static::ORIGIN_PROFILE;
$profiles[$key] = NULL;
}
else {
// Apply the weight of the originating profile directory.
foreach ($this->profileDirectories as $weight => $profile_path) {
if (strpos($file->getPath(), $profile_path) === 0) {
$origins[$key] = static::ORIGIN_PROFILE;
$profiles[$key] = $weight;
continue 2;
}
}
// If we end up here, then the extension does not belong to any of the
// current installation profile directories, so remove it.
unset($files[$key]);
}
}
// Now sort the extensions by origin and installation profile(s).
// The result of this multisort can be depicted like the following matrix,
// whereas the first integer is the weight of the originating directory and
// the second is the weight of the originating installation profile:
// 0 core/modules/node/node.module
// 1 0 profiles/parent_profile/modules/parent_module/parent_module.module
// 1 1 core/profiles/testing/modules/compatible_test/compatible_test.module
// 2 sites/all/modules/common/common.module
// 3 modules/devel/devel.module
// 4 sites/default/modules/custom/custom.module
array_multisort($origins, SORT_ASC, $profiles, SORT_ASC, $files);
// Process and return the sorted and filtered list of extensions keyed by
// extension name.
return $this->process($files);
}
/**
* Sets installation profile directories based on current site settings.
*
* @return $this
*/
public function setProfileDirectoriesFromSettings() {
$this->profileDirectories = 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) {
$this->profileDirectories[] = drupal_get_path('profile', $testing_profile);
}
}
// In case both profile directories contain the same extension, the actual
// profile always has precedence.
if ($profile) {
$this->profileDirectories[] = drupal_get_path('profile', $profile);
}
return $this;
}
/**
* Gets the installation profile directories to be scanned.
*
* @return array
* A list of installation profile directory paths relative to the system
* root directory.
*/
public function getProfileDirectories() {
return $this->profileDirectories;
}
/**
* Sets explicit profile directories to scan.
*
* @param array $paths
* A list of installation profile directory paths relative to the system
* root directory (without trailing slash) to search for extensions.
*
* @return $this
*/
public function setProfileDirectories(array $paths = NULL) {
$this->profileDirectories = $paths;
return $this;
}
/**
* Processes the filtered and sorted list of extensions.
*
* Extensions discovered in later search paths override earlier, unless they
* are not compatible with the current version of Drupal core.
*
* @param \Drupal\Core\Extension\Extension[] $all_files
* The sorted list of all extensions that were found.
*
* @return \Drupal\Core\Extension\Extension[]
* The filtered list of extensions, keyed by extension name.
*/
protected function process(array $all_files) {
$files = array();
// Duplicate files found in later search directories take precedence over
// earlier ones; they replace the extension in the existing $files array.
// The exception to this is if the later extension is not compatible with
// the current version of Drupal core, which may occur during upgrades when
// e.g. new modules were introduced in core while older contrib modules with
// the same name still exist in a later search path.
foreach ($all_files as $file) {
if (isset($files[$file->name])) {
// Skip the extension if it is incompatible with Drupal core.
$info = $this->getInfoParser()->parse($file->getPathname());
if (!isset($info['core']) || $info['core'] != \Drupal::CORE_COMPATIBILITY) {
continue;
}
}
$files[$file->name] = $file;
}
return $files;
}
/**
* Recursively scans a base directory for the requested extension type.
*
* @param string $dir
* A relative base directory path to scan, without trailing slash.
* @param bool $include_tests
* Whether to include test extensions. If FALSE, all 'tests' directories are
* excluded in the search.
*
* @return array
* An associative array whose keys are extension type names and whose values
* are associative arrays of \Drupal\Core\Extension\Extension objects, keyed
* by absolute path name.
*
* @see \Drupal\Core\Extension\Discovery\RecursiveExtensionFilterIterator
*/
protected function scanDirectory($dir, $include_tests) {
$files = array();
// In order to scan top-level directories, absolute directory paths have to
// be used (which also improves performance, since any configured PHP
// include_paths will not be consulted). Retain the relative originating
// directory being scanned, so relative paths can be reconstructed below
// (all paths are expected to be relative to DRUPAL_ROOT).
$dir_prefix = ($dir == '' ? '' : "$dir/");
$absolute_dir = ($dir == '' ? DRUPAL_ROOT : DRUPAL_ROOT . "/$dir");
if (!is_dir($absolute_dir)) {
return $files;
}
// Use Unix paths regardless of platform, skip dot directories, and return
// the RecursiveDirectoryIterator instance to have access to getSubPath(),
// since SplFileInfo does not support relative paths.
$flags = \FilesystemIterator::UNIX_PATHS;
$flags |= \FilesystemIterator::SKIP_DOTS;
$flags |= \FilesystemIterator::CURRENT_AS_SELF;
$directory_iterator = new \RecursiveDirectoryIterator($absolute_dir, $flags);
// Filter the recursive scan to discover extensions only.
// Important: Without a RecursiveFilterIterator, RecursiveDirectoryIterator
// would recurse into the entire filesystem directory tree without any kind
// of limitations.
$filter = new RecursiveExtensionFilterIterator($directory_iterator);
$filter->acceptTests($include_tests);
// The actual recursive filesystem scan is only invoked by instantiating the
// RecursiveIteratorIterator.
$iterator = new \RecursiveIteratorIterator($filter,
\RecursiveIteratorIterator::LEAVES_ONLY,
// Suppress filesystem errors in case a directory cannot be accessed.
\RecursiveIteratorIterator::CATCH_GET_CHILD
);
foreach ($iterator as $key => $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.
$type = FALSE;
$file = $fileinfo->openFile('r');
while (!$type && !$file->eof()) {
preg_match('@^type:\s*(\w+)\s*$@', $file->fgets(), $matches);
if (isset($matches[1])) {
$type = $matches[1];
}
}
if (empty($type)) {
continue;
}
$name = $fileinfo->getBasename('.info.yml');
$pathname = $dir_prefix . $fileinfo->getSubPathname();
// Supply main extension filename being used throughout Drupal.
// For themes, the filename is the info file itself.
if ($type == 'theme') {
$filename = $fileinfo->getFilename();
}
// For theme engines, the file extension is .engine.
elseif ($type == 'theme_engine') {
$filename = $name . '.engine';
}
// Otherwise, it is .module/.profile; i.e., the extension type.
else {
$filename = $name . '.' . $type;
}
$extension = new Extension($type, $pathname, $filename);
// Inject the existing RecursiveDirectoryIterator object to avoid
// unnecessary creation of additional SplFileInfo resources.
$extension->setSplFileInfo($fileinfo);
// Track the originating directory for sorting purposes.
$extension->origin = $dir;
$files[$type][$key] = $extension;
}
return $files;
}
/**
* Returns a parser for .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;
}
}

View File

@ -11,9 +11,8 @@ use Drupal\Component\Utility\String;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Parser;
/**
* Class that parses Drupal module's, theme's and profile's .info.yml files.
* Parses extension .info.yml files.
*/
class InfoParser implements InfoParserInterface {
@ -22,7 +21,7 @@ class InfoParser implements InfoParserInterface {
*
* @var array
*/
protected $parsedInfos = array();
protected static $parsedInfos = array();
/**
* Symfony YAML parser object.
@ -35,29 +34,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 +79,7 @@ class InfoParser implements InfoParserInterface {
* An array of required keys.
*/
protected function getRequiredKeys() {
return array('name', 'type');
return array('type', 'core', 'name');
}
}

View File

@ -599,9 +599,10 @@ class ModuleHandler implements ModuleHandlerInterface {
$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();
// Clear the static cache of system_rebuild_module_data() to pick up the
// new module, since it merges the installation status of modules into
// its statically cached list.
drupal_static_reset('system_rebuild_module_data');
// Update the kernel to include it.
// This reboots the kernel to register the module's bundle and its
@ -742,10 +743,10 @@ class ModuleHandler implements ModuleHandlerInterface {
// Remove any potential cache bins provided by the module.
$this->removeCacheBins($module);
// Refresh the system list to exclude the uninstalled modules.
// @todo Only needed to rebuild theme info.
// @see system_list_reset()
system_list_reset();
// Clear the static cache of system_rebuild_module_data() to pick up the
// new module, since it merges the installation status of modules into
// its statically cached list.
drupal_static_reset('system_rebuild_module_data');
// Clear the entity info cache.
entity_info_cache_clear();

View File

@ -71,7 +71,8 @@ interface ModuleHandlerInterface {
*
* @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 +81,7 @@ interface ModuleHandlerInterface {
* - 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);

View File

@ -13,7 +13,6 @@ use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ConfigInstallerInterface;
use Drupal\Core\Routing\RouteBuilder;
use Drupal\Core\SystemListingInfo;
/**
* Default theme handler using the config system for enabled/disabled themes.
@ -87,11 +86,11 @@ class ThemeHandler implements ThemeHandlerInterface {
protected $routeBuilder;
/**
* The system listing info
* An extension discovery instance.
*
* @var \Drupal\Core\SystemListingInfo
* @var \Drupal\Core\Extension\ExtensionDiscovery
*/
protected $systemListingInfo;
protected $extensionDiscovery;
/**
* Constructs a new ThemeHandler.
@ -110,17 +109,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 $extension_discovery
* (optional) A extension discovery instance (for unit tests).
*/
public function __construct(ConfigFactoryInterface $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(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend, InfoParserInterface $info_parser, ConfigInstallerInterface $config_installer = NULL, RouteBuilder $route_builder = NULL, ExtensionDiscovery $extension_discovery = 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->extensionDiscovery = $extension_discovery;
}
/**
@ -243,24 +242,9 @@ class ThemeHandler implements ThemeHandlerInterface {
* {@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->getExtensionDiscovery();
$themes = $listing->scan('theme');
$engines = $listing->scan('theme_engine');
// Set defaults for theme info.
$defaults = array(
@ -288,20 +272,15 @@ class ThemeHandler implements ThemeHandlerInterface {
// 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;
}
$themes[$key]->info = $this->infoParser->parse($theme->getPathname()) + $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->getType().
$type = 'theme';
$this->moduleHandler->alter('system_info', $themes[$key]->info, $themes[$key], $type);
@ -316,12 +295,10 @@ class ThemeHandler implements ThemeHandlerInterface {
$themes[$key]->template = TRUE;
}
// Prefix stylesheets and scripts with module path.
$path = dirname($theme->uri);
// Prefix stylesheets, scripts, and screenshot with theme path.
$path = $theme->getPath();
$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'];
}
@ -330,7 +307,7 @@ class ThemeHandler implements ThemeHandlerInterface {
// Now that we've established all our master themes, go back and fill in
// data for sub-themes.
foreach ($sub_themes as $key) {
$themes[$key]->base_themes = $this->doGetBaseThemes($themes, $key);
$themes[$key]->base_themes = $this->getBaseThemes($themes, $key);
// Don't proceed if there was a problem with the root base theme.
if (!current($themes[$key]->base_themes)) {
continue;
@ -436,23 +413,23 @@ class ThemeHandler implements ThemeHandlerInterface {
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 an extension discovery object.
*
* @return \Drupal\Core\SystemListingInfo
* The system listing object.
* @return \Drupal\Core\Extension\ExtensionDiscovery
* The extension discovery object.
*/
protected function getSystemListingInfo() {
if (!isset($this->systemListingInfo)) {
$this->systemListingInfo = new SystemListingInfo();
protected function getExtensionDiscovery() {
if (!isset($this->extensionDiscovery)) {
$this->extensionDiscovery = new ExtensionDiscovery();
}
return $this->systemListingInfo;
return $this->extensionDiscovery;
}
/**

View File

@ -37,7 +37,7 @@ interface ThemeHandlerInterface {
* Retrieved from the database, if available and the site is not in
* maintenance mode; otherwise compiled freshly from the filesystem.
*
* @return array
* @return \Drupal\Core\Extension\Extension[]
* An associative array of the currently available themes. The keys are the
* themes' machine names and the values are objects having the following
* properties:
@ -81,10 +81,10 @@ interface ThemeHandlerInterface {
public function reset();
/**
* Helper function to scan and collect theme .info.yml data and their engines.
* Scans and collects theme extension data and their engines.
*
* @return array
* An associative array of themes information.
* @return \Drupal\Core\Extension\Extension[]
* An associative array of theme extensions.
*/
public function rebuildThemeData();
@ -94,7 +94,7 @@ interface ThemeHandlerInterface {
* Themes can inherit templates and function implementations from earlier
* themes.
*
* @param array $themes
* @param \Drupal\Core\Extension\Extension[] $themes
* An array of available themes.
* @param string $theme
* The name of the theme whose base we are looking for.

View File

@ -46,8 +46,6 @@ class UpdateModuleHandler extends ModuleHandler {
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_type_build':
case 'entity_load':

View File

@ -1,220 +0,0 @@
<?php
/**
* @file
* Definition of Drupal\Core\SystemListing.
*/
namespace Drupal\Core;
use Drupal\Component\Utility\Settings;
/**
* Returns information about system object files (modules, themes, etc.).
*
* This class requires the list of profiles to be scanned (see
* \Drupal\Core\SystemListing::scan) to be passed into the constructor. Also,
* info files are not parsed.
*/
class SystemListing {
/**
* Construct this listing object.
*
* @param array $profiles
* A list of profiles to search their directories for in addition to the
* default directories.
*/
function __construct($profiles = array()) {
$this->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;
// Simpletest uses the regular built-in multi-site functionality of Drupal
// for running web tests. As a consequence, extensions of the parent site
// located in a different site-specific directory are not discovered in a
// test site environment, because the site directories are not the same.
// Therefore, add the site directory of the parent site to the search paths,
// so that contained extensions are still discovered.
// @see \Drupal\simpletest\WebTestBase::setUp()
if ($parent_site = Settings::getSingleton()->get('test_parent_site')) {
$searchdir[] = $parent_site;
}
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) {
}
}

View File

@ -1,79 +0,0 @@
<?php
/**
* @file
* Definition of Drupal\Core\SystemListingInfo.
*/
namespace Drupal\Core;
/**
* Returns information about system object files (modules, themes, etc.).
*
* This class finds the profile directories itself and also parses info files.
*/
class SystemListingInfo extends SystemListing {
/**
* Overrides Drupal\Core\SystemListing::profiles().
*/
protected function profiles($directory) {
$searchdir = array();
// The 'core/profiles' directory contains pristine collections of modules
// and themes as provided by a distribution. It is pristine in the same
// way that the 'core/modules' directory is pristine for core; users
// should avoid any modification by using the top-level or sites/<domain>
// 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');
}
}

View File

@ -29,20 +29,20 @@ class ProjectInfo {
* files for each module or theme, which is important data which is used when
* deciding if the available update data should be invalidated.
*
* @param $projects
* @param array $projects
* Reference to the array of project data of what's installed on this site.
* @param $list
* @param \Drupal\Core\Extension\Extension[] $list
* Array of data to process to add the relevant info to the $projects array.
* @param $project_type
* @param string $project_type
* The kind of data in the list. Can be 'module' or 'theme'.
* @param $status
* @param bool $status
* Boolean that controls what status (enabled or disabled) to process out of
* the $list and add to the $projects array.
* @param $additional_whitelist
* @param array $additional_whitelist
* (optional) Array of additional elements to be collected from the .info.yml
* file. Defaults to array().
*/
function processInfoList(&$projects, $list, $project_type, $status, $additional_whitelist = array()) {
function processInfoList(array &$projects, array $list, $project_type, $status, array $additional_whitelist = array()) {
foreach ($list as $file) {
// A disabled or hidden base theme of an enabled sub-theme still has all
// of its code run by the sub-theme, so we include it in our "enabled"
@ -178,13 +178,11 @@ class ProjectInfo {
/**
* Determines what project a given file object belongs to.
*
* @param $file
* A file object as returned by system_get_files_database().
* @param \Drupal\Core\Extension\Extension $file
* An extension object.
*
* @return
* @return string
* The canonical project short name.
*
* @see system_get_files_database()
*/
function getProjectName($file) {
$project_name = '';

View File

@ -5,14 +5,6 @@
* Provide test blocks.
*/
/**
* 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().
*/

View File

@ -15,13 +15,6 @@ use Drupal\breakpoint\Entity\Breakpoint;
*/
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',

View File

@ -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

View File

@ -1,13 +0,0 @@
<?php
/**
* @file
* Test breakpoint functionality for breakpoints provided by themes.
*/
/**
* Implements hook_system_theme_info().
*/
function breakpoint_theme_test_system_theme_info() {
$themes['breakpoint_test_theme'] = drupal_get_path('module', 'breakpoint_theme_test') . '/themes/breakpoint_test_theme/breakpoint_test_theme.info.yml';
return $themes;
}

View File

@ -8,6 +8,7 @@
namespace Drupal\config_test;
use Drupal\Core\Config\InstallStorage;
use Drupal\Core\Extension\ExtensionDiscovery;
/**
* Tests configuration of profiles, modules and themes.
@ -22,9 +23,11 @@ class TestInstallStorage extends InstallStorage {
*/
protected function getAllFolders() {
if (!isset($this->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')));
// @todo Refactor getComponentNames() to use the extension list directly.
$listing = new ExtensionDiscovery();
$this->folders = $this->getComponentNames('profile', array_keys($listing->scan('profile')));
$this->folders += $this->getComponentNames('module', array_keys($listing->scan('module')));
$this->folders += $this->getComponentNames('theme', array_keys($listing->scan('theme')));
}
return $this->folders;
}

View File

@ -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,12 @@ class TestSchemaStorage extends SchemaStorage {
*/
protected function getAllFolders() {
if (!isset($this->folders)) {
// @todo Refactor getComponentNames() to use the extension list directly.
$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('profile')));
$this->folders += $this->getComponentNames('module', array_keys($listing->scan('module')));
$this->folders += $this->getComponentNames('theme', array_keys($listing->scan('theme')));
}
return $this->folders;
}

View File

@ -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;
}

View File

@ -162,7 +162,7 @@ function locale_translation_project_list() {
* project = myproject" in its .info.yml file. This function will add a project
* "myproject" to the info data.
*
* @param array $data
* @param \Drupal\Core\Extension\Extension[] $data
* Array of .info.yml file data.
* @param string $type
* The project type. i.e. module, theme.

View File

@ -792,7 +792,7 @@ abstract class WebTestBase extends TestBase {
'required' => TRUE,
);
// Add the parent profile's search path to the child site's search paths.
// @see drupal_system_listing()
// @see \Drupal\Core\Extension\ExtensionDiscovery::getProfileDirectories()
$settings['conf']['simpletest.settings']['parent_profile'] = (object) array(
'value' => $this->originalProfile,
'required' => TRUE,

View File

@ -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;
@ -422,14 +423,13 @@ function simpletest_log_read($test_id, $database_prefix, $test_class) {
* @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
@ -439,24 +439,27 @@ 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('module', 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('profile', TRUE);
$all_data += $listing->scan('theme', 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';
$tests_dir = DRUPAL_ROOT . '/' . $data->getPath() . '/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/';
$basedir = DRUPAL_ROOT . '/' . $data->getPath() . '/lib/';
foreach ($files as $file) {
// Convert the file name into the namespaced class name.
$replacements = array(
@ -524,23 +527,36 @@ function simpletest_test_get_all($module = NULL) {
* Registers namespaces for disabled modules.
*/
function simpletest_classloader_register() {
// @see drupal_get_filename()
// Use the same cache prefix as simpletest_test_get_all().
$cid = "simpletest::all";
$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',
'module',
'theme',
'profile',
);
$classloader = drupal_classloader();
if ($cache = \Drupal::cache()->get($cid)) {
$extensions = $cache->data;
}
else {
$listing = new ExtensionDiscovery();
$extensions = array();
foreach ($types as $type) {
foreach ($listing->scan($type, TRUE) as $name => $file) {
$extensions[$type][$name] = $file->uri;
}
}
\Drupal::cache()->set($cid, $extensions);
}
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');
$classloader = drupal_classloader();
foreach ($types as $type) {
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);
}
}

View File

@ -47,15 +47,6 @@ class GetFilenameUnitTest extends UnitTestBase {
// a fixed location and naming.
$this->assertIdentical(drupal_get_filename('profile', 'standard'), 'core/profiles/standard/standard.profile', 'Retrieve installation profile location.');
// When a file is not found in the database cache, drupal_get_filename()
// searches several locations on the filesystem, including the core/
// directory. We use the '.script' extension below because this is a
// non-existent filetype that will definitely not exist in the database.
// Since there is already a core/scripts directory, drupal_get_filename()
// will automatically check there for 'script' files, just as it does
// for (e.g.) 'module' files in core/modules.
$this->assertIdentical(drupal_get_filename('script', 'test'), 'core/scripts/test/test.script');
// Searching for an item that does not exist returns NULL.
$this->assertNull(drupal_get_filename('module', uniqid("", TRUE)), 'Searching for an item that does not exist returns NULL.');
}

View File

@ -7,12 +7,13 @@
namespace Drupal\system\Tests\Common;
use Drupal\simpletest\WebTestBase;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\simpletest\DrupalUnitTestBase;
/**
* Tests scanning system directories in drupal_system_listing().
*/
class SystemListingTest extends WebTestBase {
class SystemListingTest extends DrupalUnitTestBase {
public static function getInfo() {
return array(
'name' => 'Drupal system listing',
@ -55,11 +56,16 @@ class SystemListingTest extends WebTestBase {
// Now scan the directories and check that the files take precedence as
// expected.
$files = drupal_system_listing('/\.module$/', 'modules');
$listing = new ExtensionDiscovery();
$listing->setProfileDirectories(array('core/profiles/testing'));
$files = $listing->scan('module');
foreach ($expected_directories as $module => $directories) {
$expected_directory = array_shift($directories);
$expected_filename = "$expected_directory/$module/$module.module";
$this->assertEqual($files[$module]->uri, $expected_filename, format_string('Module @module was found at @filename.', array('@module' => $module, '@filename' => $expected_filename)));
$expected_uri = "$expected_directory/$module/$module.module";
$this->assertEqual($files[$module]->uri, $expected_uri, format_string('Module @actual was found at @expected.', array(
'@actual' => $files[$module]->uri,
'@expected' => $expected_uri,
)));
}
}
}

View File

@ -72,7 +72,7 @@ class InfoParserUnitTest extends DrupalUnitTestBase {
$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 @@ class InfoParserUnitTest extends DrupalUnitTestBase {
$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);
}

View File

@ -254,9 +254,12 @@ class ThemeTest extends WebTestBase {
* Test that themes can't be enabled when the base theme or engine is missing.
*/
function testInvalidTheme() {
\Drupal::moduleHandler()->install(array('theme_page_test'));
// theme_page_test_system_info_alter() un-hides all hidden themes.
$this->container->get('module_handler')->install(array('theme_page_test'));
// Clear the system_list() and theme listing cache to pick up the change.
$this->container->get('theme_handler')->reset();
$this->drupalGet('admin/appearance');
$this->assertText(t('This theme requires the base theme @base_theme to operate correctly.', array('@base_theme' => 'not_real_test_basetheme')), 'Invalid base theme check succeeded.');
$this->assertText(t('This theme requires the theme engine @theme_engine to operate correctly.', array('@theme_engine' => 'not_real_engine')), 'Invalid theme engine check succeeded.');
$this->assertText(t('This theme requires the base theme @base_theme to operate correctly.', array('@base_theme' => 'not_real_test_basetheme')));
$this->assertText(t('This theme requires the theme engine @theme_engine to operate correctly.', array('@theme_engine' => 'not_real_engine')));
}
}

View File

@ -7,6 +7,7 @@
namespace Drupal\system\Tests\Theme;
use Drupal\Core\Extension\ExtensionDiscovery;
use Drupal\comment\CommentInterface;
use Drupal\simpletest\WebTestBase;
@ -28,7 +29,7 @@ class EntityFilteringThemeTest extends WebTestBase {
/**
* A list of all available themes.
*
* @var array
* @var \Drupal\Core\Extension\Extension[]
*/
protected $themes;
@ -81,9 +82,10 @@ class EntityFilteringThemeTest extends WebTestBase {
function setUp() {
parent::setUp();
// Enable all available themes for testing.
$this->themes = array_keys(list_themes());
theme_enable($this->themes);
// Enable all available non-testing themes.
$listing = new ExtensionDiscovery();
$this->themes = $listing->scan('theme', FALSE);
theme_enable(array_keys($this->themes));
// Create a test user.
$this->user = $this->drupalCreateUser(array('access content', 'access user profiles'));
@ -133,9 +135,9 @@ class EntityFilteringThemeTest extends WebTestBase {
);
// Check each path in all available themes.
foreach ($this->themes as $theme) {
foreach ($this->themes as $name => $theme) {
\Drupal::config('system.theme')
->set('default', $theme)
->set('default', $name)
->save();
foreach ($paths as $path) {
$this->drupalGet($path);

View File

@ -1052,25 +1052,6 @@ function hook_system_breadcrumb_alter(array &$breadcrumb, array $attributes, arr
$breadcrumb[] = Drupal::l(t('Text'), 'example_route_name');
}
/**
* 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
*

View File

@ -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\block\BlockPluginInterface;
use Drupal\menu_link\MenuLinkInterface;
@ -1520,22 +1521,23 @@ function system_get_info($type, $name = NULL) {
/**
* Helper function to scan and collect module .info.yml data.
*
* @return
* @return \Drupal\Core\Extension\Extension[]
* 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('module');
// Find installation profiles.
$profiles = drupal_system_listing('/^' . DRUPAL_PHP_FUNCTION_PATTERN . '\.profile$/', 'profiles', 'name', 0);
$profiles = $listing->scan('profile');
// 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(
@ -1553,17 +1555,11 @@ function _system_rebuild_module_data() {
$modules[$key]->filename = $module->uri;
// Look for the info file.
$module->info = \Drupal::service('info_parser')->parse(dirname($module->uri) . '/' . $module->name . '.info.yml');
// Skip modules/profiles that don't provide info or have the wrong type.
if (empty($module->info) || !isset($module->info['type']) || !in_array($module->info['type'], array('module', 'profile'))) {
unset($modules[$key]);
continue;
}
$module->info = \Drupal::service('info_parser')->parse($module->getPathname());
// Add the info file modification time, so it becomes available for
// contributed modules to use for ordering module lists.
$module->info['mtime'] = filemtime(dirname($module->uri) . '/' . $module->name . '.info.yml');
$module->info['mtime'] = $module->getMTime();
// Merge in defaults and save.
$modules[$key]->info = $module->info + $defaults;
@ -1576,6 +1572,7 @@ function _system_rebuild_module_data() {
// Invoke hook_system_info_alter() to give installed modules a chance to
// modify the data in the .info.yml files if necessary.
// @todo Remove $type argument, obsolete with $module->getType().
$type = 'module';
\Drupal::moduleHandler()->alter('system_info', $modules[$key]->info, $modules[$key], $type);
}
@ -1587,7 +1584,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
@ -1603,9 +1600,9 @@ function _system_rebuild_module_data() {
/**
* Ensures that dependencies of required modules are also required.
*
* @param \stdClass $module
* @param \Drupal\Core\Extension\Extension $module
* The module info.
* @param array $modules
* @param \Drupal\Core\Extension\Extension[] $modules
* The array of all module info.
*/
function _system_rebuild_module_data_ensure_required($module, &$modules) {
@ -1624,7 +1621,7 @@ function _system_rebuild_module_data_ensure_required($module, &$modules) {
/**
* Rebuild, save, and return data about all currently available modules.
*
* @return
* @return \Drupal\Core\Extension\Extension[]
* Array of all available modules and their data.
*/
function system_rebuild_module_data() {
@ -1636,14 +1633,13 @@ function system_rebuild_module_data() {
$modules = _system_rebuild_module_data();
$files = array();
ksort($modules);
// Add name, status, weight, and schema version.
// Add status, weight, and schema version.
$installed_modules = (array) \Drupal::config('system.module')->get('enabled');
foreach ($modules as $module => $record) {
$record->name = $module;
$record->weight = isset($installed_modules[$module]) ? $installed_modules[$module] : 0;
$record->status = (int) isset($installed_modules[$module]);
$record->schema_version = SCHEMA_UNINSTALLED;
$files[$module] = $record->filename;
foreach ($modules as $name => $module) {
$module->weight = isset($installed_modules[$name]) ? $installed_modules[$name] : 0;
$module->status = (int) isset($installed_modules[$name]);
$module->schema_version = SCHEMA_UNINSTALLED;
$files[$name] = $module->uri;
}
$modules = \Drupal::moduleHandler()->buildModuleDependencies($modules);
$modules_cache = $modules;
@ -1658,7 +1654,7 @@ function system_rebuild_module_data() {
/**
* Helper function to scan and collect theme .info.yml data and their engines.
*
* @return
* @return \Drupal\Core\Extension\Extension[]
* An associative array of themes information.
*
* @see \Drupal\Core\Extension\ThemeHandlerInterface::rebuildThemeData()
@ -1673,7 +1669,7 @@ function _system_rebuild_theme_data() {
/**
* Rebuild, save, and return data about all currently available themes.
*
* @return
* @return \Drupal\Core\Extension\Extension[]
* Array of all available themes and their data.
*/
function system_rebuild_theme_data() {
@ -1690,7 +1686,7 @@ function system_rebuild_theme_data() {
$files = array();
foreach ($themes as $name => $theme) {
$theme->status = (int) isset($enabled_themes[$name]);
$files[$name] = $theme->filename;
$files[$name] = $theme->uri;
}
// Replace last known theme data state.
// @todo Obsolete with proper installation status for themes.

View File

@ -1,3 +1,4 @@
core: 8.x
name: common_test
type: module
description: 'testing info file parsing'

View File

@ -1,6 +1,5 @@
# info.yml for testing missing name, description, and type keys.
package: Core
version: VERSION
core: 8.x
dependencies:
- field

View File

@ -12,14 +12,6 @@ use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\CloseDialogCommand;
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().

View File

@ -1,7 +0,0 @@
name: 'Serialization test module'
type: module
description: 'Support module for serialization tests.'
package: Testing
version: VERSION
core: 8.x
hidden: true

View File

@ -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;
}

View File

@ -52,17 +52,6 @@ function theme_test_theme($existing, $type, $theme, $path) {
return $items;
}
/**
* 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_preprocess_HOOK() for HTML document templates.
*/

View File

@ -166,7 +166,7 @@ class UpdateManager implements UpdateManagerInterface {
$this->keyValueStore->delete($key);
}
else {
$projects = $this->keyValueStore->get($key);
$projects = $this->keyValueStore->get($key, array());
}
return $projects;
}

View File

@ -8,15 +8,6 @@ use Symfony\Component\HttpFoundation\BinaryFileResponse;
* Module for testing Update Manager functionality.
*/
/**
* 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().
*

View File

@ -596,7 +596,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
* @see _system_rebuild_module_data()
*/
function update_verify_update_archive($project, $archive_file, $directory) {

View File

@ -7,6 +7,7 @@
namespace Drupal\Tests\Core\Extension;
use Drupal\Core\Extension\Extension;
use Drupal\Core\Extension\InfoParser;
use Drupal\Core\Extension\ThemeHandler;
use Drupal\Core\Config\ConfigInstaller;
@ -65,11 +66,11 @@ class ThemeHandlerTest extends UnitTestCase {
protected $configInstaller;
/**
* The system listing info.
* The extension discovery.
*
* @var \Drupal\Core\SystemListingInfo|\PHPUnit_Framework_MockObject_MockObject
* @var \Drupal\Core\Extension\ExtensionDiscovery|\PHPUnit_Framework_MockObject_MockObject
*/
protected $systemListingInfo;
protected $extensionDiscovery;
/**
* The tested theme handler.
@ -101,10 +102,10 @@ class ThemeHandlerTest extends UnitTestCase {
$this->routeBuilder = $this->getMockBuilder('Drupal\Core\Routing\RouteBuilder')
->disableOriginalConstructor()
->getMock();
$this->systemListingInfo = $this->getMockBuilder('Drupal\Core\SystemListingInfo')
$this->extensionDiscovery = $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->extensionDiscovery);
$this->getContainerWithCacheBins($this->cacheBackend);
}
@ -145,17 +146,12 @@ class ThemeHandlerTest extends UnitTestCase {
->expects($this->once())
->method('save');
$this->systemListingInfo->expects($this->any())
$this->extensionDiscovery->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));
@ -186,27 +182,25 @@ class ThemeHandlerTest extends UnitTestCase {
->method('clear')
->will($this->returnSelf());
$this->systemListingInfo->expects($this->any())
$this->extensionDiscovery->expects($this->any())
->method('scan')
->will($this->returnValue(array()));
$this->themeHandler->enable(array('bartik'));
$this->themeHandler->systemList['bartik'] = (object) array(
'name' => 'bartik',
'info' => array(
'stylesheets' => array(
'all' => array(
'css/layout.css',
'css/style.css',
'css/colors.css',
),
$this->themeHandler->systemList['bartik'] = new Extension('theme', DRUPAL_ROOT . '/core/themes/bartik/bartik.info.yml', 'bartik.info.yml');
$this->themeHandler->systemList['bartik']->info = array(
'stylesheets' => array(
'all' => array(
'css/layout.css',
'css/style.css',
'css/colors.css',
),
'scripts' => array(
'example' => 'theme.js',
),
'engine' => 'twig',
'base theme' => 'stark',
),
'scripts' => array(
'example' => 'theme.js',
),
'engine' => 'twig',
'base theme' => 'stark',
);
$list_info = $this->themeHandler->listInfo();
@ -218,18 +212,16 @@ class ThemeHandlerTest extends UnitTestCase {
$this->assertEquals('stark', $list_info['bartik']->base_theme);
$this->assertEquals(0, $list_info['bartik']->status);
$this->themeHandler->systemList['seven'] = (object) array(
'name' => 'seven',
'info' => array(
'stylesheets' => array(
'screen' => array(
'style.css',
),
$this->themeHandler->systemList['seven'] = new Extension('theme', DRUPAL_ROOT . '/core/themes/seven/seven.info.yml', 'seven.info.yml');
$this->themeHandler->systemList['seven']->info = array(
'stylesheets' => array(
'screen' => array(
'style.css',
),
'scripts' => array(),
),
'status' => 1,
'scripts' => array(),
);
$this->themeHandler->systemList['seven']->status = 1;
$this->themeHandler->enable(array('seven'));
@ -246,14 +238,11 @@ class ThemeHandlerTest extends UnitTestCase {
* @see \Drupal\Core\Extension\ThemeHandler::rebuildThemeData()
*/
public function testRebuildThemeData() {
$this->systemListingInfo->expects($this->at(0))
$this->extensionDiscovery->expects($this->at(0))
->method('scan')
->with($this->anything(), 'themes', 'name', 1)
->with('theme')
->will($this->returnValue(array(
'seven' => (object) array(
'name' => 'seven',
'uri' => DRUPAL_ROOT . '/core/themes/seven/seven.info.yml',
),
'seven' => new Extension('theme', DRUPAL_ROOT . '/core/themes/seven/seven.info.yml', 'seven.info.yml'),
)));
$this->infoParser->expects($this->once())
->method('parse')
@ -271,7 +260,7 @@ class ThemeHandlerTest extends UnitTestCase {
$info = $theme_data['seven'];
// Ensure some basic properties.
$this->assertInstanceOf('stdClass', $info);
$this->assertInstanceOf('Drupal\Core\Extension\Extension', $info);
$this->assertEquals('seven', $info->name);
$this->assertEquals(DRUPAL_ROOT . '/core/themes/seven/seven.info.yml', $info->uri);
$this->assertEquals(DRUPAL_ROOT . '/core/themes/seven/seven.info.yml', $info->filename);

View File

@ -0,0 +1,5 @@
type: theme_engine
name: PHPTemplate
core: 8.x
version: VERSION
package: Core

View File

@ -0,0 +1,5 @@
type: theme_engine
name: Twig
core: 8.x
version: VERSION
package: Core