Skip to content
Snippets Groups Projects
Commit 90a19373 authored by Tobias Zimmermann's avatar Tobias Zimmermann
Browse files

Issue #2770983 by tstoeckler, rjacobs: Fetch definitions remotely and

store them locally.
parent a979845f
No related branches found
No related tags found
No related merge requests found
Showing
with 666 additions and 17 deletions
Libraries 8.x-3.x, xxxx-xx-xx
-----------------------------
#2770983 by tstoeckler, rjacobs: Fetch definitions remotely and store them locally.
#2770983 by tstoeckler: Make Libraries API work on Windows.
#2770983 by tstoeckler: Allow libraries to be symlinked to their right place.
#2756265 by rajeshwari10: Replace deprecated usage of SafeMarkup::checkPlain().
......
library_definitions:
definition:
local:
# @todo Reconsider having library definitions be webserver-writable and consider implementing a stream wrapper that
# finds library definitions in e.g. sites/all/libraries.
# @todo Implement a stream wrapper that finds library definitions in e.g.
# sites/all/libraries.
path: 'public://library-definitions'
remote:
# @todo Enable this by default, when there is an actual canonical registry.
enable: false
url: ''
enable: TRUE
# @todo Use a less "hidden" path, when available.
url: 'http://cgit.drupalcode.org/sandbox-rjacobs-2761167/plain/registry'
......@@ -4,7 +4,7 @@ libraries.settings:
type: config_object
title: 'Libraries API settings'
mapping:
library_definitions:
definition:
type: mapping
title: 'Library definition settings'
mapping:
......
<?php
/**
* @file
* Containsinstall, uninstall and update functions for Libraries API.
*/
use Drupal\libraries\ExternalLibrary\Definition\FileDefinitionDiscovery;
/**
* Implements hook_install().
*/
function libraries_install() {
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
$file_system->mkdir('public://library-definitions');
}
/**
* Implements hook_uninstall().
*/
function libraries_uninstall() {
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
$file_system = \Drupal::service('file_system');
$file_system->rmdir('public://library-definitions');
}
\ No newline at end of file
......@@ -2,12 +2,27 @@ services:
libraries.manager:
class: Drupal\libraries\ExternalLibrary\LibraryManager
arguments:
- '@libraries.definitions.discovery'
- '@libraries.definition.discovery'
- '@plugin.manager.libraries.library_type'
- '@libraries.extension_handler'
libraries.definitions.discovery:
class: Drupal\libraries\ExternalLibrary\Definition\StreamDefinitionDiscovery
# These services are modified depending on the values of the
# 'libraries.settings' configuration object. See LibrariesServiceProvider.
libraries.definition.discovery:
class: Drupal\libraries\ExternalLibrary\Definition\ChainDefinitionDiscovery
arguments: ['@serialization.yaml']
calls:
- [addDiscovery, ['@libraries.definition.discovery.local']]
libraries.definition.discovery.local:
class: Drupal\libraries\ExternalLibrary\Definition\WritableFileDefinitionDiscovery
arguments: ['@serialization.yaml', 'public://library-definitions']
libraries.definition.discovery.remote:
class: Drupal\libraries\ExternalLibrary\Definition\GuzzleDefinitionDiscovery
arguments:
- '@http_client'
- '@serialization.json'
# @todo Use a less "hidden" path, when available.
- 'http://cgit.drupalcode.org/sandbox-rjacobs-2761167/plain/registry'
plugin.manager.libraries.library_type:
class: Drupal\libraries\ExternalLibrary\Type\LibraryTypeFactory
......@@ -19,6 +34,12 @@ services:
class: Drupal\libraries\ExternalLibrary\Version\VersionDetectorManager
parent: default_plugin_manager
libraries.config_subscriber:
class: Drupal\libraries\Config\LibrariesConfigSubscriber
arguments: ['@kernel']
tags:
- { name: event_subscriber }
libraries.extension_handler:
class: Drupal\libraries\Extension\ExtensionHandler
arguments: ['%app.root', '@module_handler', '@theme_handler']
......
<?php
namespace Drupal\libraries\Config;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\DrupalKernelInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Reacts to configuration changes of the 'libraries.settings' configuration.
*/
class LibrariesConfigSubscriber implements EventSubscriberInterface {
/**
* The Drupal kernel.
*
* @var \Drupal\Core\DrupalKernelInterface
*/
protected $kernel;
/**
* Constructs a Libraries API configuration subscriber.
*
* @param \Drupal\Core\DrupalKernelInterface $kernel
* The Drupal kernel.
*/
public function __construct(DrupalKernelInterface $kernel) {
$this->kernel = $kernel;
}
/**
* Invalidates the container when the definition settings are updated.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The configuration event.
*/
public function onConfigSave(ConfigCrudEvent $event) {
if (($event->getConfig()->getName() === 'libraries.settings') && $event->isChanged('definition')) {
$this->kernel->invalidateContainer();
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
return [ConfigEvents::SAVE => 'onConfigSave'];
}
}
<?php
namespace Drupal\libraries\ExternalLibrary\Definition;
use Drupal\libraries\ExternalLibrary\Exception\LibraryDefinitionNotFoundException;
/**
* Provides a definition discovery that checks a list of other discoveries.
*
* The discoveries are checked sequentially. If the definition was not present
* in some discoveries but is found in a later discovery the definition will be
* written to the earlier discoveries if they implement
* WritableDefinitionDiscoveryInterface.
*
* @see \Drupal\libraries\ExternalLibrary\Definition\WritableDefinitionDiscoveryInterface
*/
class ChainDefinitionDiscovery implements DefinitionDiscoveryInterface {
/**
* The list of definition discoveries that will be checked.
*
* @var \Drupal\libraries\ExternalLibrary\Definition\DefinitionDiscoveryInterface[]
*/
protected $discoveries = [];
/**
* {@inheritdoc}
*/
public function hasDefinition($id) {
foreach ($this->discoveries as $discovery) {
if ($discovery->hasDefinition($id)) {
return TRUE;
}
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function getDefinition($id) {
/** @var \Drupal\libraries\ExternalLibrary\Definition\WritableDefinitionDiscoveryInterface[] $discoveries_to_write */
$discoveries_to_write = [];
foreach ($this->discoveries as $discovery) {
if ($discovery->hasDefinition($id)) {
$definition = $discovery->getDefinition($id);
break;
}
elseif ($discovery instanceof WritableDefinitionDiscoveryInterface) {
$discoveries_to_write[] = $discovery;
}
}
if (!isset($definition)) {
throw new LibraryDefinitionNotFoundException($id);
}
foreach ($discoveries_to_write as $discovery_to_write) {
$discovery_to_write->writeDefinition($id, $definition);
}
return $definition;
}
/**
* Adds a definition discovery to the list to check.
*
* @param \Drupal\libraries\ExternalLibrary\Definition\DefinitionDiscoveryInterface $discovery
* The definition discovery to add.
*
* @return $this
*/
public function addDiscovery(DefinitionDiscoveryInterface $discovery) {
$this->discoveries[] = $discovery;
return $this;
}
}
<?php
namespace Drupal\libraries\ExternalLibrary\Definition;
use Drupal\Component\Serialization\SerializationInterface;
use Drupal\libraries\ExternalLibrary\Exception\LibraryDefinitionNotFoundException;
/**
* Provides a libraries definition discovery using PHP's native file functions.
*
* It supports either a URI with a stream wrapper, an absolute file path or a
* file path relative to the Drupal root as a base URI.
*
* By default YAML files are used.
*
* @see \Drupal\libraries\StreamWrapper\LibraryDefinitionsStream
*
* @ingroup libraries
*/
class FileDefinitionDiscovery extends FileDefinitionDiscoveryBase implements DefinitionDiscoveryInterface {
/**
* {@inheritdoc}
*/
public function hasDefinition($id) {
return file_exists($this->getFileUri($id));
}
/**
* {@inheritdoc}
*/
protected function getSerializedDefinition($id) {
return file_get_contents($this->getFileUri($id));
}
}
......@@ -6,25 +6,14 @@ use Drupal\Component\Serialization\SerializationInterface;
use Drupal\libraries\ExternalLibrary\Exception\LibraryDefinitionNotFoundException;
/**
* Provides a stream-based implementation of a libraries definition discovery.
* Provides a base implementation for file-based definition discoveries.
*
* Given a library ID of 'example', it reads the library definition from the URI
* 'library-definitions://example.yml'. See LibraryDefinitionsStream for more
* information.
*
* Using a stream wrapper has the benefit of being able to swap out the specific
* storage implementation without any other part of the code needing to change.
* For example, the specific directory which holds the library definitions on
* disk can be changed, multiple directories can be layered as though they were
* one, or the library definitions can even be read from a remote location
* without any part of the code other than the stream wrapper implementation
* itself needing to change.
*
* @see \Drupal\libraries\StreamWrapper\LibraryDefinitionsStream
*
* @ingroup libraries
* This discovery assumes that library files contain the serialized library
* definition and are accessible under a common base URI. The expected library
* file URI will be constructed from this by appending '/$id.$extension' to
* this, where $id is the library ID and $extension is the serializer extension.
*/
class StreamDefinitionDiscovery implements DefinitionDiscoveryInterface {
abstract class FileDefinitionDiscoveryBase implements DefinitionDiscoveryInterface {
/**
* The serializer for the library definition files.
......@@ -34,52 +23,47 @@ class StreamDefinitionDiscovery implements DefinitionDiscoveryInterface {
protected $serializer;
/**
* The scheme of the stream to use for library definitions.
* The base URI for the library files.
*
* @var string
*/
protected $scheme = 'library-definitions';
protected $baseUri;
/**
* Constructs a stream-based library definition discovery.
*
* @param \Drupal\Component\Serialization\SerializationInterface $serializer
* The serializer for the library definition files.
* @param string $base_uri
* The base URI for the library files.
*/
public function __construct(SerializationInterface $serializer) {
public function __construct(SerializationInterface $serializer, $base_uri) {
$this->serializer = $serializer;
$this->baseUri = $base_uri;
}
/**
* Checks whether a library definition exists for the given ID.
*
* @param string $id
* The library ID to check for.
*
* @return bool
* TRUE if the library definition exists; FALSE otherwise.
* {@inheritdoc}
*/
public function hasDefinition($id) {
return file_exists($this->getFileUri($id));
public function getDefinition($id) {
if (!$this->hasDefinition($id)) {
throw new LibraryDefinitionNotFoundException($id);
}
return $this->serializer->decode($this->getSerializedDefinition($id));
}
/**
* Returns the library definition for the given ID.
* Gets the contents of the library file.
*
* @param string $id
* The library ID to retrieve the definition for.
* @param $id
* The library ID to retrieve the serialized definition for.
*
* @return array
* The library definition array parsed from the definition JSON file.
* @return string
* The serialized library definition.
*
* @throws \Drupal\libraries\ExternalLibrary\Exception\LibraryDefinitionNotFoundException
*/
public function getDefinition($id) {
if (!$this->hasDefinition($id)) {
throw new LibraryDefinitionNotFoundException($id);
}
return $this->serializer->decode(file_get_contents($this->getFileUri($id)));
}
abstract protected function getSerializedDefinition($id);
/**
* Returns the file URI of the library definition file for a given library ID.
......@@ -92,7 +76,7 @@ class StreamDefinitionDiscovery implements DefinitionDiscoveryInterface {
*/
protected function getFileUri($id) {
$filename = $id . '.' . $this->serializer->getFileExtension();
return "$this->scheme://$filename";
return "$this->baseUri/$filename";
}
}
<?php
namespace Drupal\libraries\ExternalLibrary\Definition;
use Drupal\Component\Serialization\SerializationInterface;
use Drupal\libraries\ExternalLibrary\Exception\LibraryDefinitionNotFoundException;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;
/**
* Provides a definition discovery that fetches remote definitions using Guzzle.
*
* By default JSON files are assumed to be in JSON format.
*
* @todo Cache responses statically by ID to avoid multiple HTTP requests when
* calling hasDefinition() and getDefinition() sequentially.
*/
class GuzzleDefinitionDiscovery extends FileDefinitionDiscoveryBase implements DefinitionDiscoveryInterface {
/**
* The HTTP client.
*
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* Constructs a Guzzle-based definition discvoery.
*
* @param \GuzzleHttp\ClientInterface $http_client
* The HTTP client.
* @param \Drupal\Component\Serialization\SerializationInterface $serializer
* The serializer for the library definition files.
* @param string $base_url
* The base URL for the library files.
*/
public function __construct(ClientInterface $http_client, SerializationInterface $serializer, $base_url) {
parent::__construct($serializer, $base_url);
$this->httpClient = $http_client;
}
/**
* {@inheritdoc}
*/
public function hasDefinition($id) {
try {
$response = $this->httpClient->request('GET', $this->getFileUri($id));
return $response->getStatusCode() === 200;
}
catch (GuzzleException $exception) {
return FALSE;
}
}
/**
* {@inheritdoc}
*/
protected function getSerializedDefinition($id) {
try {
$response = $this->httpClient->request('GET', $this->getFileUri($id));
return $response->getBody()->getContents();
}
catch (GuzzleException $exception) {
throw new LibraryDefinitionNotFoundException($id, '', 0, $exception);
}
catch (\RuntimeException $exception) {
throw new LibraryDefinitionNotFoundException($id, '', 0, $exception);
}
}
}
<?php
namespace Drupal\libraries\ExternalLibrary\Definition;
/**
* Provides an interface for library definition discoveries that are writable.
*
* @see \Drupal\libraries\ExternalLibrary\Definition\DefinitionDiscoveryInterface
* @see \Drupal\libraries\ExternalLibrary\Definition\ChainDefinitionDiscovery
*/
interface WritableDefinitionDiscoveryInterface extends DefinitionDiscoveryInterface {
/**
* Writes a library definition persistently.
*
* @param string $id
* The library ID.
* @param array $definition
* The library definition to write.
*
* @return $this
*/
public function writeDefinition($id, $definition);
}
<?php
namespace Drupal\libraries\ExternalLibrary\Definition;
/**
* Provides a definition discovery based on a writable directory or stream.
*
* @see \Drupal\libraries\ExternalLibrary\Definition\FileDefinitionDiscovery
*/
class WritableFileDefinitionDiscovery extends FileDefinitionDiscovery implements WritableDefinitionDiscoveryInterface {
/**
* {@inheritdoc}
*/
public function writeDefinition($id, $definition) {
file_put_contents($this->getFileUri($id), $this->serializer->encode($definition));
return $this;
}
}
<?php
namespace Drupal\libraries;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceModifierInterface;
use Symfony\Component\DependencyInjection\Reference;
/**
* Modifies Libraries API services based on configuration.
*/
class LibrariesServiceProvider implements ServiceModifierInterface {
/**
* {@inheritdoc}
*/
public function alter(ContainerBuilder $container) {
if ($container->has('config.factory')) {
// The configuration factory depends on the cache factory, but that
// depends on the 'cache_default_bin_backends' parameter that has not yet
// been set by \Drupal\Core\Cache\ListCacheBinsPass::process() at this
// point.
$parameter_name = 'cache_default_bin_backends';
if (!$container->hasParameter($parameter_name)) {
$container->setParameter($parameter_name, []);
}
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
$config_factory = $container->get('config.factory');
$config = $config_factory->get('libraries.settings');
if (!$config->isNew()) {
// Set the local definition path.
$container
->getDefinition('libraries.definition.discovery.local')
->replaceArgument(1, $config->get('definition.local.path'));
// Set the remote definition URL. Note that this is set even if
// the remote discovery is not enabled below in case the
// 'libraries.definition.discovery.remote' service is used explicitly.
$container
->getDefinition('libraries.definition.discovery.remote')
->replaceArgument(2, $config->get('definition.remote.url'));
// Because it is less convenient to remove a method call than to add
// one, the remote discovery is not registered in libraries.services.yml
// and instead added here, even though the 'definition.remote.enable'
// configuration value is TRUE by default.
if ($config->get('definition.remote.enable')) {
// Add the remote discovery to the list of chained discoveries.
$container
->getDefinition('libraries.definition.discovery')
->addMethodCall('addDiscovery', [new Reference('libraries.definition.discovery.remote')]);
}
}
// At this point the event dispatcher has not yet been populated with
// event subscribers by RegisterEventSubscribersPass::process() but has
// already bin injected in the configuration factory. Reset those services
// accordingly.
$container->set('event_dispatcher', NULL);
$container->set('config.factory', NULL);
}
}
}
......@@ -80,7 +80,7 @@ class LibraryDefinitionsStream extends LocalStream {
protected function getConfig($key) {
return $this->configFactory
->get('libraries.settings')
->get("library_definitions.$key");
->get("definitions.$key");
}
}
{
"type": "asset",
"version_detector": {
"id": "static",
"configuration": {
"version": "1.0.0"
}
},
"remote_url": "http://example.com",
"css": {
"base": {
"example.css": {}
}
},
"js": {
"example.js": {}
}
}
{
"type": "asset_multiple",
"version_detector": {
"id": "static",
"configuration": {
"version": "1.0.0"
}
},
"remote_url": "http://example.com",
"libraries": {
"first": {
"css": {
"base": {
"example.first.css": {}
}
},
"js": {
"example.first.js": {}
}
},
"second": {
"css": {
"base": {
"example.second.css": {}
}
},
"js": {
"example.second.js": {}
}
}
}
}
{
"type": "php_file",
"files": [
"test_php_file_library.php"
]
}
<?php
namespace Drupal\Tests\libraries\Functional\ExternalLibrary\Definition;
use Drupal\Tests\BrowserTestBase;
/**
* Tests that remote library definitions are found and downloaded.
*
* This is a browser test because Guzzle is not usable from a kernel test.
*
* @group libraries
*
* @todo Make this a kernel test when https://www.drupal.org/node/2571475 is in.
*/
class ChainDefinitionDiscoveryTest extends BrowserTestBase {
/**
* The chained library definition discovery.
*
* @var \Drupal\libraries\ExternalLibrary\Definition\DefinitionDiscoveryInterface
*/
protected $discovery;
/**
* The local library definition discovery.
*
* @var \Drupal\libraries\ExternalLibrary\Definition\DefinitionDiscoveryInterface
*/
protected $localDiscovery;
/**
* The remote library definition discovery.
*
* @var \Drupal\libraries\ExternalLibrary\Definition\DefinitionDiscoveryInterface
*/
protected $remoteDiscovery;
/**
* {@inheritdoc}
*/
public static $modules = ['libraries'];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
/** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
$module_handler = $this->container->get('module_handler');
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
$config_factory = $this->container->get('config.factory');
// Set up the remote library definition URL to point to the local website.
$base_url = getenv('SIMPLETEST_BASE_URL');
$module_path = $module_handler->getModule('libraries')->getPath();
$url = "$base_url/$module_path/tests/library_definitions";
$config_factory->getEditable('libraries.settings')
->set('definition.remote.url', $url)
->save();
// LibrariesConfigSubscriber::onConfigSave() invalidates the container so
// that it is rebuilt on the next request. We need the container rebuilt
// immediately, however.
$this->rebuildContainer();
$this->discovery = $this->container->get('libraries.definition.discovery');
$this->localDiscovery = $this->container->get('libraries.definition.discovery.local');
$this->remoteDiscovery = $this->container->get('libraries.definition.discovery.remote');
}
/**
* Tests that remote definitions are written locally.
*/
public function testRemoteFetching() {
$library_id = 'test_asset_library';
$expected_definition = [
'type' => 'asset',
'version_detector' => [
'id' => 'static',
'configuration' => [
'version' => '1.0.0'
],
],
'remote_url' => 'http://example.com',
'css' => [
'base' => [
'example.css' => [],
],
],
'js' => [
'example.js' => [],
],
];
$this->assertFalse($this->localDiscovery->hasDefinition($library_id));
$this->assertTrue($this->remoteDiscovery->hasDefinition($library_id));
$this->assertEquals($this->remoteDiscovery->getDefinition($library_id), $expected_definition);
$this->assertTrue($this->discovery->hasDefinition($library_id));
$this->assertEquals($this->discovery->getDefinition($library_id), $expected_definition);
$this->assertTrue($this->localDiscovery->hasDefinition($library_id));
$this->assertEquals($this->localDiscovery->getDefinition($library_id), $expected_definition);
}
}
......@@ -53,19 +53,29 @@ abstract class LibraryTypeKernelTestBase extends KernelTestBase {
protected function setUp() {
parent::setUp();
$this->libraryManager = $this->container->get('libraries.manager');
$this->libraryTypeFactory = $this->container->get('plugin.manager.libraries.library_type');
/** @var \Drupal\Core\Extension\ModuleHandlerInterface $module_handler */
$module_handler = $this->container->get('module_handler');
$this->modulePath = $module_handler->getModule('libraries')->getPath();
$this->installConfig('libraries');
// Disable remote definition fetching and set the local definitions path to
// the module directory.
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
$config_factory = $this->container->get('config.factory');
$config_factory->getEditable('libraries.settings')
->set('library_definitions.local.path', "{$this->modulePath}/tests/library_definitions")
->set('definition.local.path', "{$this->modulePath}/tests/library_definitions")
->set('definition.remote.enable', FALSE)
->save();
// LibrariesConfigSubscriber::onConfigSave() invalidates the container so
// that it is rebuilt on the next request. We need the container rebuilt
// immediately, however.
/** @var \Drupal\Core\DrupalKernelInterface $kernel */
$kernel = $this->container->get('kernel');
$this->container = $kernel->rebuildContainer();
$this->libraryManager = $this->container->get('libraries.manager');
$this->libraryTypeFactory = $this->container->get('plugin.manager.libraries.library_type');
}
/**
......@@ -96,11 +106,9 @@ abstract class LibraryTypeKernelTestBase extends KernelTestBase {
}
catch (LibraryDefinitionNotFoundException $exception) {
$this->fail();
$this->fail("Missing library definition for test $type_id library.");
}
catch (LibraryTypeNotFoundException $exception) {
$this->fail();
$this->fail("Missing library type declaration for test $type_id library.");
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment