diff --git a/core/lib/Drupal/Core/CoreServiceProvider.php b/core/lib/Drupal/Core/CoreServiceProvider.php index 9117891..013dec1 100644 --- a/core/lib/Drupal/Core/CoreServiceProvider.php +++ b/core/lib/Drupal/Core/CoreServiceProvider.php @@ -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()); diff --git a/core/lib/Drupal/Core/DependencyInjection/Compiler/AuthenticationProviderPass.php b/core/lib/Drupal/Core/DependencyInjection/Compiler/AuthenticationProviderPass.php new file mode 100644 index 0000000..20708e1 --- /dev/null +++ b/core/lib/Drupal/Core/DependencyInjection/Compiler/AuthenticationProviderPass.php @@ -0,0 +1,27 @@ +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); + } + +} diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php index 9194e65..64e4ee9 100644 --- a/core/modules/page_cache/src/Tests/PageCacheTest.php +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php @@ -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(); diff --git a/core/modules/rest/config/install/rest.settings.yml b/core/modules/rest/config/install/rest.settings.yml index d3aa82b..2d8185e 100644 --- a/core/modules/rest/config/install/rest.settings.yml +++ b/core/modules/rest/config/install/rest.settings.yml @@ -1,50 +1,3 @@ -# 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: ~ diff --git a/core/modules/rest/config/optional/rest.resource.entity.node.yml b/core/modules/rest/config/optional/rest.resource.entity.node.yml new file mode 100644 index 0000000..0cf4d78 --- /dev/null +++ b/core/modules/rest/config/optional/rest.resource.entity.node.yml @@ -0,0 +1,29 @@ +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 diff --git a/core/modules/rest/config/schema/rest.schema.yml b/core/modules/rest/config/schema/rest.schema.yml index 8014b31..41bc2bf 100644 --- a/core/modules/rest/config/schema/rest.schema.yml +++ b/core/modules/rest/config/schema/rest.schema.yml @@ -1,20 +1,15 @@ # 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' diff --git a/core/modules/rest/rest.install b/core/modules/rest/rest.install index 4bca69b..1bfa778 100644 --- a/core/modules/rest/rest.install +++ b/core/modules/rest/rest.install @@ -5,6 +5,9 @@ * Install, update and uninstall functions for the rest module. */ +use Drupal\rest\Entity\RestResourceConfig; +use Drupal\rest\RestResourceConfigInterface; + /** * Implements hook_requirements(). */ @@ -21,3 +24,33 @@ function rest_requirements($phase) { } return $requirements; } + +/** + * @defgroup updates-8.1.x-to-8.2.x Updates from 8.1.x to 8.2.x + * @{ + * Update functions from 8.1.x to 8.2.x. + */ + +/** + * Install the REST config entity type and convert old settings-based config. + * + * @todo Automatically upgrade those REST resource config entities that have the same formats/auth mechanisms for all methods to "granular: resource" in https://www.drupal.org/node/2721595. + */ +function rest_update_8201() { + \Drupal::entityDefinitionUpdateManager()->installEntityType(\Drupal::entityTypeManager()->getDefinition('rest_resource_config')); + foreach (\Drupal::config('rest.settings')->get('resources') as $key => $resource) { + $resource = RestResourceConfig::create([ + 'id' => str_replace(':', '.', $key), + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => $resource, + ]); + $resource->save(); + } + \Drupal::configFactory()->getEditable('rest.settings') + ->clear('resources') + ->save(); +} + +/** + * @} End of "defgroup updates-8.1.x-to-8.2.x". + */ diff --git a/core/modules/rest/rest.permissions.yml b/core/modules/rest/rest.permissions.yml index 2ab7154..171a284 100644 --- a/core/modules/rest/rest.permissions.yml +++ b/core/modules/rest/rest.permissions.yml @@ -1,2 +1,5 @@ permission_callbacks: - Drupal\rest\RestPermissions::permissions + +administer rest resources: + title: 'Administer REST resource configuration' diff --git a/core/modules/rest/rest.services.yml b/core/modules/rest/rest.services.yml index 6b613e3..c702719 100644 --- a/core/modules/rest/rest.services.yml +++ b/core/modules/rest/rest.services.yml @@ -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: diff --git a/core/modules/rest/src/Entity/ConfigDependencies.php b/core/modules/rest/src/Entity/ConfigDependencies.php new file mode 100644 index 0000000..72fc8cc --- /dev/null +++ b/core/modules/rest/src/Entity/ConfigDependencies.php @@ -0,0 +1,199 @@ +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; + } + +} diff --git a/core/modules/rest/src/Entity/RestResourceConfig.php b/core/modules/rest/src/Entity/RestResourceConfig.php new file mode 100644 index 0000000..f2b8b0e --- /dev/null +++ b/core/modules/rest/src/Entity/RestResourceConfig.php @@ -0,0 +1,261 @@ +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. + * + * @throws \InvalidArgumentException + * If the method is not supported. + */ + protected function normalizeRestMethod($method) { + $valid_methods = ['GET', 'POST', 'PATCH', 'DELETE']; + $normalised_method = strtoupper($method); + if (!in_array($normalised_method, $valid_methods)) { + throw new \InvalidArgumentException('The method is not supported.'); + } + return $normalised_method; + } + +} diff --git a/core/modules/rest/src/Plugin/ResourceBase.php b/core/modules/rest/src/Plugin/ResourceBase.php index 33cb3aa..549ac54 100644 --- a/core/modules/rest/src/Plugin/ResourceBase.php +++ b/core/modules/rest/src/Plugin/ResourceBase.php @@ -194,8 +194,6 @@ protected function getBaseRoute($canonical_path, $method) { $route = new Route($canonical_path, array( '_controller' => 'Drupal\rest\RequestHandler::handle', - // Pass the resource plugin ID along as default property. - '_plugin' => $this->pluginId, ), array( '_permission' => "restful $lower_method $this->pluginId", ), diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index b01033f..7e620df 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -2,6 +2,7 @@ namespace Drupal\rest\Plugin\rest\resource; +use Drupal\Component\Plugin\DependentPluginInterface; use Drupal\Core\Config\Entity\ConfigEntityType; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; @@ -9,9 +10,9 @@ use Drupal\Core\Entity\EntityStorageException; use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; -use Drupal\rest\ModifiedResourceResponse; use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\rest\ModifiedResourceResponse; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -32,14 +33,14 @@ * } * ) */ -class EntityResource extends ResourceBase { +class EntityResource extends ResourceBase implements DependentPluginInterface { /** - * The entity type manager. + * The entity type targeted by this resource. * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface + * @var \Drupal\Core\Entity\EntityTypeInterface */ - protected $entityTypeManager; + protected $entityType; /** * Constructs a Drupal\rest\Plugin\rest\resource\EntityResource object. @@ -50,16 +51,16 @@ class EntityResource extends ResourceBase { * The plugin_id for the plugin instance. * @param mixed $plugin_definition * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager * @param array $serializer_formats * The available serialization formats. * @param \Psr\Log\LoggerInterface $logger * A logger instance. - * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager - * The entity type manager. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, $serializer_formats, LoggerInterface $logger) { parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger); - $this->entityTypeManager = $entity_type_manager; + $this->entityType = $entity_type_manager->getDefinition($plugin_definition['entity_type']); } /** @@ -70,9 +71,9 @@ public static function create(ContainerInterface $container, array $configuratio $configuration, $plugin_id, $plugin_definition, + $container->get('entity_type.manager'), $container->getParameter('serializer.formats'), - $container->get('logger.factory')->get('rest'), - $container->get('entity_type.manager') + $container->get('logger.factory')->get('rest') ); } @@ -331,8 +332,16 @@ public function availableMethods() { * TRUE if the entity is a Config Entity, FALSE otherwise. */ protected function isConfigEntityResource() { - $entity_type_id = $this->getPluginDefinition()['entity_type']; - return $this->entityTypeManager->getDefinition($entity_type_id) instanceof ConfigEntityType; + return $this->entityType instanceof ConfigEntityType; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + if (isset($this->entityType)) { + return ['module' => [$this->entityType->getProvider()]]; + } } } diff --git a/core/modules/rest/src/RequestHandler.php b/core/modules/rest/src/RequestHandler.php index 5bbb9be..2c3310a 100644 --- a/core/modules/rest/src/RequestHandler.php +++ b/core/modules/rest/src/RequestHandler.php @@ -2,11 +2,14 @@ namespace Drupal\rest; +use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Cache\CacheableResponseInterface; use Drupal\Core\Render\RenderContext; use Drupal\Core\Routing\RouteMatchInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -17,11 +20,35 @@ /** * Acts as intermediate request forwarder for resource plugins. */ -class RequestHandler implements ContainerAwareInterface { +class RequestHandler implements ContainerAwareInterface, ContainerInjectionInterface { use ContainerAwareTrait; /** + * The resource configuration storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $resourceStorage; + + /** + * Creates a new RequestHandler instance. + * + * @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage + * The resource configuration storage. + */ + public function __construct(EntityStorageInterface $entity_storage) { + $this->resourceStorage = $entity_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static($container->get('entity_type.manager')->getStorage('rest_resource_config')); + } + + /** * Handles a web API request. * * @param \Drupal\Core\Routing\RouteMatchInterface $route_match @@ -33,13 +60,12 @@ class RequestHandler implements ContainerAwareInterface { * The response object. */ public function handle(RouteMatchInterface $route_match, Request $request) { - - $plugin = $route_match->getRouteObject()->getDefault('_plugin'); $method = strtolower($request->getMethod()); - $resource = $this->container - ->get('plugin.manager.rest') - ->createInstance($plugin); + $resource_config_id = $route_match->getRouteObject()->getDefault('_rest_resource_config'); + /** @var \Drupal\rest\RestResourceConfigInterface $resource_config */ + $resource_config = $this->resourceStorage->load($resource_config_id); + $resource = $resource_config->getResourcePlugin(); // Deserialize incoming data if available. /** @var \Symfony\Component\Serializer\SerializerInterface $serializer */ @@ -53,9 +79,8 @@ public function handle(RouteMatchInterface $route_match, Request $request) { // formats are configured allow all and hope that the serializer knows the // format. If the serializer cannot handle it an exception will be thrown // that bubbles up to the client. - $config = $this->container->get('config.factory')->get('rest.settings')->get('resources'); - $method_settings = $config[$plugin][$request->getMethod()]; - if (empty($method_settings['supported_formats']) || in_array($format, $method_settings['supported_formats'])) { + $request_method = $request->getMethod(); + if (in_array($format, $resource_config->getFormats($request_method))) { $definition = $resource->getPluginDefinition(); $class = $definition['serialization_class']; try { @@ -101,7 +126,7 @@ public function handle(RouteMatchInterface $route_match, Request $request) { } return $response instanceof ResourceResponseInterface ? - $this->renderResponse($request, $response, $serializer, $format) : + $this->renderResponse($request, $response, $serializer, $format, $resource_config) : $response; } @@ -132,6 +157,8 @@ public function csrfToken() { * The serializer to use. * @param string $format * The response format. + * @param \Drupal\rest\RestResourceConfigInterface $resource_config + * The resource config. * * @return \Drupal\rest\ResourceResponse * The altered response. @@ -139,7 +166,7 @@ public function csrfToken() { * @todo Add test coverage for language negotiation contexts in * https://www.drupal.org/node/2135829. */ - protected function renderResponse(Request $request, ResourceResponseInterface $response, SerializerInterface $serializer, $format) { + protected function renderResponse(Request $request, ResourceResponseInterface $response, SerializerInterface $serializer, $format, RestResourceConfigInterface $resource_config) { $data = $response->getResponseData(); if ($response instanceof CacheableResponseInterface) { @@ -153,9 +180,8 @@ protected function renderResponse(Request $request, ResourceResponseInterface $r $response->addCacheableDependency($context->pop()); } - // Add rest settings config's cache tags. - $response->addCacheableDependency($this->container->get('config.factory') - ->get('rest.settings')); + // Add rest config's cache tags. + $response->addCacheableDependency($resource_config); } else { $output = $serializer->serialize($data, $format); diff --git a/core/modules/rest/src/RestPermissions.php b/core/modules/rest/src/RestPermissions.php index 57e95e4..473c192 100644 --- a/core/modules/rest/src/RestPermissions.php +++ b/core/modules/rest/src/RestPermissions.php @@ -2,8 +2,8 @@ namespace Drupal\rest; -use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\DependencyInjection\ContainerInjectionInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\rest\Plugin\Type\ResourcePluginManager; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -20,30 +20,30 @@ class RestPermissions implements ContainerInjectionInterface { protected $restPluginManager; /** - * The config factory. + * The REST resource config storage. * - * @var \Drupal\Core\Config\ConfigFactoryInterface + * @var \Drupal\Core\Entity\EntityManagerInterface */ - protected $configFactory; + protected $resourceConfigStorage; /** * Constructs a new RestPermissions instance. * * @param \Drupal\rest\Plugin\Type\ResourcePluginManager $rest_plugin_manager * The rest resource plugin manager. - * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory - * The config factory. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. */ - public function __construct(ResourcePluginManager $rest_plugin_manager, ConfigFactoryInterface $config_factory) { + public function __construct(ResourcePluginManager $rest_plugin_manager, EntityTypeManagerInterface $entity_type_manager) { $this->restPluginManager = $rest_plugin_manager; - $this->configFactory = $config_factory; + $this->resourceConfigStorage = $entity_type_manager->getStorage('rest_resource_config'); } /** * {@inheritdoc} */ public static function create(ContainerInterface $container) { - return new static($container->get('plugin.manager.rest'), $container->get('config.factory')); + return new static($container->get('plugin.manager.rest'), $container->get('entity_type.manager')); } /** @@ -53,12 +53,11 @@ public static function create(ContainerInterface $container) { */ public function permissions() { $permissions = []; - $resources = $this->configFactory->get('rest.settings')->get('resources'); - if ($resources && $enabled = array_intersect_key($this->restPluginManager->getDefinitions(), $resources)) { - foreach ($enabled as $id => $resource) { - $plugin = $this->restPluginManager->createInstance($id); - $permissions = array_merge($permissions, $plugin->permissions()); - } + /** @var \Drupal\rest\RestResourceConfigInterface[] $resource_configs */ + $resource_configs = $this->resourceConfigStorage->loadMultiple(); + foreach ($resource_configs as $resource_config) { + $plugin = $resource_config->getResourcePlugin(); + $permissions = array_merge($permissions, $plugin->permissions()); } return $permissions; } diff --git a/core/modules/rest/src/RestResourceConfigInterface.php b/core/modules/rest/src/RestResourceConfigInterface.php new file mode 100644 index 0000000..7f34bc4 --- /dev/null +++ b/core/modules/rest/src/RestResourceConfigInterface.php @@ -0,0 +1,56 @@ +manager = $manager; - $this->config = $config; + $this->resourceConfigStorage = $entity_type_manager->getStorage('rest_resource_config'); $this->logger = $logger; } @@ -58,56 +59,68 @@ public function __construct(ResourcePluginManager $manager, ConfigFactoryInterfa * @return array */ protected function alterRoutes(RouteCollection $collection) { - $routes = array(); - - // Silently ignore resources that are in the settings but are not defined on - // the plugin manager currently. That avoids exceptions when REST module is - // enabled before another module that provides the resource plugin specified - // in the settings. - // @todo Remove in https://www.drupal.org/node/2308745 - $resources = $this->config->get('rest.settings')->get('resources') ?: array(); - $enabled_resources = array_intersect_key($resources, $this->manager->getDefinitions()); - if (count($resources) != count($enabled_resources)) { - trigger_error('rest.settings lists resources relying on the following missing plugins: ' . implode(', ', array_keys(array_diff_key($resources, $enabled_resources)))); + // Iterate over all enabled REST resource configs. + /** @var \Drupal\rest\RestResourceConfigInterface[] $resource_configs */ + $resource_configs = $this->resourceConfigStorage->loadMultiple(); + // Iterate over all enabled resource plugins. + foreach ($resource_configs as $resource_config) { + $resource_routes = $this->getRoutesForResourceConfig($resource_config); + $collection->addCollection($resource_routes); } + } - // Iterate over all enabled resource plugins. - foreach ($enabled_resources as $id => $enabled_methods) { - $plugin = $this->manager->createInstance($id); - foreach ($plugin->routes() as $name => $route) { - // @todo: Are multiple methods possible here? - $methods = $route->getMethods(); - // Only expose routes where the method is enabled in the configuration. - if ($methods && ($method = $methods[0]) && $method && isset($enabled_methods[$method])) { - $route->setRequirement('_access_rest_csrf', 'TRUE'); - - // Check that authentication providers are defined. - if (empty($enabled_methods[$method]['supported_auth']) || !is_array($enabled_methods[$method]['supported_auth'])) { - $this->logger->error('At least one authentication provider must be defined for resource @id', array(':id' => $id)); - continue; - } - - // Check that formats are defined. - if (empty($enabled_methods[$method]['supported_formats']) || !is_array($enabled_methods[$method]['supported_formats'])) { - $this->logger->error('At least one format must be defined for resource @id', array(':id' => $id)); - continue; - } - - // If the route has a format requirement, then verify that the - // resource has it. - $format_requirement = $route->getRequirement('_format'); - if ($format_requirement && !in_array($format_requirement, $enabled_methods[$method]['supported_formats'])) { - continue; - } - - // The configuration seems legit at this point, so we set the - // authentication provider and add the route. - $route->setOption('_auth', $enabled_methods[$method]['supported_auth']); - $routes["rest.$name"] = $route; - $collection->add("rest.$name", $route); + /** + * Provides all routes for a given REST resource config. + * + * This method determines where a resource is reachable, what path + * replacements are used, the required HTTP method for the operation etc. + * + * @param \Drupal\rest\RestResourceConfigInterface $rest_resource_config + * The rest resource config. + * + * @return \Symfony\Component\Routing\RouteCollection + * The route collection. + */ + protected function getRoutesForResourceConfig(RestResourceConfigInterface $rest_resource_config) { + $plugin = $rest_resource_config->getResourcePlugin(); + $collection = new RouteCollection(); + + foreach ($plugin->routes() as $name => $route) { + /** @var \Symfony\Component\Routing\Route $route */ + // @todo: Are multiple methods possible here? + $methods = $route->getMethods(); + // Only expose routes where the method is enabled in the configuration. + if ($methods && ($method = $methods[0]) && $supported_formats = $rest_resource_config->getFormats($method)) { + $route->setRequirement('_access_rest_csrf', 'TRUE'); + + // Check that authentication providers are defined. + if (empty($rest_resource_config->getAuthenticationProviders($method))) { + $this->logger->error('At least one authentication provider must be defined for resource @id', array(':id' => $rest_resource_config->id())); + continue; + } + + // Check that formats are defined. + if (empty($rest_resource_config->getFormats($method))) { + $this->logger->error('At least one format must be defined for resource @id', array(':id' => $rest_resource_config->id())); + continue; } + + // If the route has a format requirement, then verify that the + // resource has it. + $format_requirement = $route->getRequirement('_format'); + if ($format_requirement && !in_array($format_requirement, $rest_resource_config->getFormats($method))) { + continue; + } + + // The configuration seems legit at this point, so we set the + // authentication provider and add the route. + $route->setOption('_auth', $rest_resource_config->getAuthenticationProviders($method)); + $route->setDefault('_rest_resource_config', $rest_resource_config->id()); + $collection->add("rest.$name", $route); } + } + return $collection; } } diff --git a/core/modules/rest/src/Tests/AuthTest.php b/core/modules/rest/src/Tests/AuthTest.php index 08db9ab..e9fb7d7 100644 --- a/core/modules/rest/src/Tests/AuthTest.php +++ b/core/modules/rest/src/Tests/AuthTest.php @@ -16,7 +16,7 @@ class AuthTest extends RESTTestBase { * * @var array */ - public static $modules = array('basic_auth', 'hal', 'rest', 'entity_test', 'comment'); + public static $modules = array('basic_auth', 'hal', 'rest', 'entity_test'); /** * Tests reading from an authenticated resource. diff --git a/core/modules/rest/src/Tests/CreateTest.php b/core/modules/rest/src/Tests/CreateTest.php index 035480b..28c8af6 100644 --- a/core/modules/rest/src/Tests/CreateTest.php +++ b/core/modules/rest/src/Tests/CreateTest.php @@ -23,7 +23,7 @@ class CreateTest extends RESTTestBase { * * @var array */ - public static $modules = array('hal', 'rest', 'entity_test', 'comment'); + public static $modules = array('hal', 'rest', 'entity_test', 'comment', 'node'); /** * The 'serializer' service. diff --git a/core/modules/rest/src/Tests/DeleteTest.php b/core/modules/rest/src/Tests/DeleteTest.php index ccba38e..4cc8016 100644 --- a/core/modules/rest/src/Tests/DeleteTest.php +++ b/core/modules/rest/src/Tests/DeleteTest.php @@ -16,7 +16,7 @@ class DeleteTest extends RESTTestBase { * * @var array */ - public static $modules = array('hal', 'rest', 'entity_test'); + public static $modules = array('hal', 'rest', 'entity_test', 'node'); /** * Tests several valid and invalid delete requests on all entity types. diff --git a/core/modules/rest/src/Tests/NodeTest.php b/core/modules/rest/src/Tests/NodeTest.php index 7f0ed81..9eedd2c 100644 --- a/core/modules/rest/src/Tests/NodeTest.php +++ b/core/modules/rest/src/Tests/NodeTest.php @@ -19,7 +19,7 @@ class NodeTest extends RESTTestBase { * * @var array */ - public static $modules = array('hal', 'rest', 'comment'); + public static $modules = array('hal', 'rest', 'comment', 'node'); /** * Enables node specific REST API configuration and authentication. diff --git a/core/modules/rest/src/Tests/PageCacheTest.php b/core/modules/rest/src/Tests/PageCacheTest.php index 0ce66da..2cf0d43 100644 --- a/core/modules/rest/src/Tests/PageCacheTest.php +++ b/core/modules/rest/src/Tests/PageCacheTest.php @@ -76,7 +76,7 @@ public function testConfigChangePageCache() { $this->httpRequest($url, 'GET', NULL, $this->defaultMimeType); $this->assertResponse(200, 'HTTP response code is correct.'); $this->assertHeader('x-drupal-cache', 'MISS'); - $this->assertCacheTag('config:rest.settings'); + $this->assertCacheTag('config:rest.resource.entity.entity_test'); $this->assertCacheTag('entity_test:1'); $this->assertCacheTag('entity_test_access:field_test_text'); @@ -84,17 +84,17 @@ public function testConfigChangePageCache() { $this->httpRequest($url, 'GET', NULL, $this->defaultMimeType); $this->assertResponse(200, 'HTTP response code is correct.'); $this->assertHeader('x-drupal-cache', 'HIT'); - $this->assertCacheTag('config:rest.settings'); + $this->assertCacheTag('config:rest.resource.entity.entity_test'); $this->assertCacheTag('entity_test:1'); $this->assertCacheTag('entity_test_access:field_test_text'); - // Trigger a config save which should clear the page cache, so we should get - // a cache miss now for the same request. - $this->config('rest.settings')->save(); + // Trigger a resource config save which should clear the page cache, so we + // should get a cache miss now for the same request. + $this->resourceConfigStorage->load('entity.entity_test')->save(); $this->httpRequest($url, 'GET', NULL, $this->defaultMimeType); $this->assertResponse(200, 'HTTP response code is correct.'); $this->assertHeader('x-drupal-cache', 'MISS'); - $this->assertCacheTag('config:rest.settings'); + $this->assertCacheTag('config:rest.resource.entity.entity_test'); $this->assertCacheTag('entity_test:1'); $this->assertCacheTag('entity_test_access:field_test_text'); diff --git a/core/modules/rest/src/Tests/RESTTestBase.php b/core/modules/rest/src/Tests/RESTTestBase.php index ad5f13d..4779b6b 100644 --- a/core/modules/rest/src/Tests/RESTTestBase.php +++ b/core/modules/rest/src/Tests/RESTTestBase.php @@ -4,6 +4,7 @@ use Drupal\Core\Config\Entity\ConfigEntityType; use Drupal\node\NodeInterface; +use Drupal\rest\RestResourceConfigInterface; use Drupal\simpletest\WebTestBase; /** @@ -12,6 +13,13 @@ abstract class RESTTestBase extends WebTestBase { /** + * The REST resource config storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $resourceConfigStorage; + + /** * The default serialization format to use for testing REST operations. * * @var string @@ -52,15 +60,18 @@ * * @var array */ - public static $modules = array('rest', 'entity_test', 'node'); + public static $modules = array('rest', 'entity_test'); protected function setUp() { parent::setUp(); $this->defaultFormat = 'hal_json'; $this->defaultMimeType = 'application/hal+json'; $this->defaultAuth = array('cookie'); + $this->resourceConfigStorage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config'); // Create a test content type for node testing. - $this->drupalCreateContentType(array('name' => 'resttest', 'type' => 'resttest')); + if (in_array('node', static::$modules)) { + $this->drupalCreateContentType(array('name' => 'resttest', 'type' => 'resttest')); + } } /** @@ -268,29 +279,49 @@ protected function entityValues($entity_type_id) { * @param array $auth * (Optional) The list of valid authentication methods. */ - protected function enableService($resource_type, $method = 'GET', $format = NULL, $auth = NULL) { - // Enable REST API for this entity type. - $config = $this->config('rest.settings'); - $settings = array(); - + protected function enableService($resource_type, $method = 'GET', $format = NULL, array $auth = []) { if ($resource_type) { + // Enable REST API for this entity type. + $resource_config_id = str_replace(':', '.', $resource_type); + // get entity by id + /** @var \Drupal\rest\RestResourceConfigInterface $resource_config */ + $resource_config = $this->resourceConfigStorage->load($resource_config_id); + if (!$resource_config) { + $resource_config = $this->resourceConfigStorage->create([ + 'id' => $resource_config_id, + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => [] + ]); + } + $configuration = $resource_config->get('configuration'); + if (is_array($format)) { - $settings[$resource_type][$method]['supported_formats'] = $format; + for ($i = 0; $i < count($format); $i++) { + $configuration[$method]['supported_formats'][] = $format[$i]; + } } else { if ($format == NULL) { $format = $this->defaultFormat; } - $settings[$resource_type][$method]['supported_formats'][] = $format; + $configuration[$method]['supported_formats'][] = $format; } - if ($auth == NULL) { + if (!is_array($auth) || empty($auth)) { $auth = $this->defaultAuth; } - $settings[$resource_type][$method]['supported_auth'] = $auth; + foreach ($auth as $auth_provider) { + $configuration[$method]['supported_auth'][] = $auth_provider; + } + + $resource_config->set('configuration', $configuration); + $resource_config->save(); + } + else { + foreach ($this->resourceConfigStorage->loadMultiple() as $resource_config) { + $resource_config->delete(); + } } - $config->set('resources', $settings); - $config->save(); $this->rebuildCache(); } diff --git a/core/modules/rest/src/Tests/ReadTest.php b/core/modules/rest/src/Tests/ReadTest.php index be6d4ae..5baaf2d 100644 --- a/core/modules/rest/src/Tests/ReadTest.php +++ b/core/modules/rest/src/Tests/ReadTest.php @@ -22,6 +22,7 @@ class ReadTest extends RESTTestBase { public static $modules = [ 'hal', 'rest', + 'node', 'entity_test', 'config_test', 'taxonomy', diff --git a/core/modules/rest/src/Tests/ResourceTest.php b/core/modules/rest/src/Tests/ResourceTest.php index df99cc6..e4d6c00 100644 --- a/core/modules/rest/src/Tests/ResourceTest.php +++ b/core/modules/rest/src/Tests/ResourceTest.php @@ -2,8 +2,8 @@ namespace Drupal\rest\Tests; -use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Session\AccountInterface; +use Drupal\rest\RestResourceConfigInterface; use Drupal\user\Entity\Role; /** @@ -32,9 +32,7 @@ class ResourceTest extends RESTTestBase { */ protected function setUp() { parent::setUp(); - $this->config = $this->config('rest.settings'); - - // Create an entity programmatically. + // Create an entity programmatic. $this->entity = $this->entityCreate('entity_test'); $this->entity->save(); @@ -47,20 +45,17 @@ protected function setUp() { * Tests that a resource without formats cannot be enabled. */ public function testFormats() { - $settings = array( - 'entity:entity_test' => array( - 'GET' => array( - 'supported_auth' => array( + $this->resourceConfigStorage->create([ + 'id' => 'entity.entity_test', + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => [ + 'GET' => [ + 'supported_auth' => [ 'basic_auth', - ), - ), - ), - ); - - // Attempt to enable the resource. - $this->config->set('resources', $settings); - $this->config->save(); - $this->rebuildCache(); + ], + ], + ], + ])->save(); // Verify that accessing the resource returns 406. $response = $this->httpRequest($this->entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); @@ -77,20 +72,17 @@ public function testFormats() { * Tests that a resource without authentication cannot be enabled. */ public function testAuthentication() { - $settings = array( - 'entity:entity_test' => array( - 'GET' => array( - 'supported_formats' => array( + $this->resourceConfigStorage->create([ + 'id' => 'entity.entity_test', + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => [ + 'GET' => [ + 'supported_formats' => [ 'hal_json', - ), - ), - ), - ); - - // Attempt to enable the resource. - $this->config->set('resources', $settings); - $this->config->save(); - $this->rebuildCache(); + ], + ], + ], + ])->save(); // Verify that accessing the resource returns 401. $response = $this->httpRequest($this->entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET'); @@ -118,30 +110,4 @@ public function testUriPaths() { } } - /** - * Tests that a resource with a missing plugin does not cause an exception. - */ - public function testMissingPlugin() { - $settings = array( - 'entity:nonexisting' => array( - 'GET' => array( - 'supported_formats' => array( - 'hal_json', - ), - ), - ), - ); - - try { - // Attempt to enable the resource. - $this->config->set('resources', $settings); - $this->config->save(); - $this->rebuildCache(); - $this->pass('rest.settings referencing a missing REST resource plugin does not cause an exception.'); - } - catch (PluginNotFoundException $e) { - $this->fail('rest.settings referencing a missing REST resource plugin caused an exception.'); - } - } - } diff --git a/core/modules/rest/src/Tests/Update/RestConfigurationEntitiesUpdateTest.php b/core/modules/rest/src/Tests/Update/RestConfigurationEntitiesUpdateTest.php new file mode 100644 index 0000000..11f40c7 --- /dev/null +++ b/core/modules/rest/src/Tests/Update/RestConfigurationEntitiesUpdateTest.php @@ -0,0 +1,62 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz', + __DIR__ . '/../../../../rest/tests/fixtures/update/drupal-8.rest-rest_update_8201.php', + ]; + } + + /** + * Tests rest_update_8201(). + */ + public function testResourcesConvertedToConfigEntities() { + /** @var \Drupal\Core\Entity\EntityStorageInterface $resource_config_storage */ + $resource_config_storage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config'); + + // Make sure we have the expected values before the update. + $rest_settings = $this->config('rest.settings'); + $this->assertTrue(array_key_exists('resources', $rest_settings->getRawData())); + $this->assertTrue(array_key_exists('entity:node', $rest_settings->getRawData()['resources'])); + $resource_config_entities = $resource_config_storage->loadMultiple(); + $this->assertIdentical([], array_keys($resource_config_entities)); + + // Read the existing 'entity:node' resource configuration so we can verify + // it after the update. + $node_configuration = $rest_settings->getRawData()['resources']['entity:node']; + + $this->runUpdates(); + + // Make sure we have the expected values after the update. + $rest_settings = $this->config('rest.settings'); + $this->assertFalse(array_key_exists('resources', $rest_settings->getRawData())); + $resource_config_entities = $resource_config_storage->loadMultiple(); + $this->assertIdentical(['entity.node'], array_keys($resource_config_entities)); + $node_resource_config_entity = $resource_config_entities['entity.node']; + $this->assertIdentical(RestResourceConfigInterface::METHOD_GRANULARITY, $node_resource_config_entity->get('granularity')); + $this->assertIdentical($node_configuration, $node_resource_config_entity->get('configuration')); + } + +} diff --git a/core/modules/rest/src/Tests/UpdateTest.php b/core/modules/rest/src/Tests/UpdateTest.php index 98df739..1d99cf2 100644 --- a/core/modules/rest/src/Tests/UpdateTest.php +++ b/core/modules/rest/src/Tests/UpdateTest.php @@ -23,7 +23,7 @@ class UpdateTest extends RESTTestBase { * * @var array */ - public static $modules = ['hal', 'rest', 'entity_test', 'comment']; + public static $modules = ['hal', 'rest', 'entity_test', 'node', 'comment']; /** * {@inheritdoc} diff --git a/core/modules/rest/tests/fixtures/update/drupal-8.rest-rest_update_8201.php b/core/modules/rest/tests/fixtures/update/drupal-8.rest-rest_update_8201.php new file mode 100644 index 0000000..f035e94 --- /dev/null +++ b/core/modules/rest/tests/fixtures/update/drupal-8.rest-rest_update_8201.php @@ -0,0 +1,63 @@ +insert('key_value') + ->fields([ + 'collection' => 'system.schema', + 'name' => 'rest', + 'value' => 'i:8000;', + ]) + ->fields([ + 'collection' => 'system.schema', + 'name' => 'serialization', + 'value' => 'i:8000;', + ]) + ->execute(); + +// Update core.extension. +$extensions = $connection->select('config') + ->fields('config', ['data']) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute() + ->fetchField(); +$extensions = unserialize($extensions); +$extensions['module']['rest'] = 8000; +$extensions['module']['serialization'] = 8000; +$connection->update('config') + ->fields([ + 'data' => serialize($extensions), + ]) + ->condition('collection', '') + ->condition('name', 'core.extension') + ->execute(); + +// Install the rest configuration. +$config = [ + 'resources' => [ + 'entity:node' => [ + 'GET' => [ + 'supported_formats' => ['json'], + 'supported_auth' => [], + ], + ], + ], + 'link_domain' => '~', +]; +$data = $connection->insert('config') + ->fields([ + 'name' => 'rest.settings', + 'data' => serialize($config), + 'collection' => '' + ]) + ->execute(); diff --git a/core/modules/rest/tests/src/Kernel/Entity/ConfigDependenciesTest.php b/core/modules/rest/tests/src/Kernel/Entity/ConfigDependenciesTest.php new file mode 100644 index 0000000..b9a9062 --- /dev/null +++ b/core/modules/rest/tests/src/Kernel/Entity/ConfigDependenciesTest.php @@ -0,0 +1,192 @@ + 'hal', 'json' => 'serialization'], ['basic_auth' => 'basic_auth']); + + $rest_config = RestResourceConfig::create([ + 'plugin_id' => 'entity:entity_test', + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => [ + 'GET' => [ + 'supported_auth' => ['cookie'], + 'supported_formats' => ['json'], + ], + 'POST' => [ + 'supported_auth' => ['basic_auth'], + 'supported_formats' => ['hal_json'], + ], + ], + ]); + + $result = $config_dependencies->calculateDependencies($rest_config); + $this->assertEquals(['module' => [ + 'serialization', 'basic_auth', 'hal', + ]], $result); + } + + /** + * @covers ::onDependencyRemoval + * @covers ::calculateDependenciesForMethodGranularity + */ + public function testOnDependencyRemovalRemoveUnrelatedDependency() { + $config_dependencies = new ConfigDependencies(['hal_json' => 'hal', 'json' => 'serialization'], ['basic_auth' => 'basic_auth']); + + $rest_config = RestResourceConfig::create([ + 'plugin_id' => 'entity:entity_test', + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => [ + 'GET' => [ + 'supported_auth' => ['cookie'], + 'supported_formats' => ['json'], + ], + 'POST' => [ + 'supported_auth' => ['basic_auth'], + 'supported_formats' => ['hal_json'], + ], + ], + ]); + + $this->assertFalse($config_dependencies->onDependencyRemoval($rest_config, ['module' => ['node']])); + $this->assertEquals([ + 'GET' => [ + 'supported_auth' => ['cookie'], + 'supported_formats' => ['json'], + ], + 'POST' => [ + 'supported_auth' => ['basic_auth'], + 'supported_formats' => ['hal_json'], + ], + ], $rest_config->get('configuration')); + } + + /** + * @covers ::onDependencyRemoval + * @covers ::calculateDependenciesForMethodGranularity + */ + public function testOnDependencyRemovalRemoveFormat() { + $config_dependencies = new ConfigDependencies(['hal_json' => 'hal', 'json' => 'serialization'], ['basic_auth' => 'basic_auth']); + + $rest_config = RestResourceConfig::create([ + 'plugin_id' => 'entity:entity_test', + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => [ + 'GET' => [ + 'supported_auth' => ['cookie'], + 'supported_formats' => ['json'], + ], + 'POST' => [ + 'supported_auth' => ['basic_auth'], + 'supported_formats' => ['hal_json'], + ], + ], + ]); + + $this->assertTrue($config_dependencies->onDependencyRemoval($rest_config, ['module' => ['hal']])); + $this->assertEquals(['json'], $rest_config->getFormats('GET')); + $this->assertEquals([], $rest_config->getFormats('POST')); + $this->assertEquals([ + 'GET' => [ + 'supported_auth' => ['cookie'], + 'supported_formats' => ['json'], + ], + 'POST' => [ + 'supported_auth' => ['basic_auth'], + ], + ], $rest_config->get('configuration')); + } + + /** + * @covers ::onDependencyRemoval + * @covers ::calculateDependenciesForMethodGranularity + */ + public function testOnDependencyRemovalRemoveAuth() { + $config_dependencies = new ConfigDependencies(['hal_json' => 'hal', 'json' => 'serialization'], ['basic_auth' => 'basic_auth']); + + $rest_config = RestResourceConfig::create([ + 'plugin_id' => 'entity:entity_test', + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => [ + 'GET' => [ + 'supported_auth' => ['cookie'], + 'supported_formats' => ['json'], + ], + 'POST' => [ + 'supported_auth' => ['basic_auth'], + 'supported_formats' => ['hal_json'], + ], + ], + ]); + + $this->assertTrue($config_dependencies->onDependencyRemoval($rest_config, ['module' => ['basic_auth']])); + $this->assertEquals(['cookie'], $rest_config->getAuthenticationProviders('GET')); + $this->assertEquals([], $rest_config->getAuthenticationProviders('POST')); + $this->assertEquals([ + 'GET' => [ + 'supported_auth' => ['cookie'], + 'supported_formats' => ['json'], + ], + 'POST' => [ + 'supported_formats' => ['hal_json'], + ], + ], $rest_config->get('configuration')); + } + + /** + * @covers ::onDependencyRemoval + * @covers ::calculateDependenciesForMethodGranularity + */ + public function testOnDependencyRemovalRemoveAuthAndFormats() { + $config_dependencies = new ConfigDependencies(['hal_json' => 'hal', 'json' => 'serialization'], ['basic_auth' => 'basic_auth']); + + $rest_config = RestResourceConfig::create([ + 'plugin_id' => 'entity:entity_test', + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => [ + 'GET' => [ + 'supported_auth' => ['cookie'], + 'supported_formats' => ['json'], + ], + 'POST' => [ + 'supported_auth' => ['basic_auth'], + 'supported_formats' => ['hal_json'], + ], + ], + ]); + + $this->assertTrue($config_dependencies->onDependencyRemoval($rest_config, ['module' => ['basic_auth', 'hal']])); + $this->assertEquals(['json'], $rest_config->getFormats('GET')); + $this->assertEquals(['cookie'], $rest_config->getAuthenticationProviders('GET')); + $this->assertEquals([], $rest_config->getFormats('POST')); + $this->assertEquals([], $rest_config->getAuthenticationProviders('POST')); + $this->assertEquals([ + 'GET' => [ + 'supported_auth' => ['cookie'], + 'supported_formats' => ['json'], + ], + ], $rest_config->get('configuration')); + } + +} diff --git a/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigTest.php b/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigTest.php new file mode 100644 index 0000000..89e9b1d --- /dev/null +++ b/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigTest.php @@ -0,0 +1,44 @@ + 'entity:entity_test', + 'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY, + 'configuration' => [ + 'GET' => [ + 'supported_auth' => ['cookie'], + 'supported_formats' => ['json'], + ], + 'POST' => [ + 'supported_auth' => ['basic_auth'], + 'supported_formats' => ['hal_json'], + ], + ], + ]); + + $rest_config->calculateDependencies(); + $this->assertEquals(['module' => ['basic_auth', 'entity_test', 'hal', 'serialization', 'user']], $rest_config->getDependencies()); + } + +} diff --git a/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php b/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php index 36a482c..e09b175 100644 --- a/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php +++ b/core/modules/rest/tests/src/Kernel/RequestHandlerTest.php @@ -1,18 +1,14 @@ requestHandler = new RequestHandler(); + $this->entityStorage = $this->prophesize(EntityStorageInterface::class); + $this->requestHandler = new RequestHandler($this->entityStorage->reveal()); $this->requestHandler->setContainer($this->container); } @@ -47,17 +51,19 @@ public function setUp() { */ public function testBaseHandler() { $request = new Request(); - $route_match = new RouteMatch('test', new Route('/rest/test', ['_plugin' => 'restplugin', '_format' => 'json'])); + $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => 'restplugin', '_format' => 'json'])); $resource = $this->prophesize(StubRequestHandlerResourcePlugin::class); $resource->get(NULL, $request) ->shouldBeCalled(); - // Setup stub plugin manager that will return our plugin. - $stub = $this->prophesize(ResourcePluginManager::class); - $stub->createInstance('restplugin') - ->willReturn($resource->reveal()); - $this->container->set('plugin.manager.rest', $stub->reveal()); + // Setup the configuration. + $config = $this->prophesize(RestResourceConfigInterface::class); + $config->getResourcePlugin()->willReturn($resource->reveal()); + $config->getCacheContexts()->willReturn([]); + $config->getCacheTags()->willReturn([]); + $config->getCacheMaxAge()->willReturn(12); + $this->entityStorage->load('restplugin')->willReturn($config->reveal()); // Response returns NULL this time because response from plugin is not // a ResourceResponse so it is passed through directly. @@ -89,15 +95,17 @@ public function testBaseHandler() { */ public function testSerialization($data) { $request = new Request(); - $route_match = new RouteMatch('test', new Route('/rest/test', ['_plugin' => 'restplugin', '_format' => 'json'])); + $route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => 'restplugin', '_format' => 'json'])); $resource = $this->prophesize(StubRequestHandlerResourcePlugin::class); - // Setup stub plugin manager that will return our plugin. - $stub = $this->prophesize(ResourcePluginManager::class); - $stub->createInstance('restplugin') - ->willReturn($resource->reveal()); - $this->container->set('plugin.manager.rest', $stub->reveal()); + // Setup the configuration. + $config = $this->prophesize(RestResourceConfigInterface::class); + $config->getResourcePlugin()->willReturn($resource->reveal()); + $config->getCacheContexts()->willReturn([]); + $config->getCacheTags()->willReturn([]); + $config->getCacheMaxAge()->willReturn(12); + $this->entityStorage->load('restplugin')->willReturn($config->reveal()); $response = new ResourceResponse($data); $resource->get(NULL, $request) diff --git a/core/modules/rest/tests/src/Kernel/RestLinkManagerTest.php b/core/modules/rest/tests/src/Kernel/RestLinkManagerTest.php index fc7c41d..ab558e5 100644 --- a/core/modules/rest/tests/src/Kernel/RestLinkManagerTest.php +++ b/core/modules/rest/tests/src/Kernel/RestLinkManagerTest.php @@ -14,7 +14,7 @@ class RestLinkManagerTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['rest', 'rest_test', 'system']; + public static $modules = ['serialization', 'rest', 'rest_test', 'system']; /** * {@inheritdoc} diff --git a/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php new file mode 100644 index 0000000..753d3c1 --- /dev/null +++ b/core/modules/serialization/src/EventSubscriber/DefaultExceptionSubscriber.php @@ -0,0 +1,134 @@ +serializer = $serializer; + $this->serializerFormats = $serializer_formats; + } + + /** + * {@inheritdoc} + */ + protected function getHandledFormats() { + return $this->serializerFormats; + } + + /** + * {@inheritdoc} + */ + protected static function getPriority() { + // This will fire after the most common HTML handler, since HTML requests + // are still more common than HTTP requests. + return -75; + } + + /** + * Handles a 400 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on400(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_BAD_REQUEST); + } + + /** + * Handles a 403 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on403(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_FORBIDDEN); + } + + /** + * Handles a 404 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on404(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_NOT_FOUND); + } + + /** + * Handles a 405 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on405(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_METHOD_NOT_ALLOWED); + } + + /** + * Handles a 406 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on406(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_NOT_ACCEPTABLE); + } + + /** + * Handles a 422 error for HTTP. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The event to process. + */ + public function on422(GetResponseForExceptionEvent $event) { + $this->setEventResponse($event, Response::HTTP_UNPROCESSABLE_ENTITY); + } + + /** + * Sets the Response for the exception event. + * + * @param \Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent $event + * The current exception event. + * @param int $status + * The HTTP status code to set for the response. + */ + protected function setEventResponse(GetResponseForExceptionEvent $event, $status) { + $format = $event->getRequest()->getRequestFormat(); + $content = ['message' => $event->getException()->getMessage()]; + $encoded_content = $this->serializer->serialize($content, $format); + $response = new Response($encoded_content, $status); + $event->setResponse($response); + } + +} diff --git a/core/modules/system/src/Tests/System/ResponseGeneratorTest.php b/core/modules/system/src/Tests/System/ResponseGeneratorTest.php index 9d6cf4f..d0d22a2 100644 --- a/core/modules/system/src/Tests/System/ResponseGeneratorTest.php +++ b/core/modules/system/src/Tests/System/ResponseGeneratorTest.php @@ -16,7 +16,7 @@ class ResponseGeneratorTest extends RESTTestBase { * * @var array */ - public static $modules = array('hal', 'rest', 'node'); + public static $modules = array('hal', 'rest', 'node', 'basic_auth'); /** * {@inheritdoc} diff --git a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php index 19e1fcf..82169ad 100644 --- a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php +++ b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php @@ -265,7 +265,15 @@ protected function runUpdates() { } // Ensure that the update hooks updated all entity schema. - $this->assertFalse(\Drupal::service('entity.definition_update_manager')->needsUpdates(), 'After all updates ran, entity schema is up to date.'); + $needs_updates = \Drupal::entityDefinitionUpdateManager()->needsUpdates(); + $this->assertFalse($needs_updates, 'After all updates ran, entity schema is up to date.'); + if ($needs_updates) { + foreach (\Drupal::entityDefinitionUpdateManager()->getChangeSummary() as $entity_type_id => $summary) { + foreach ($summary as $message) { + $this->fail($message); + } + } + } } /** diff --git a/core/modules/system/system.install b/core/modules/system/system.install index b8f5f5f..1f4c3ad 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1138,7 +1138,11 @@ function system_update_8004() { $manager = \Drupal::entityDefinitionUpdateManager(); foreach (array_keys(\Drupal::entityManager() ->getDefinitions()) as $entity_type_id) { - $manager->updateEntityType($manager->getEntityType($entity_type_id)); + // Only update the entity type if it already exists. This condition is + // needed in case new entity types are introduced after this update. + if ($entity_type = $manager->getEntityType($entity_type_id)) { + $manager->updateEntityType($entity_type); + } } } diff --git a/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/AuthenticationProviderPassTest.php b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/AuthenticationProviderPassTest.php new file mode 100644 index 0000000..6f99d09 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/DependencyInjection/Compiler/AuthenticationProviderPassTest.php @@ -0,0 +1,44 @@ +setDefinition('serializer', new Definition(Serializer::class, [[], []])); + + $definition = new Definition('TestClass'); + $definition->addTag('authentication_provider', ['provider_id' => 'bunny_auth']); + $definition->addTag('_provider', ['provider' => 'test_provider_a']); + $container->setDefinition('test_provider_a.authentication.bunny_auth', $definition); + + $definition = new Definition('TestClass'); + $definition->addTag('authentication_provider', ['provider_id' => 'llama_auth', 'priority' => 100]); + $definition->addTag('_provider', ['provider' => 'test_provider_a']); + $container->setDefinition('test_provider_a.authentication.llama_auth', $definition); + + $definition = new Definition('TestClass'); + $definition->addTag('authentication_provider', ['provider_id' => 'camel_auth', 'priority' => -100]); + $definition->addTag('_provider', ['provider' => 'test_provider_b']); + $container->setDefinition('test_provider_b.authentication.camel_auth', $definition); + + $compiler_pass = new AuthenticationProviderPass(); + $compiler_pass->process($container); + + $this->assertEquals(['bunny_auth' => 'test_provider_a', 'llama_auth' => 'test_provider_a', 'camel_auth' => 'test_provider_b'], $container->getParameter('authentication_providers')); + } + +}