Commit 871da5e4 authored by alexpott's avatar alexpott

Issue #2308745 by Alumei, dawehner, Wim Leers, larowlan, Arla, alexpott,...

Issue #2308745 by Alumei, dawehner, Wim Leers, larowlan, Arla, alexpott, g.oechsler, R.Muilwijk, Berdir, catch, klausi, clemens.tolboom, MattA, Crell: Remove rest.settings.yml, use rest_resource config entities
parent 9bb1c369
......@@ -4,6 +4,7 @@
use Drupal\Core\Cache\Context\CacheContextsPass;
use Drupal\Core\Cache\ListCacheBinsPass;
use Drupal\Core\DependencyInjection\Compiler\AuthenticationProviderPass;
use Drupal\Core\DependencyInjection\Compiler\BackendCompilerPass;
use Drupal\Core\DependencyInjection\Compiler\GuzzleMiddlewarePass;
use Drupal\Core\DependencyInjection\Compiler\ContextProvidersPass;
......@@ -90,6 +91,7 @@ public function register(ContainerBuilder $container) {
$container->addCompilerPass(new ListCacheBinsPass());
$container->addCompilerPass(new CacheContextsPass());
$container->addCompilerPass(new ContextProvidersPass());
$container->addCompilerPass(new AuthenticationProviderPass());
// Register plugin managers.
$container->addCompilerPass(new PluginManagerPass());
......
<?php
namespace Drupal\Core\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
* Registers the authentication_providers container parameter.
*/
class AuthenticationProviderPass implements CompilerPassInterface {
/**
* {@inheritdoc}
*/
public function process(ContainerBuilder $container) {
$authentication_providers = [];
foreach ($container->findTaggedServiceIds('authentication_provider') as $service_id => $attributes) {
$authentication_provider = $attributes[0]['provider_id'];
if ($provider_tag = $container->getDefinition($service_id)->getTag('_provider')) {
$authentication_providers[$authentication_provider] = $provider_tag[0]['provider'];
}
}
$container->setParameter('authentication_providers', $authentication_providers);
}
}
......@@ -131,7 +131,7 @@ function testQueryParameterFormatRequests() {
$this->assertRaw('{"content":"oh hai this is json"}', 'The correct Json response was returned.');
// Enable REST support for nodes and hal+json.
\Drupal::service('module_installer')->install(['node', 'rest', 'hal']);
\Drupal::service('module_installer')->install(['node', 'rest', 'hal', 'basic_auth']);
$this->drupalCreateContentType(['type' => 'article']);
$node = $this->drupalCreateNode(['type' => 'article']);
$node_uri = $node->urlInfo();
......
# Enable all methods on nodes.
# You must install Hal and Basic_auth modules for this to work. Also, if you are
# going to use Basic_auth in a production environment then you should consider
# setting up SSL.
# There are alternatives to Basic_auth in contrib such as OAuth module.
resources:
entity:node:
GET:
supported_formats:
- hal_json
supported_auth:
- basic_auth
POST:
supported_formats:
- hal_json
supported_auth:
- basic_auth
PATCH:
supported_formats:
- hal_json
supported_auth:
- basic_auth
DELETE:
supported_formats:
- hal_json
supported_auth:
- basic_auth
# Multiple formats and multiple authentication providers can be defined for a
# resource:
#
# resources:
# entity:node:
# GET:
# supported_formats:
# - json
# - hal_json
# - xml
# supported_auth:
# - cookie
# - basic_auth
#
# hal_json is the only format supported for POST and PATCH methods.
#
# The full documentation is located at
# https://www.drupal.org/documentation/modules/rest.
# Set the domain for REST type and relation links.
# If left blank, the site's domain will be used.
link_domain: ~
id: entity.node
plugin_id: 'entity:node'
granularity: method
configuration:
GET:
supported_formats:
- hal_json
supported_auth:
- basic_auth
POST:
supported_formats:
- hal_json
supported_auth:
- basic_auth
PATCH:
supported_formats:
- hal_json
supported_auth:
- basic_auth
DELETE:
supported_formats:
- hal_json
supported_auth:
- basic_auth
dependencies:
module:
- node
- basic_auth
- hal
# Schema for the configuration files of the REST module.
rest.settings:
type: config_object
label: 'REST settings'
mapping:
resources:
type: sequence
label: 'Resources'
sequence:
type: rest_resource
label: 'Resource'
link_domain:
type: string
label: 'Domain of the relation'
rest_resource:
# Method-level granularity of REST resource configuration.
# @todo Add resource-level granularity in https://www.drupal.org/node/2721595.
rest_resource.method:
type: mapping
mapping:
GET:
......@@ -45,3 +40,20 @@ rest_request:
sequence:
type: string
label: 'Authentication'
rest.resource.*:
type: config_entity
label: 'REST resource config'
mapping:
id:
type: string
label: 'REST resource config ID'
plugin_id:
type: string
label: 'REST resource plugin id'
granularity:
type: string
label: 'REST resource configuration granularity'
configuration:
type: rest_resource.[%parent.granularity]
label: 'REST resource configuration'
......@@ -21,3 +21,24 @@ function rest_requirements($phase) {
}
return $requirements;
}
/**
* @defgroup updates-8.1.x-to-8.2.x
* @{
* Update functions from 8.1.x to 8.2.x.
*/
/**
* Install the REST config entity type and fix old settings-based config.
*/
function rest_update_8201() {
\Drupal::entityDefinitionUpdateManager()->installEntityType(\Drupal::entityTypeManager()->getDefinition('rest_resource_config'));
\Drupal::state()->set('rest_update_8201_resources', \Drupal::config('rest.settings')->get('resources'));
\Drupal::configFactory()->getEditable('rest.settings')
->clear('resources')
->save();
}
/**
* @} End of "defgroup updates-8.1.x-to-8.2.x".
*/
permission_callbacks:
- Drupal\rest\RestPermissions::permissions
administer rest resources:
title: 'Administer REST resource configuration'
<?php
/**
* @file
* Post update functions for Rest.
*/
use Drupal\rest\Entity\RestResourceConfig;
use Drupal\rest\RestResourceConfigInterface;
/**
* @addtogroup updates-8.1.x-to-8.2.x
* @{
*/
/**
* Create REST resource configuration entities.
*
* @todo https://www.drupal.org/node/2721595 Automatically upgrade those REST
* resource config entities that have the same formats/auth mechanisms for all
* methods to "granular: resource".
*
* @see rest_update_8201()
*/
function rest_post_update_create_rest_resource_config_entities() {
$resources = \Drupal::state()->get('rest_update_8201_resources', []);
foreach ($resources as $key => $resource) {
$resource = RestResourceConfig::create([
'id' => str_replace(':', '.', $key),
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
'configuration' => $resource,
]);
$resource->save();
}
}
/**
* @} End of "addtogroup updates-8.1.x-to-8.2.x".
*/
......@@ -24,7 +24,7 @@ services:
arguments: ['@cache.default', '@entity.manager', '@module_handler', '@config.factory', '@request_stack']
rest.resource_routes:
class: Drupal\rest\Routing\ResourceRoutes
arguments: ['@plugin.manager.rest', '@config.factory', '@logger.channel.rest']
arguments: ['@plugin.manager.rest', '@entity_type.manager', '@logger.channel.rest']
tags:
- { name: 'event_subscriber' }
logger.channel.rest:
......
<?php
namespace Drupal\rest\Entity;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\rest\RestResourceConfigInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Calculates rest resource config dependencies.
*/
class ConfigDependencies implements ContainerInjectionInterface {
/**
* The serialization format providers, keyed by format.
*
* @var string[]
*/
protected $formatProviders;
/**
* The authentication providers, keyed by ID.
*
* @var string[]
*/
protected $authProviders;
/**
* Creates a new ConfigDependencies instance.
*
* @param string[] $format_providers
* The serialization format providers, keyed by format.
* @param string[] $auth_providers
* The authentication providers, keyed by ID.
*/
public function __construct(array $format_providers, array $auth_providers) {
$this->formatProviders = $format_providers;
$this->authProviders = $auth_providers;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->getParameter('serializer.format_providers'),
$container->getParameter('authentication_providers')
);
}
/**
* Calculates dependencies of a specific rest resource configuration.
*
* @param \Drupal\rest\RestResourceConfigInterface $rest_config
* The rest configuration.
*
* @return string[][]
* Dependencies keyed by dependency type.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies()
*/
public function calculateDependencies(RestResourceConfigInterface $rest_config) {
$granularity = $rest_config->get('granularity');
if ($granularity === RestResourceConfigInterface::METHOD_GRANULARITY) {
return $this->calculateDependenciesForMethodGranularity($rest_config);
}
else {
throw new \InvalidArgumentException("A different granularity then 'method' is not supported yet.");
// @todo Add resource-level granularity support in https://www.drupal.org/node/2721595.
}
}
/**
* Calculates dependencies of a specific rest resource configuration.
*
* @param \Drupal\rest\RestResourceConfigInterface $rest_config
* The rest configuration.
*
* @return string[][]
* Dependencies keyed by dependency type.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::calculateDependencies()
*/
protected function calculateDependenciesForMethodGranularity(RestResourceConfigInterface $rest_config) {
// The dependency lists for authentication providers and formats
// generated on container build.
$dependencies = [];
foreach (array_keys($rest_config->get('configuration')) as $request_method) {
// Add dependencies based on the supported authentication providers.
foreach ($rest_config->getAuthenticationProviders($request_method) as $auth) {
if (isset($this->authProviders[$auth])) {
$module_name = $this->authProviders[$auth];
$dependencies['module'][] = $module_name;
}
}
// Add dependencies based on the supported authentication formats.
foreach ($rest_config->getFormats($request_method) as $format) {
if (isset($this->formatProviders[$format])) {
$module_name = $this->formatProviders[$format];
$dependencies['module'][] = $module_name;
}
}
}
return $dependencies;
}
/**
* Informs the entity that entities it depends on will be deleted.
*
* @param \Drupal\rest\RestResourceConfigInterface $rest_config
* The rest configuration.
* @param array $dependencies
* An array of dependencies that will be deleted keyed by dependency type.
* Dependency types are, for example, entity, module and theme.
*
* @return bool
* TRUE if the entity has been changed as a result, FALSE if not.
*
* @see \Drupal\Core\Config\Entity\ConfigEntityInterface::onDependencyRemoval()
*/
public function onDependencyRemoval(RestResourceConfigInterface $rest_config, array $dependencies) {
$granularity = $rest_config->get('granularity');
if ($granularity === RestResourceConfigInterface::METHOD_GRANULARITY) {
return $this->onDependencyRemovalForMethodGranularity($rest_config, $dependencies);
}
else {
throw new \InvalidArgumentException("A different granularity then 'method' is not supported yet.");
// @todo Add resource-level granularity support in https://www.drupal.org/node/2721595.
}
}
/**
* Informs the entity that entities it depends on will be deleted.
*
* @param \Drupal\rest\RestResourceConfigInterface $rest_config
* The rest configuration.
* @param array $dependencies
* An array of dependencies that will be deleted keyed by dependency type.
* Dependency types are, for example, entity, module and theme.
*
* @return bool
* TRUE if the entity has been changed as a result, FALSE if not.
*/
protected function onDependencyRemovalForMethodGranularity(RestResourceConfigInterface $rest_config, array $dependencies) {
$changed = FALSE;
// Only module-related dependencies can be fixed. All other types of
// dependencies cannot, because they were not generated based on supported
// authentication providers or formats.
if (isset($dependencies['module'])) {
// Try to fix dependencies.
$removed_auth = array_keys(array_intersect($this->authProviders, $dependencies['module']));
$removed_formats = array_keys(array_intersect($this->formatProviders, $dependencies['module']));
$configuration_before = $configuration = $rest_config->get('configuration');
if (!empty($removed_auth) || !empty($removed_formats)) {
// Try to fix dependency problems by removing affected
// authentication providers and formats.
foreach (array_keys($rest_config->get('configuration')) as $request_method) {
foreach ($removed_formats as $format) {
if (in_array($format, $rest_config->getFormats($request_method))) {
$configuration[$request_method]['supported_formats'] = array_diff($configuration[$request_method]['supported_formats'], $removed_formats);
}
}
foreach ($removed_auth as $auth) {
if (in_array($auth, $rest_config->getAuthenticationProviders($request_method))) {
$configuration[$request_method]['supported_auth'] = array_diff($configuration[$request_method]['supported_auth'], $removed_auth);
}
}
if (empty($configuration[$request_method]['supported_auth'])) {
// Remove the key if there are no more authentication providers
// supported by this request method.
unset($configuration[$request_method]['supported_auth']);
}
if (empty($configuration[$request_method]['supported_formats'])) {
// Remove the key if there are no more formats supported by this
// request method.
unset($configuration[$request_method]['supported_formats']);
}
if (empty($configuration[$request_method])) {
// Remove the request method altogether if it no longer has any
// supported authentication providers or formats.
unset($configuration[$request_method]);
}
}
}
if (!empty($configuration_before != $configuration)) {
$rest_config->set('configuration', $configuration);
// Only mark the dependencies problems as fixed if there is any
// configuration left.
$changed = TRUE;
}
}
// If the dependency problems are not marked as fixed at this point they
// should be related to the resource plugin and the config entity should
// be deleted.
return $changed;
}
}
<?php
namespace Drupal\rest\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
use Drupal\rest\RestResourceConfigInterface;
/**
* Defines a RestResourceConfig configuration entity class.
*
* @ConfigEntityType(
* id = "rest_resource_config",
* label = @Translation("REST resource configuration"),
* config_prefix = "resource",
* admin_permission = "administer rest resources",
* label_callback = "getLabelFromPlugin",
* entity_keys = {
* "id" = "id"
* },
* config_export = {
* "id",
* "plugin_id",
* "granularity",
* "configuration"
* }
* )
*/
class RestResourceConfig extends ConfigEntityBase implements RestResourceConfigInterface {
/**
* The REST resource config id.
*
* @var string
*/
protected $id;
/**
* The REST resource plugin id.
*
* @var string
*/
protected $plugin_id;
/**
* The REST resource configuration granularity.
*
* @todo Currently only 'method', but https://www.drupal.org/node/2721595 will add 'resource'
*
* @var string
*/
protected $granularity;
/**
* The REST resource configuration.
*
* @var array
*/
protected $configuration;
/**
* The rest resource plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $pluginManager;
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type) {
parent::__construct($values, $entity_type);
// The config entity id looks like the plugin id but uses __ instead of :
// because : is not valid for config entities.
if (!isset($this->plugin_id) && isset($this->id)) {
// Generate plugin_id on first entity creation.
$this->plugin_id = str_replace('.', ':', $this->id);
}
}
/**
* The label callback for this configuration entity.
*
* @return string The label.
*/
protected function getLabelFromPlugin() {
$plugin_definition = $this->getResourcePluginManager()
->getDefinition(['id' => $this->plugin_id]);
return $plugin_definition['label'];
}
/**
* Returns the resource plugin manager.
*
* @return \Drupal\Component\Plugin\PluginManagerInterface
*/
protected function getResourcePluginManager() {
if (!isset($this->pluginManager)) {
$this->pluginManager = \Drupal::service('plugin.manager.rest');
}
return $this->pluginManager;
}
/**
* {@inheritdoc}
*/
public function getResourcePlugin() {
return $this->getPluginCollections()['resource']->get($this->plugin_id);
}
/**
* {@inheritdoc}
*/
public function getMethods() {
if ($this->granularity === RestResourceConfigInterface::METHOD_GRANULARITY) {
return $this->getMethodsForMethodGranularity();
}
else {
throw new \InvalidArgumentException("A different granularity then 'method' is not supported yet.");
// @todo Add resource-level granularity support in https://www.drupal.org/node/2721595.
}
}
/**
* Retrieves a list of supported HTTP methods for this resource.
*
* @return string[]
* A list of supported HTTP methods.
*/
protected function getMethodsForMethodGranularity() {
$methods = array_keys($this->configuration);
return array_map([$this, 'normalizeRestMethod'], $methods);
}
/**
* {@inheritdoc}
*/
public function getAuthenticationProviders($method) {
if ($this->granularity === RestResourceConfigInterface::METHOD_GRANULARITY) {
return $this->getAuthenticationProvidersForMethodGranularity($method);
}
else {
throw new \InvalidArgumentException("A different granularity then 'method' is not supported yet.");
// @todo Add resource-level granularity support in https://www.drupal.org/node/2721595.
}
}
/**
* Retrieves a list of supported authentication providers.
*
* @param string $method
* The request method e.g GET or POST.
*
* @return string[]
* A list of supported authentication provider IDs.
*/
public function getAuthenticationProvidersForMethodGranularity($method) {
$method = $this->normalizeRestMethod($method);
if (in_array($method, $this->getMethods()) && isset($this->configuration[$method]['supported_auth'])) {
return $this->configuration[$method]['supported_auth'];
}
return [];
}
/**
* {@inheritdoc}
*/
public function getFormats($method) {
if ($this->granularity === RestResourceConfigInterface::METHOD_GRANULARITY) {
return $this->getFormatsForMethodGranularity($method);
}
else {
throw new \InvalidArgumentException("A different granularity then 'method' is not supported yet.");
// @todo Add resource-level granularity support in https://www.drupal.org/node/2721595.
}
}
/**
* Retrieves a list of supported response formats.
*
* @param string $method
* The request method e.g GET or POST.
*
* @return string[]
* A list of supported format IDs.
*/
protected function getFormatsForMethodGranularity($method) {
$method = $this->normalizeRestMethod($method);
if (in_array($method, $this->getMethods()) && isset($this->configuration[$method]['supported_formats'])) {
return $this->configuration[$method]['supported_formats'];
}
return [];
}
/**
* {@inheritdoc}
*/
public function getPluginCollections() {
return [
'resource' => new DefaultSingleLazyPluginCollection($this->getResourcePluginManager(), $this->plugin_id, [])
];
}
/**
* (@inheritdoc)
*/
public function calculateDependencies() {
parent::calculateDependencies();
foreach ($this->getRestResourceDependencies()->calculateDependencies($this) as $type => $dependencies) {
foreach ($dependencies as $dependency) {
$this->addDependency($type, $dependency);
}
}
return $this;
}
/**
* {@inheritdoc}
*/
public function onDependencyRemoval(array $dependencies) {
$parent = parent::onDependencyRemoval($dependencies);
// If the dependency problems are not marked as fixed at this point they
// should be related to the resource plugin and the config entity should
// be deleted.
$changed = $this->getRestResourceDependencies()->onDependencyRemoval($this, $dependencies);
return $parent || $changed;
}
/**
* Returns the REST resource dependencies.
*
* @return \Drupal\rest\Entity\ConfigDependencies
*/
protected function getRestResourceDependencies() {
return \Drupal::service('class_resolver')->getInstanceFromDefinition(ConfigDependencies::class);
}
/**
* Normalizes the method to upper case and check validity.
*
* @param string $method
* The request method.
*
* @return string
* The normalised request method.
*