diff --git a/core/core.services.yml b/core/core.services.yml index 18e37f98ebea9ba604c9d55e4781ff0a249e1a2a..4563d66908aa79a373106c92c329d5c432bd7a06 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -740,5 +740,8 @@ services: class: Drupal\Core\Asset\JsCollectionGrouper asset.js.dumper: class: Drupal\Core\Asset\AssetDumper + library.discovery: + class: Drupal\Core\Asset\LibraryDiscovery + arguments: ['@cache.cache', '@module_handler'] info_parser: class: Drupal\Core\Extension\InfoParser diff --git a/core/includes/common.inc b/core/includes/common.inc index 9ec0e8613093e8363c612e26b84a5297a2fd5c49..d118c5600465a2e14af1261011ffc42b9993c410 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -2602,16 +2602,18 @@ function drupal_process_states(&$elements) { * TRUE if the library was successfully added; FALSE if the library or one of * its dependencies could not be added. * - * @see drupal_get_library() + * @see \Drupal\Core\Asset\LibraryDiscovery * @see hook_library_info_alter() */ function _drupal_add_library($library_name, $every_page = NULL) { $added = &drupal_static(__FUNCTION__, array()); + /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */ + $library_discovery = \Drupal::service('library.discovery'); list($extension, $name) = explode('/', $library_name, 2); // Only process the library if it exists and it was not added already. if (!isset($added[$extension][$name])) { - if ($library = drupal_get_library($library_name)) { + if ($library = $library_discovery->getLibraryByName($extension, $name)) { // Allow modules and themes to dynamically attach request and context // specific data for this library; e.g., localization. \Drupal::moduleHandler()->alter('library', $library, $library_name); @@ -2642,188 +2644,6 @@ function _drupal_add_library($library_name, $every_page = NULL) { return $added[$extension][$name]; } -/** - * Retrieves information for a JavaScript/CSS library. - * - * Library information is statically cached. Libraries are keyed by module for - * several reasons: - * - Libraries are not unique. Multiple modules might ship with the same library - * in a different version or variant. This registry cannot (and does not - * attempt to) prevent library conflicts. - * - Modules implementing and thereby depending on a library that is registered - * by another module can only rely on that module's library. - * - Two (or more) modules can still register the same library and use it - * without conflicts in case the libraries are loaded on certain pages only. - * - * @param $library_name - * The name of a registered library to retrieve. By default, all - * libraries registered by the extension are returned. - * - * @return - * The definition of the requested library, if $name was passed and it exists, - * or FALSE if it does not exist. If no $name was passed, an associative array - * of libraries registered by the module is returned (which may be empty). - * - * @see _drupal_add_library() - * @see hook_library_info_alter() - * - * @todo The purpose of drupal_get_*() is completely different to other page - * requisite API functions; find and use a different name. - */ -function drupal_get_library($library_name) { - $libraries = &drupal_static(__FUNCTION__, array()); - - $library_info = explode('/', $library_name, 2); - $extension = $library_info[0]; - $name = isset($library_info[1]) ? $library_info[1] : NULL; - if (!isset($libraries[$extension]) && ($cache = \Drupal::cache()->get('library:info:' . $extension))) { - $libraries[$extension] = $cache->data; - } - - if (!isset($libraries[$extension])) { - $libraries[$extension] = array(); - if ($extension === 'core') { - $path = 'core'; - $extension_type = 'core'; - } - else { - // @todo Add a $type argument OR automatically figure out the type based - // on current extension data, possibly using a module->theme fallback. - $path = drupal_get_path('module', $extension); - $extension_type = 'module'; - if (!$path) { - $path = drupal_get_path('theme', $extension); - $extension_type = 'theme'; - } - } - $library_file = $path . '/' . $extension . '.libraries.yml'; - - if ($library_file && file_exists(DRUPAL_ROOT . '/' . $library_file)) { - $libraries[$extension] = array(); - $parser = new Parser(); - try { - $libraries[$extension] = $parser->parse(file_get_contents(DRUPAL_ROOT . '/' . $library_file)); - } - catch (ParseException $e) { - // Rethrow a more helpful exception, since ParseException lacks context. - throw new \RuntimeException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e); - } - // Allow modules to alter the module's registered libraries. - \Drupal::moduleHandler()->alter('library_info', $libraries[$extension], $extension); - } - - foreach ($libraries[$extension] as $id => &$library) { - if (!isset($library['js']) && !isset($library['css']) && !isset($library['settings'])) { - throw new \RuntimeException(sprintf("Incomplete library definition for '%s' in %s", $id, $library_file)); - } - $library += array('dependencies' => array(), 'js' => array(), 'css' => array()); - - if (isset($library['version'])) { - // @todo Retrieve version of a non-core extension. - if ($library['version'] === 'VERSION') { - $library['version'] = \Drupal::VERSION; - } - // Remove 'v' prefix from external library versions. - elseif ($library['version'][0] === 'v') { - $library['version'] = substr($library['version'], 1); - } - } - - foreach (array('js', 'css') as $type) { - // Prepare (flatten) the SMACSS-categorized definitions. - // @todo After Asset(ic) changes, retain the definitions as-is and - // properly resolve dependencies for all (css) libraries per category, - // and only once prior to rendering out an HTML page. - if ($type == 'css' && !empty($library[$type])) { - foreach ($library[$type] as $category => $files) { - foreach ($files as $source => $options) { - if (!isset($options['weight'])) { - $options['weight'] = 0; - } - // Apply the corresponding weight defined by CSS_* constants. - $options['weight'] += constant('CSS_' . strtoupper($category)); - $library[$type][$source] = $options; - } - unset($library[$type][$category]); - } - } - foreach ($library[$type] as $source => $options) { - unset($library[$type][$source]); - // Allow to omit the options hashmap in YAML declarations. - if (!is_array($options)) { - $options = array(); - } - if ($type == 'js' && isset($options['weight']) && $options['weight'] > 0) { - throw new \UnexpectedValueException("The $extension/$id library defines a positive weight for '$source'. Only negative weights are allowed (but should be avoided). Instead of a positive weight, specify accurate dependencies for this library."); - } - // Unconditionally apply default groups for the defined asset files. - // The library system is a dependency management system. Each library - // properly specifies its dependencies instead of relying on a custom - // processing order. - if ($type == 'js') { - $options['group'] = JS_LIBRARY; - } - elseif ($type == 'css') { - $options['group'] = $extension_type == 'theme' ? CSS_AGGREGATE_THEME : CSS_AGGREGATE_DEFAULT; - } - // By default, all library assets are files. - if (!isset($options['type'])) { - $options['type'] = 'file'; - } - if ($options['type'] == 'external') { - $options['data'] = $source; - } - // Determine the file asset URI. - else { - if ($source[0] === '/') { - // An absolute path maps to DRUPAL_ROOT / base_path(). - if ($source[1] !== '/') { - $options['data'] = substr($source, 1); - } - // A protocol-free URI (e.g., //cdn.com/example.js) is external. - else { - $options['type'] = 'external'; - $options['data'] = $source; - } - } - // A stream wrapper URI (e.g., public://generated_js/example.js). - elseif (file_valid_uri($source)) { - $options['data'] = $source; - } - // By default, file paths are relative to the registering extension. - else { - $options['data'] = $path . '/' . $source; - } - } - $options['version'] = $library['version']; - - $library[$type][] = $options; - } - } - // @todo Introduce drupal_add_settings(). - if (isset($library['settings'])) { - $library['js'][] = array( - 'type' => 'setting', - 'data' => $library['settings'], - ); - unset($library['settings']); - } - } - \Drupal::cache()->set('library:info:' . $extension, $libraries[$extension], Cache::PERMANENT, array( - 'extension' => array(TRUE, $extension), - 'library_info' => array(TRUE), - )); - } - - if (isset($name)) { - if (!isset($libraries[$extension][$name])) { - $libraries[$extension][$name] = FALSE; - } - return $libraries[$extension][$name]; - } - return $libraries[$extension]; -} - /** * Assists in attaching the tableDrag JavaScript behavior to a themed table. * diff --git a/core/lib/Drupal/Core/Asset/Exception/IncompleteLibraryDefinitionException.php b/core/lib/Drupal/Core/Asset/Exception/IncompleteLibraryDefinitionException.php new file mode 100644 index 0000000000000000000000000000000000000000..52aa09cb448741666bb883e90061ee723ea317d6 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Exception/IncompleteLibraryDefinitionException.php @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException. + */ + +namespace Drupal\Core\Asset\Exception; + +/** + * Defines a custom exception if a library has no CSS/JS/JS setting specified. + */ +class IncompleteLibraryDefinitionException extends \RuntimeException { + +} diff --git a/core/lib/Drupal/Core/Asset/Exception/InvalidLibraryFileException.php b/core/lib/Drupal/Core/Asset/Exception/InvalidLibraryFileException.php new file mode 100644 index 0000000000000000000000000000000000000000..33cf83095bc9ab67e68d9170d2fef552a1031b60 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/Exception/InvalidLibraryFileException.php @@ -0,0 +1,15 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Asset\Exception\InvalidLibraryFileException. + */ + +namespace Drupal\Core\Asset\Exception; + +/** + * Defines an exception if the library file could not be parsed. + */ +class InvalidLibraryFileException extends \RunTimeException { + +} diff --git a/core/lib/Drupal/Core/Asset/LibraryDiscovery.php b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php new file mode 100644 index 0000000000000000000000000000000000000000..130876a84e3de22f7200ec7152a0c20ba6a1168e --- /dev/null +++ b/core/lib/Drupal/Core/Asset/LibraryDiscovery.php @@ -0,0 +1,314 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Asset\LibraryDiscovery. + */ + +namespace Drupal\Core\Asset; + +use Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException; +use Drupal\Core\Asset\Exception\InvalidLibraryFileException; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Extension\ThemeHandlerInterface; +use Symfony\Component\Yaml\Exception\ParseException; +use Symfony\Component\Yaml\Parser; + + +/** + * Discovers available asset libraries in Drupal. + */ +class LibraryDiscovery implements LibraryDiscoveryInterface { + + /** + * Stores the library information keyed by extension. + * + * @var array + */ + protected $libraries; + + /** + * The cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cache; + + /** + * The module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * Constructs a new LibraryDiscovery instance. + * + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + */ + public function __construct(CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) { + $this->cache = $cache_backend; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + public function getLibrariesByExtension($extension) { + $this->ensureLibraryInformation($extension); + return $this->libraries[$extension]; + } + + /** + * {@inheritdoc} + */ + public function getLibraryByName($extension, $name) { + $this->ensureLibraryInformation($extension); + return isset($this->libraries[$extension][$name]) ? $this->libraries[$extension][$name] : FALSE; + } + + /** + * Ensures that the libraries property is filled. + * + * @param string $extension + * The name of the extension that registered a library. + */ + protected function ensureLibraryInformation($extension) { + $this->getCache($extension); + if (!isset($this->libraries[$extension])) { + if ($information = $this->buildLibrariesByExtension($extension)) { + $this->libraries[$extension] = $information; + } + else { + $this->libraries[$extension] = FALSE; + } + $this->setCache($extension, $this->libraries[$extension]); + } + } + + /** + * Fills up the libraries property from cache, if available. + * + * @param string $extension + * The name of the extension that registered a library. + */ + protected function getCache($extension) { + if (!isset($this->libraries[$extension])) { + if ($cache = $this->cache->get('library:info:' . $extension)) { + $this->libraries[$extension] = $cache->data; + } + } + } + + /** + * Sets the library information into a cache entry. + * + * @param string $extension + * The name of the extension that registered a library. + * + * @param bool|array $information + * All library definitions of the passed extension or FALSE if no + * information is available. + */ + protected function setCache($extension, $information) { + $this->cache->set('library:info:' . $extension, $information, Cache::PERMANENT, array( + 'extension' => array(TRUE, $extension), + 'library_info' => array(TRUE), + )); + } + + /** + * Parses and builds up all the libraries information of an extension. + * + * @param string $extension + * The name of the extension that registered a library. + * + * @return array + * All library definitions of the passed extension. + * + * @throws \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException + * Thrown when a library has no js/css/setting. + * @throws \UnexpectedValueException + * Thrown when a js file defines a positive weight. + */ + protected function buildLibrariesByExtension($extension) { + $this->libraries[$extension] = array(); + if ($extension === 'core') { + $path = 'core'; + $extension_type = 'core'; + } + else { + if ($this->moduleHandler->moduleExists($extension)) { + $extension_type = 'module'; + } + else { + $extension_type = 'theme'; + } + $path = $this->drupalGetPath($extension_type, $extension); + } + $library_file = $path . '/' . $extension . '.libraries.yml'; + + if ($library_file && file_exists(DRUPAL_ROOT . '/' . $library_file)) { + $this->libraries[$extension] = array(); + $this->parseLibraryInfo($extension, $library_file); + } + + foreach ($this->libraries[$extension] as $id => &$library) { + if (!isset($library['js']) && !isset($library['css']) && !isset($library['settings'])) { + throw new IncompleteLibraryDefinitionException(sprintf("Incomplete library definition for '%s' in %s", $id, $library_file)); + } + $library += array('dependencies' => array(), 'js' => array(), 'css' => array()); + + if (isset($library['version'])) { + // @todo Retrieve version of a non-core extension. + if ($library['version'] === 'VERSION') { + $library['version'] = \Drupal::VERSION; + } + // Remove 'v' prefix from external library versions. + elseif ($library['version'][0] === 'v') { + $library['version'] = substr($library['version'], 1); + } + } + + foreach (array('js', 'css') as $type) { + // Prepare (flatten) the SMACSS-categorized definitions. + // @todo After Asset(ic) changes, retain the definitions as-is and + // properly resolve dependencies for all (css) libraries per category, + // and only once prior to rendering out an HTML page. + if ($type == 'css' && !empty($library[$type])) { + foreach ($library[$type] as $category => $files) { + foreach ($files as $source => $options) { + if (!isset($options['weight'])) { + $options['weight'] = 0; + } + // Apply the corresponding weight defined by CSS_* constants. + $options['weight'] += constant('CSS_' . strtoupper($category)); + $library[$type][$source] = $options; + } + unset($library[$type][$category]); + } + } + foreach ($library[$type] as $source => $options) { + unset($library[$type][$source]); + // Allow to omit the options hashmap in YAML declarations. + if (!is_array($options)) { + $options = array(); + } + if ($type == 'js' && isset($options['weight']) && $options['weight'] > 0) { + throw new \UnexpectedValueException("The $extension/$id library defines a positive weight for '$source'. Only negative weights are allowed (but should be avoided). Instead of a positive weight, specify accurate dependencies for this library."); + } + // Unconditionally apply default groups for the defined asset files. + // The library system is a dependency management system. Each library + // properly specifies its dependencies instead of relying on a custom + // processing order. + if ($type == 'js') { + $options['group'] = JS_LIBRARY; + } + elseif ($type == 'css') { + $options['group'] = $extension_type == 'theme' ? CSS_AGGREGATE_THEME : CSS_AGGREGATE_DEFAULT; + } + // By default, all library assets are files. + if (!isset($options['type'])) { + $options['type'] = 'file'; + } + if ($options['type'] == 'external') { + $options['data'] = $source; + } + // Determine the file asset URI. + else { + if ($source[0] === '/') { + // An absolute path maps to DRUPAL_ROOT / base_path(). + if ($source[1] !== '/') { + $options['data'] = substr($source, 1); + } + // A protocol-free URI (e.g., //cdn.com/example.js) is external. + else { + $options['type'] = 'external'; + $options['data'] = $source; + } + } + // A stream wrapper URI (e.g., public://generated_js/example.js). + elseif ($this->fileValidUri($source)) { + $options['data'] = $source; + } + // By default, file paths are relative to the registering extension. + else { + $options['data'] = $path . '/' . $source; + } + } + + if (!isset($library['version'])) { + // @todo Get the information from the extension. + $options['version'] = -1; + } + else { + $options['version'] = $library['version']; + } + + $library[$type][] = $options; + } + } + + // @todo Introduce drupal_add_settings(). + if (isset($library['settings'])) { + $library['js'][] = array( + 'type' => 'setting', + 'data' => $library['settings'], + ); + unset($library['settings']); + } + // @todo Convert all uses of #attached[library][]=array('provider','name') + // into #attached[library][]='provider/name' and remove this. + foreach ($library['dependencies'] as $i => $dependency) { + $library['dependencies'][$i] = $dependency; + } + } + return $this->libraries[$extension]; + } + + /** + * Wraps drupal_get_path(). + */ + protected function drupalGetPath($type, $name) { + return drupal_get_path($type, $name); + } + + /** + * Wraps file_valid_uri(). + */ + protected function fileValidUri($source) { + return file_valid_uri($source); + } + + /** + * Parses a given library file and allows module to alter it. + * + * This method sets the parsed information onto the library property. + * + * @param string $extension + * The name of the extension that registered a library. + * @param string $library_file + * The relative filename to the DRUPAL_ROOT of the wanted library file. + * + * @throws \Drupal\Core\Asset\Exception\InvalidLibraryFileException + * Thrown when a parser exception got thrown. + */ + protected function parseLibraryInfo($extension, $library_file) { + $parser = new Parser(); + try { + $this->libraries[$extension] = $parser->parse(file_get_contents(DRUPAL_ROOT . '/' . $library_file)); + } + catch (ParseException $e) { + // Rethrow a more helpful exception, since ParseException lacks context. + throw new InvalidLibraryFileException(sprintf('Invalid library definition in %s: %s', $library_file, $e->getMessage()), 0, $e); + } + // Allow modules to alter the module's registered libraries. + $this->moduleHandler->alter('library_info', $this->libraries[$extension], $extension); + } + +} diff --git a/core/lib/Drupal/Core/Asset/LibraryDiscoveryInterface.php b/core/lib/Drupal/Core/Asset/LibraryDiscoveryInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..ac77f70c9db9389b1670a77e9059fde7fd452b75 --- /dev/null +++ b/core/lib/Drupal/Core/Asset/LibraryDiscoveryInterface.php @@ -0,0 +1,53 @@ +<?php + +/** + * @file + * Contains \Drupal\Core\Asset\LibraryDiscoveryInterface. + */ + +namespace Drupal\Core\Asset; + +/** + * Discovers information for asset (CSS/JavaScript) libraries. + * + * Library information is statically cached. Libraries are keyed by extension + * for several reasons: + * - Libraries are not unique. Multiple extensions might ship with the same + * library in a different version or variant. This registry cannot (and does + * not attempt to) prevent library conflicts. + * - Extensions implementing and thereby depending on a library that is + * registered by another extension can only rely on that extension's library. + * - Two (or more) extensions can still register the same library and use it + * without conflicts in case the libraries are loaded on certain pages only. + */ +interface LibraryDiscoveryInterface { + + /** + * Gets all libraries defined by an extension. + * + * @param string $extension + * The name of the extension that registered a library. + * + * @return array + * An associative array of libraries registered by $extension is returned + * (which may be empty). + * + * @see self::getLibraryByName() + */ + public function getLibrariesByExtension($extension); + + /** + * Gets a single library defined by an extension by name. + * + * @param string $extension + * The name of the extension that registered a library. + * @param string $name + * The name of a registered library to retrieve. + * + * @return array|FALSE + * The definition of the requested library, if $name was passed and it + * exists, otherwise FALSE. + */ + public function getLibraryByName($extension, $name); + +} diff --git a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php index 0881810841dc979dde209fb7b1933708745839cf..937b83ccb90d0cee535c14dadee796a15d23d83f 100644 --- a/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php +++ b/core/modules/system/lib/Drupal/system/Tests/Common/JavaScriptTest.php @@ -621,7 +621,9 @@ function testLibraryRender() { */ function testLibraryAlter() { // Verify that common_test altered the title of Farbtastic. - $library = drupal_get_library('core/jquery.farbtastic'); + /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */ + $library_discovery = \Drupal::service('library.discovery'); + $library = $library_discovery->getLibraryByName('core', 'jquery.farbtastic'); $this->assertEqual($library['version'], '0.0', 'Registered libraries were altered.'); // common_test_library_info_alter() also added a dependency on jQuery Form. @@ -637,7 +639,9 @@ function testLibraryAlter() { * @see common_test.library.yml */ function testLibraryNameConflicts() { - $farbtastic = drupal_get_library('common_test/jquery.farbtastic'); + /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */ + $library_discovery = \Drupal::service('library.discovery'); + $farbtastic = $library_discovery->getLibraryByName('common_test', 'jquery.farbtastic'); $this->assertEqual($farbtastic['version'], '0.1', 'Alternative libraries can be added to the page.'); } @@ -645,7 +649,9 @@ function testLibraryNameConflicts() { * Tests non-existing libraries. */ function testLibraryUnknown() { - $result = drupal_get_library('unknown/unknown'); + /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */ + $library_discovery = \Drupal::service('library.discovery'); + $result = $library_discovery->getLibraryByName('unknown', 'unknown'); $this->assertFalse($result, 'Unknown library returned FALSE.'); drupal_static_reset('drupal_get_library'); @@ -669,19 +675,21 @@ function testAttachedLibrary() { * Tests retrieval of libraries via drupal_get_library(). */ function testGetLibrary() { + /** @var \Drupal\Core\Asset\LibraryDiscoveryInterface $library_discovery */ + $library_discovery = \Drupal::service('library.discovery'); // Retrieve all libraries registered by a module. - $libraries = drupal_get_library('common_test'); + $libraries = $library_discovery->getLibrariesByExtension('common_test'); $this->assertTrue(isset($libraries['jquery.farbtastic']), 'Retrieved all module libraries.'); // Retrieve all libraries for a module not declaring any libraries. // Note: This test installs language module. - $libraries = drupal_get_library('dblog'); + $libraries = $library_discovery->getLibrariesByExtension('dblog'); $this->assertEqual($libraries, array(), 'Retrieving libraries from a module not declaring any libraries returns an emtpy array.'); // Retrieve a specific library by module and name. - $farbtastic = drupal_get_library('common_test/jquery.farbtastic'); + $farbtastic = $library_discovery->getLibraryByName('common_test', 'jquery.farbtastic'); $this->assertEqual($farbtastic['version'], '0.1', 'Retrieved a single library.'); // Retrieve a non-existing library by module and name. - $farbtastic = drupal_get_library('common_test/foo'); + $farbtastic = $library_discovery->getLibraryByName('common_test', 'foo'); $this->assertIdentical($farbtastic, FALSE, 'Retrieving a non-existing library returns FALSE.'); } diff --git a/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php new file mode 100644 index 0000000000000000000000000000000000000000..230cac61dd57a7081a2b71320685f415f0da9a4b --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/LibraryDiscoveryTest.php @@ -0,0 +1,494 @@ +<?php + +/** + * @file + * Contains \Drupal\Tests\Core\Asset\LibraryDiscoveryTest. + */ + +namespace Drupal\Tests\Core\Asset; + +use Drupal\Core\Asset\LibraryDiscovery; +use Drupal\Core\Cache\Cache; +use Drupal\Tests\UnitTestCase; + +if (!defined('DRUPAL_ROOT')) { + define('DRUPAL_ROOT', dirname(dirname(substr(__DIR__, 0, -strlen(__NAMESPACE__))))); +} + +if (!defined('CSS_AGGREGATE_DEFAULT')) { + define('CSS_AGGREGATE_DEFAULT', 0); + define('CSS_AGGREGATE_THEME', 100); + define('CSS_BASE', -200); + define('CSS_LAYOUT', -100); + define('CSS_COMPONENT', 0); + define('CSS_STATE', 100); + define('CSS_THEME', 200); + define('JS_SETTING', -200); + define('JS_LIBRARY', -100); + define('JS_DEFAULT', 0); + define('JS_THEME', 100); +} + +/** + * Tests the library discovery. + * + * @coversDefaultClass \Drupal\Core\Asset\LibraryDiscovery + */ +class LibraryDiscoveryTest extends UnitTestCase { + + /** + * The tested library provider. + * + * @var \Drupal\Core\Asset\LibraryDiscovery|\Drupal\Tests\Core\Asset\TestLibraryDiscovery + */ + protected $libraryDiscovery; + + /** + * The mocked cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $cache; + + /** + * The mocked module handler. + * + * @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject + */ + protected $moduleHandler; + + /** + * The mocked theme handler. + * + * @var \Drupal\Core\Extension\ThemeHandlerInterface + */ + protected $themeHandler; + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Tests \Drupal\Core\Asset\LibraryProvider', + 'description' => '', + 'group' => 'Asset handling', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + $this->cache = $this->getMock('Drupal\Core\Cache\CacheBackendInterface'); + $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface'); + $this->themeHandler = $this->getMock('Drupal\Core\Extension\ThemeHandlerInterface'); + $this->libraryDiscovery = new TestLibraryDiscovery($this->cache, $this->moduleHandler, $this->themeHandler); + } + + /** + * Tests that basic functionality works for getLibraryByName. + * + * @covers ::getLibraryByName() + * @covers ::buildLibrariesByExtension() + */ + public function testGetLibraryByNameSimple() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('example_module') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'example_module', $path); + + $library = $this->libraryDiscovery->getLibraryByName('example_module', 'example'); + $this->assertCount(0, $library['js']); + $this->assertCount(1, $library['css']); + $this->assertCount(0, $library['dependencies']); + $this->assertEquals($path . '/css/example.css', $library['css'][0]['data']); + + // Ensures that VERSION is replaced by the current core version. + $this->assertEquals(\Drupal::VERSION, $library['version']); + } + + /** + * Tests that basic functionality works for getLibrariesByExtension. + * + * @covers ::getLibrariesByExtension() + * @covers ::buildLibrariesByExtension() + */ + public function testGetLibrariesByExtensionSimple() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('example_module') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'example_module', $path); + + $libraries = $this->libraryDiscovery->getLibrariesByExtension('example_module', 'example'); + $this->assertCount(1, $libraries); + $this->assertEquals($path . '/css/example.css', $libraries['example']['css'][0]['data']); + } + + /** + * Tests that a theme can be used instead of a module. + * + * @covers ::getLibraryByName() + * @covers ::buildLibrariesByExtension() + */ + public function testGetLibraryByNameWithTheme() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('example_theme') + ->will($this->returnValue(FALSE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('theme', 'example_theme', $path); + + $library = $this->libraryDiscovery->getLibraryByName('example_theme', 'example'); + $this->assertCount(0, $library['js']); + $this->assertCount(1, $library['css']); + $this->assertCount(0, $library['dependencies']); + $this->assertEquals($path . '/css/example.css', $library['css'][0]['data']); + } + + /** + * Tests that a module with a missing library file results in FALSE. + * + * @covers ::getLibraryByName() + * @covers ::getLibrariesByExtension() + * @covers ::buildLibrariesByExtension() + */ + public function testGetLibraryWithMissingLibraryFile() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('example_module') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files_not_existing'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'example_module', $path); + + $this->assertFalse($this->libraryDiscovery->getLibraryByName('example_module', 'example')); + $this->assertFalse($this->libraryDiscovery->getLibrariesByExtension('example_module')); + } + + /** + * Tests that an exception is thrown when a libraries file couldn't be parsed. + * + * @expectedException \Drupal\Core\Asset\Exception\InvalidLibraryFileException + * + * @covers ::buildLibrariesByExtension() + */ + public function testInvalidLibrariesFile() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('invalid_file') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'invalid_file', $path); + + $this->libraryDiscovery->getLibrariesByExtension('invalid_file'); + } + + /** + * Tests that an exception is thrown when no CSS/JS/setting is specified. + * + * @expectedException \Drupal\Core\Asset\Exception\IncompleteLibraryDefinitionException + * @expectedExceptionMessage Incomplete library definition for 'example' in core/tests/Drupal/Tests/Core/Asset/library_test_files/example_module_missing_information.libraries.yml + * + * @covers ::buildLibrariesByExtension() + */ + public function testGetLibraryWithMissingInformation() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('example_module_missing_information') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'example_module_missing_information', $path); + + $this->libraryDiscovery->getLibrariesByExtension('example_module_missing_information'); + } + + /** + * Tests that the version property of external libraries is handled. + * + * @covers ::buildLibrariesByExtension() + */ + public function testExternalLibraries() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('external') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'external', $path); + + $library = $this->libraryDiscovery->getLibraryByName('external', 'example_external'); + $this->assertEquals($path . '/css/example_external.css', $library['css'][0]['data']); + $this->assertEquals('3.14', $library['version']); + } + + /** + * Ensures that CSS weights are taken into account properly. + * + * @covers ::buildLibrariesByExtension() + */ + public function testDefaultCssWeights() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('css_weights') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'css_weights', $path); + + $library = $this->libraryDiscovery->getLibraryByName('css_weights', 'example'); + $css = $library['css']; + $this->assertCount(10, $css); + + // The following default weights are tested: + // - CSS_BASE: -200 + // - CSS_LAYOUT: -100 + // - CSS_COMPONENT: 0 + // - CSS_STATE: 100 + // - CSS_THEME: 200 + $this->assertEquals(200, $css[0]['weight']); + $this->assertEquals(200 + 29, $css[1]['weight']); + $this->assertEquals(-200, $css[2]['weight']); + $this->assertEquals(-200 + 97, $css[3]['weight']); + $this->assertEquals(-100, $css[4]['weight']); + $this->assertEquals(-100 + 92, $css[5]['weight']); + $this->assertEquals(0, $css[6]['weight']); + $this->assertEquals(45, $css[7]['weight']); + $this->assertEquals(100, $css[8]['weight']); + $this->assertEquals(100 + 8, $css[9]['weight']); + } + + /** + * Ensures that you cannot provide positive weights for JavaScript libraries. + * + * @expectedException \UnexpectedValueException + * + * @covers ::buildLibrariesByExtension() + */ + public function testJsWithPositiveWeight() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('js_positive_weight') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'js_positive_weight', $path); + + $this->libraryDiscovery->getLibrariesByExtension('js_positive_weight'); + } + + /** + * Tests a library with CSS/JavaScript and a setting. + * + * @covers ::buildLibrariesByExtension() + */ + public function testLibraryWithCssJsSetting() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('css_js_settings') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'css_js_settings', $path); + + $library = $this->libraryDiscovery->getLibraryByName('css_js_settings', 'example'); + + // Ensures that the group and type are set automatically. + $this->assertEquals(-100, $library['js'][0]['group']); + $this->assertEquals('file', $library['js'][0]['type']); + $this->assertEquals($path . '/js/example.js', $library['js'][0]['data']); + + $this->assertEquals(0, $library['css'][0]['group']); + $this->assertEquals('file', $library['css'][0]['type']); + $this->assertEquals($path . '/css/base.css', $library['css'][0]['data']); + + $this->assertEquals('setting', $library['js'][1]['type']); + $this->assertEquals(array('key' => 'value'), $library['js'][1]['data']); + } + + /** + * Tests a library with dependencies. + * + * @covers ::buildLibrariesByExtension() + */ + public function testLibraryWithDependencies() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('dependencies') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'dependencies', $path); + + $library = $this->libraryDiscovery->getLibraryByName('dependencies', 'example'); + $this->assertCount(2, $library['dependencies']); + $this->assertEquals('external/example_external', $library['dependencies'][0]); + $this->assertEquals('example_module/example', $library['dependencies'][1]); + } + + /** + * Tests a library with a couple of data formats like full URL. + * + * @covers ::buildLibrariesByExtension() + */ + public function testLibraryWithDataTypes() { + $this->moduleHandler->expects($this->atLeastOnce()) + ->method('moduleExists') + ->with('data_types') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'data_types', $path); + + $this->libraryDiscovery->setFileValidUri('public://test.css', TRUE); + $this->libraryDiscovery->setFileValidUri('public://test2.css', FALSE); + + $library = $this->libraryDiscovery->getLibraryByName('data_types', 'example'); + $this->assertCount(5, $library['css']); + $this->assertEquals('external', $library['css'][0]['type']); + $this->assertEquals('http://example.com/test.css', $library['css'][0]['data']); + $this->assertEquals('file', $library['css'][1]['type']); + $this->assertEquals('tmp/test.css', $library['css'][1]['data']); + $this->assertEquals('external', $library['css'][2]['type']); + $this->assertEquals('//cdn.com/test.css', $library['css'][2]['data']); + $this->assertEquals('file', $library['css'][3]['type']); + $this->assertEquals('public://test.css', $library['css'][3]['data']); + } + + /** + * Tests the internal static cache. + * + * @covers ::ensureLibraryInformation() + */ + public function testStaticCache() { + $this->moduleHandler->expects($this->once()) + ->method('moduleExists') + ->with('example_module') + ->will($this->returnValue(TRUE)); + $this->cache->expects($this->once()) + ->method('get') + ->with('library:info:' . 'example_module') + ->will($this->returnValue(NULL)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'example_module', $path); + + $library = $this->libraryDiscovery->getLibraryByName('example_module', 'example'); + $this->assertEquals($path . '/css/example.css', $library['css'][0]['data']); + + $library = $this->libraryDiscovery->getLibraryByName('example_module', 'example'); + $this->assertEquals($path . '/css/example.css', $library['css'][0]['data']); + } + + /** + * Tests the external cache. + * + * @covers ::getCache() + */ + public function testExternalCache() { + // Ensure that the module handler does not need to be touched. + $this->moduleHandler->expects($this->never()) + ->method('moduleExists'); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + + // Setup a cache entry which will be retrieved, but just once, so the static + // cache still works. + $this->cache->expects($this->once()) + ->method('get') + ->with('library:info:' . 'example_module') + ->will($this->returnValue((object) array( + 'data' => array( + 'example' => array( + 'css' => array( + array( + 'data' => $path . '/css/example.css', + ), + ), + ), + ) + ))); + + $library = $this->libraryDiscovery->getLibraryByName('example_module', 'example'); + $this->assertEquals($path . '/css/example.css', $library['css'][0]['data']); + + $library = $this->libraryDiscovery->getLibraryByName('example_module', 'example'); + $this->assertEquals($path . '/css/example.css', $library['css'][0]['data']); + } + + /** + * Tests setting the external cache. + * + * @covers ::setCache() + */ + public function testSetCache() { + $this->moduleHandler->expects($this->once()) + ->method('moduleExists') + ->with('example_module') + ->will($this->returnValue(TRUE)); + + $path = __DIR__ . '/library_test_files'; + $path = substr($path, strlen(DRUPAL_ROOT) + 1); + $this->libraryDiscovery->setPaths('module', 'example_module', $path); + + $this->cache->expects($this->once()) + ->method('set') + ->with('library:info:example_module', $this->isType('array'), Cache::PERMANENT, array( + 'extension' => array(TRUE, 'example_module'), + 'library_info' => array(TRUE), + )); + + $library = $this->libraryDiscovery->getLibraryByName('example_module', 'example'); + $this->assertEquals($path . '/css/example.css', $library['css'][0]['data']); + } + +} + +/** + * Wraps the tested class to mock the external dependencies. + */ +class TestLibraryDiscovery extends LibraryDiscovery { + + protected $paths; + + protected $validUris; + + protected function drupalGetPath($type, $name) { + return isset($this->paths[$type][$name]) ? $this->paths[$type][$name] : NULL; + } + + public function setPaths($type, $name, $path) { + $this->paths[$type][$name] = $path; + } + + protected function fileValidUri($source) { + return isset($this->validUris[$source]) ? $this->validUris[$source] : FALSE; + } + + public function setFileValidUri($source, $valid) { + $this->validUris[$source] = $valid; + } + +} diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/css_js_settings.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/css_js_settings.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..fdb479ff1e298a147bc5fc78fe46d9ac306cbe31 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/css_js_settings.libraries.yml @@ -0,0 +1,8 @@ +example: + css: + base: + css/base.css: {} + js: + js/example.js: {} + settings: + key: value diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/css_weights.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/css_weights.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..fd0d6eca44cd5247861dc8c4088b45ee99297d94 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/css_weights.libraries.yml @@ -0,0 +1,22 @@ +example: + css: + theme: + css/theme__no_weight.css: {} + css/theme__weight.css: + weight: 29 + base: + css/base__no_weight.css: {} + css/base__weight.css: + weight: 97 + layout: + css/layout__no_weight.css: {} + css/layout__weight.css: + weight: 92 + component: + css/component__no_weight.css: {} + css/component__weight.css: + weight: 45 + state: + css/state__no_weight.css: {} + css/state__weight.css: + weight: 8 diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/data_types.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/data_types.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..eb817df18b8eb2ca292fde4053f37e315463d063 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/data_types.libraries.yml @@ -0,0 +1,13 @@ +example: + css: + theme: + # External URL. + 'http://example.com/test.css': + type: external + # Absolute path. + /tmp/test.css: {} + # Protocol free. + //cdn.com/test.css: {} + # Stream wrapper URI. + 'public://test.css': {} + 'example://test2.css': {} diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/dependencies.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/dependencies.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..9a8b4ddf2818e9b1efb4143dbbf8131936fc098d --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/dependencies.libraries.yml @@ -0,0 +1,6 @@ +example: + css: + css/example.js: {} + dependencies: + - external/example_external + - example_module/example diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/example_module.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/example_module.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..653438ce90d1d0cc846e714bf2b446c5a38e8b0a --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/example_module.libraries.yml @@ -0,0 +1,5 @@ +example: + version: VERSION + css: + theme: + css/example.css: {} diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/example_module_missing_information.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/example_module_missing_information.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..0495fb603f02669657356d82dc92947fd54c400e --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/example_module_missing_information.libraries.yml @@ -0,0 +1,2 @@ +example: + version: VERSION diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/example_theme.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/example_theme.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..62c13461e18581c424254205c2b7f2fbc3cdca45 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/example_theme.libraries.yml @@ -0,0 +1,4 @@ +example: + css: + theme: + css/example.css: {} diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/external.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/external.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..0486a7271970e02723e85965e16e318e9f075ce5 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/external.libraries.yml @@ -0,0 +1,5 @@ +example_external: + version: v3.14 + css: + theme: + css/example_external.css: {} diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/invalid_file.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/invalid_file.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..f62f2896e13b810f677530024d4d3bad05a97275 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/invalid_file.libraries.yml @@ -0,0 +1,3 @@ +example: + key1: value1 + key2: value2 diff --git a/core/tests/Drupal/Tests/Core/Asset/library_test_files/js_positive_weight.libraries.yml b/core/tests/Drupal/Tests/Core/Asset/library_test_files/js_positive_weight.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..9fc48142bef2ec5a1291005ee3301c239ad1b578 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Asset/library_test_files/js_positive_weight.libraries.yml @@ -0,0 +1,4 @@ +example: + js: + js/positive_weight.js: + weight: 10