diff --git a/core/modules/page_cache/src/Tests/PageCacheTest.php b/core/modules/page_cache/src/Tests/PageCacheTest.php index 42fbec5..2e8c2cb 100644 --- a/core/modules/page_cache/src/Tests/PageCacheTest.php +++ b/core/modules/page_cache/src/Tests/PageCacheTest.php @@ -135,7 +135,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.endpoint.entity__node.yml b/core/modules/rest/config/optional/rest.endpoint.entity__node.yml new file mode 100644 index 0000000..4561a2f --- /dev/null +++ b/core/modules/rest/config/optional/rest.endpoint.entity__node.yml @@ -0,0 +1,28 @@ +id: entity__node +plugin_id: 'entity:node' +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..bbcea6f 100644 --- a/core/modules/rest/config/schema/rest.schema.yml +++ b/core/modules/rest/config/schema/rest.schema.yml @@ -1,15 +1,8 @@ # 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' @@ -45,3 +38,17 @@ rest_request: sequence: type: string label: 'Authentication' + +rest.endpoint.*: + type: config_entity + label: 'REST endpooint' + mapping: + id: + type: string + label: 'REST endpoint ID' + plugin_id: + type: string + label: 'REST endpoint plugin id' + configuration: + type: rest_resource + label: 'REST endpoint configuration' diff --git a/core/modules/rest/rest.install b/core/modules/rest/rest.install index 4bca69b..41cdd43 100644 --- a/core/modules/rest/rest.install +++ b/core/modules/rest/rest.install @@ -5,6 +5,8 @@ * Install, update and uninstall functions for the rest module. */ +use Drupal\rest\Entity\RestEndpoint; + /** * Implements hook_requirements(). */ @@ -21,3 +23,20 @@ function rest_requirements($phase) { } return $requirements; } + +/** + * Install the REST endpoint entity type and convert old settings-based config. + */ +function rest_update_8100() { + \Drupal::entityDefinitionUpdateManager()->installEntityType(\Drupal::entityTypeManager()->getDefinition('rest_endpoint')); + foreach (\Drupal::config('rest.settings')->get('resources') as $key => $resource) { + $resource = RestEndpoint::create([ + 'id' => str_replace(':', '__', $key), + 'configuration' => $resource, + ]); + $resource->save(); + } + \Drupal::configFactory()->getEditable('rest.settings') + ->clear('resources') + ->save(); +} diff --git a/core/modules/rest/rest.permissions.yml b/core/modules/rest/rest.permissions.yml index 2ab7154..0a276ee 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 endpoints: + title: 'Administer REST endpoint 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/RestEndpoint.php b/core/modules/rest/src/Entity/RestEndpoint.php new file mode 100644 index 0000000..92ac871 --- /dev/null +++ b/core/modules/rest/src/Entity/RestEndpoint.php @@ -0,0 +1,381 @@ +pluginManager = \Drupal::service('plugin.manager.rest'); + $this->logger = \Drupal::service('logger.factory')->get('rest'); + $this->setContainer(\Drupal::getContainer()); + 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->pluginManager + ->getDefinition(['id' => $this->getResourcePluginID()]); + return $plugin_definition['label']; + } + + /** + * {@inheritdoc} + */ + public function getResourcePluginID() { + return $this->plugin_id; + } + + /** + * {@inheritdoc} + */ + public function getResourcePlugin() { + return $this->getPluginCollections()['resource']->get($this->getResourcePluginID()); + } + + /** + * {@inheritdoc} + */ + public function isRequestMethodEnabled($method) { + $method = $this->normaliseRestMethod($method); + return isset($this->configuration[$method]); + } + + /** + * {@inheritdoc} + */ + public function getSupportedAuthenticationProviders($method) { + $method = $this->normaliseRestMethod($method); + if ($this->isRequestMethodEnabled($method) && isset($this->configuration[$method]['supported_auth'])) { + return $this->configuration[$method]['supported_auth']; + } + return []; + } + + /** + * {@inheritdoc} + */ + public function hasSupportForAuthenticationProvider($method, $auth) { + $method = $this->normaliseRestMethod($method); + return $this->supportsAuthenticationProviders($method) + && in_array($auth, $this->configuration[$method]['supported_auth']); + } + + /** + * {@inheritdoc} + */ + public function supportsAuthenticationProviders($method) { + $method = $this->normaliseRestMethod($method); + return $this->isRequestMethodEnabled($method) + && isset($this->configuration[$method]['supported_auth']) + && !empty($this->configuration[$method]['supported_auth']); + } + + /** + * {@inheritdoc} + */ + public function addSupportedAuthenticationProvider($method, $auth) { + $method = $this->normaliseRestMethod($method); + if (!$this->isRequestMethodEnabled($method)) { + $this->configuration[$method] = ['supported_auth' => []]; + } + if (!isset($this->configuration[$method]['supported_auth'])){ + $this->configuration[$method]['supported_auth'] = []; + } + if (!in_array($auth, $this->configuration[$method]['supported_auth'])) { + $this->configuration[$method]['supported_auth'][] = $auth; + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeSupportedAuthenticationProvider($method, $auth) { + $method = $this->normaliseRestMethod($method); + if ($this->supportsAuthenticationProviders($method)) { + $new_auth = array_filter($this->configuration[$method]['supported_auth'], function ($val) use ($auth) { + return ($val != $auth); + }); + $this->configuration[$method]['supported_auth'] = $new_auth; + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSupportedFormats($method) { + $method = $this->normaliseRestMethod($method); + if ($this->isRequestMethodEnabled($method) && isset($this->configuration[$method]['supported_formats'])) { + return $this->configuration[$method]['supported_formats']; + } + return []; + } + + /** + * {@inheritdoc} + */ + public function supportsFormat($method, $format) { + $method = $this->normaliseRestMethod($method); + return $this->hasSupportedFormats($method) + && in_array($format, $this->configuration[$method]['supported_formats']); + } + + /** + * {@inheritdoc} + */ + public function hasSupportedFormats($method) { + $method = $this->normaliseRestMethod($method); + return $this->isRequestMethodEnabled($method) + && isset($this->configuration[$method]['supported_formats']) + && !empty($this->configuration[$method]['supported_formats']); + } + + /** + * {@inheritdoc} + */ + public function addSupportedFormat($method, $format) { + $method = $this->normaliseRestMethod($method); + if (!$this->isRequestMethodEnabled($method)) { + $this->configuration[$method] = ['supported_formats' => []]; + } + if (!isset($this->configuration[$method]['supported_formats'])){ + $this->configuration[$method]['supported_formats'] = []; + } + if (!in_array($format, $this->configuration[$method]['supported_formats'])) { + $this->configuration[$method]['supported_formats'][] = $format; + } + return $this; + } + + /** + * {@inheritdoc} + */ + public function removeSupportedFormat($method, $format) { + $method = $this->normaliseRestMethod($method); + if ($this->hasSupportedFormats($method)){ + $new_auth = array_filter($this->configuration[$method]['supported_formats'], function ($val) use ($format) { + return ($val != $format); + }); + $this->configuration[$method]['supported_formats'] = $new_auth; + } + return $this; + } + + /** + * Returns the plugin collections used by this entity. + * + * @return \Drupal\Component\Plugin\LazyPluginCollection[] + * An array of plugin collections, keyed by the property name they use to + * store their configuration. + */ + public function getPluginCollections() { + return [ + 'resource' => new DefaultSingleLazyPluginCollection($this->pluginManager, $this->getResourcePluginID(), []) + ]; + } + + /** + * (@inheritdoc) + */ + public function calculateDependencies() { + parent::calculateDependencies(); + // The dependency lists for authentication providers and formats + // generated on container build. + $auth_dependencies_list = $this->container->getParameter('rest.dependencies.auth'); + $format_dependencies_list = $this->container->getParameter('rest.dependencies.format'); + foreach (array_keys($this->configuration) as $request_method) { + // Add dependencies based on the supported authentication providers. + foreach ($this->getSupportedAuthenticationProviders($request_method) as $auth) { + if (isset($auth_dependencies_list[$auth])) { + $module_name = $auth_dependencies_list[$auth]; + $this->addDependency('module', $module_name); + } + } + // Add dependencies based on the supported authentication formats. + foreach ($this->getSupportedFormats($request_method) as $format) { + if (isset($format_dependencies_list[$format])) { + $module_name = $format_dependencies_list[$format]; + $this->addDependency('module', $module_name); + } + } + } + return $this->dependencies; + } + + /** + * {@inheritdoc} + */ + public function onDependencyRemoval(array $dependencies) { + $parent = parent::onDependencyRemoval($dependencies); + $changed = FALSE; + $format_dependencies_list = $this->container->getParameter('rest.dependencies.format'); + $auth_dependencies_list = $this->container->getParameter('rest.dependencies.auth'); + // 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 = []; + $removed_formats = []; + foreach ($dependencies['module'] as $dep_module) { + // Check if the removed dependency module contained an authentication + // provider. + foreach ($auth_dependencies_list as $auth => $auth_module) { + if ($dep_module != $auth_module) { + continue; + } + $removed_auth[] = $auth; + } + // Check if the removed dependency module contained a format. + foreach ($format_dependencies_list as $format => $format_module) { + if ($dep_module != $format_module) { + continue; + } + $removed_formats[] = $format; + } + } + if (!empty($removed_auth) || !empty($removed_formats)) { + // Try to fix dependency problems by removing affected + // authentication providers and formats. + foreach (array_keys($this->configuration) as $request_method) { + foreach ($removed_formats as $format) { + if ($this->supportsFormat($request_method, $format)) { + $this->removeSupportedFormat($request_method, $format); + } + } + foreach ($removed_auth as $auth) { + if ($this->hasSupportForAuthenticationProvider($request_method, $auth)) { + $this->removeSupportedAuthenticationProvider($request_method, $auth); + } + } + if (!$this->supportsAuthenticationProviders($request_method)) { + // Remove the key if there are no more authentication providers + // supported by this request method. + unset($this->configuration[$request_method]['supported_auth']); + } + if (!$this->hasSupportedFormats($request_method)) { + // Remove the key if there are no more formats supported by this + // request method. + unset($this->configuration[$request_method]['supported_formats']); + } + if (empty($this->configuration[$request_method])) { + // Remove the request method altogether if it no longer has any + // supported authentication providers or formats. + unset($this->configuration[$request_method]); + } + } + } + if (!empty($this->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 $parent || $changed; + } + + /** + * Normalize 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 normaliseRestMethod($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 f41335a..bde9ec6 100644 --- a/core/modules/rest/src/Plugin/ResourceBase.php +++ b/core/modules/rest/src/Plugin/ResourceBase.php @@ -12,7 +12,6 @@ use Psr\Log\LoggerInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Routing\Route; -use Symfony\Component\Routing\RouteCollection; /** * Common base class for resource plugins. @@ -93,59 +92,6 @@ public function permissions() { } /** - * {@inheritdoc} - */ - public function routes() { - $collection = new RouteCollection(); - - $definition = $this->getPluginDefinition(); - $canonical_path = isset($definition['uri_paths']['canonical']) ? $definition['uri_paths']['canonical'] : '/' . strtr($this->pluginId, ':', '/') . '/{id}'; - $create_path = isset($definition['uri_paths']['https://www.drupal.org/link-relations/create']) ? $definition['uri_paths']['https://www.drupal.org/link-relations/create'] : '/' . strtr($this->pluginId, ':', '/'); - - $route_name = strtr($this->pluginId, ':', '.'); - - $methods = $this->availableMethods(); - foreach ($methods as $method) { - $route = $this->getBaseRoute($canonical_path, $method); - - switch ($method) { - case 'POST': - $route->setPath($create_path); - // Restrict the incoming HTTP Content-type header to the known - // serialization formats. - $route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats))); - $collection->add("$route_name.$method", $route); - break; - - case 'PATCH': - // Restrict the incoming HTTP Content-type header to the known - // serialization formats. - $route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats))); - $collection->add("$route_name.$method", $route); - break; - - case 'GET': - case 'HEAD': - // Restrict GET and HEAD requests to the media type specified in the - // HTTP Accept headers. - foreach ($this->serializerFormats as $format_name) { - // Expose one route per available format. - $format_route = clone $route; - $format_route->addRequirements(array('_format' => $format_name)); - $collection->add("$route_name.$method.$format_name", $format_route); - } - break; - - default: - $collection->add("$route_name.$method", $route); - break; - } - } - - return $collection; - } - - /** * Provides predefined HTTP request methods. * * Plugins can override this method to provide additional custom request @@ -184,23 +130,13 @@ public function availableMethods() { } /** - * Setups the base route for all HTTP methods. - * - * @param string $canonical_path - * The canonical path for the resource. - * @param string $method - * The HTTP method to be used for the route. - * - * @return \Symfony\Component\Routing\Route - * The created base route. + * {@inheritdoc} */ - protected function getBaseRoute($canonical_path, $method) { + public function getBaseRoute($canonical_path, $method) { $lower_method = strtolower($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/ResourceInterface.php b/core/modules/rest/src/Plugin/ResourceInterface.php index 2aef8324..3fc4f4e 100644 --- a/core/modules/rest/src/Plugin/ResourceInterface.php +++ b/core/modules/rest/src/Plugin/ResourceInterface.php @@ -22,17 +22,6 @@ interface ResourceInterface extends PluginInspectionInterface { /** - * Returns a collection of routes with URL path information for the resource. - * - * This method determines where a resource is reachable, what path - * replacements are used, the required HTTP method for the operation etc. - * - * @return \Symfony\Component\Routing\RouteCollection - * A collection of routes that should be registered for this resource. - */ - public function routes(); - - /** * Provides an array of permissions suitable for .permissions.yml files. * * A resource plugin can define a set of user permissions that are used on the @@ -51,4 +40,16 @@ public function permissions(); */ public function availableMethods(); + /** + * Setups the base route for all HTTP methods. + * + * @param string $canonical_path + * The canonical path for the resource. + * @param string $method + * The HTTP method to be used for the route. + * + * @return \Symfony\Component\Routing\Route + * The created base route. + */ + public function getBaseRoute($canonical_path, $method); } diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php index 34b0bda..561881d 100644 --- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php +++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php @@ -7,10 +7,14 @@ namespace Drupal\rest\Plugin\rest\resource; +use Drupal\Component\Plugin\DependentPluginInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityStorageException; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\rest\Plugin\ResourceBase; use Drupal\rest\ResourceResponse; +use Psr\Log\LoggerInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -31,7 +35,49 @@ * * @see \Drupal\rest\Plugin\Deriver\EntityDeriver */ -class EntityResource extends ResourceBase { +class EntityResource extends ResourceBase implements DependentPluginInterface { + /** + * Definition of the resources entity type. + * + * @var \Drupal\Core\Entity\EntityTypeInterface + */ + protected $entityType; + + /** + * Constructs a Drupal\rest\Plugin\rest\resource\EntityResource object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * 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. + */ + 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->entityType = $entity_type_manager->getDefinition($plugin_definition['entity_type']); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->getParameter('serializer.formats'), + $container->get('logger.factory')->get('rest') + ); + } /** * Responds to entity GET requests. @@ -232,7 +278,7 @@ protected function validate(EntityInterface $entity) { /** * {@inheritdoc} */ - protected function getBaseRoute($canonical_path, $method) { + public function getBaseRoute($canonical_path, $method) { $route = parent::getBaseRoute($canonical_path, $method); $definition = $this->getPluginDefinition(); @@ -243,5 +289,12 @@ protected function getBaseRoute($canonical_path, $method) { return $route; } - + /** + * {@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 5a04cd8..b8dad32 100644 --- a/core/modules/rest/src/RequestHandler.php +++ b/core/modules/rest/src/RequestHandler.php @@ -9,6 +9,7 @@ use Drupal\Core\Render\RenderContext; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\rest\Entity\RestEndpoint; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; use Symfony\Component\HttpFoundation\Request; @@ -36,13 +37,11 @@ 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') - ->getInstance(array('id' => $plugin)); + $endpoint_id = $route_match->getRouteObject()->getDefault('_rest_endpoint'); + /** @var \Drupal\rest\RestEndpointInterface $endpoint */ + $endpoint = RestEndpoint::load($endpoint_id); + $resource = $endpoint->getResourcePlugin(); // Deserialize incoming data if available. $serializer = $this->container->get('serializer'); @@ -55,9 +54,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 (!$endpoint->hasSupportedFormats($request_method) || $endpoint->supportsFormat($request_method, $format)) { $definition = $resource->getPluginDefinition(); $class = $definition['serialization_class']; try { @@ -121,8 +119,8 @@ public function handle(RouteMatchInterface $route_match, Request $request) { } $response->headers->set('Content-Type', $request->getMimeType($format)); - // Add rest settings config's cache tags. - $response->addCacheableDependency($this->container->get('config.factory')->get('rest.settings')); + // Add rest endpoint's cache tags. + $response->addCacheableDependency($endpoint); } return $response; } diff --git a/core/modules/rest/src/RestEndpointInterface.php b/core/modules/rest/src/RestEndpointInterface.php new file mode 100644 index 0000000..93618d7 --- /dev/null +++ b/core/modules/rest/src/RestEndpointInterface.php @@ -0,0 +1,130 @@ +restPluginManager = $rest_plugin_manager; - $this->configFactory = $config_factory; + $this->endpointStorage = $entity_type_manager->getStorage('rest_endpoint'); } /** * {@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')); } /** @@ -58,12 +58,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 $key => $resource) { - $plugin = $this->restPluginManager->getInstance(['id' => $key]); - $permissions = array_merge($permissions, $plugin->permissions()); - } + /** @var \Drupal\rest\RestEndpointInterface[] $endpoints */ + $endpoints = $this->endpointStorage->loadMultiple(); + foreach ($endpoints as $endpoint) { + $plugin = $endpoint->getResourcePlugin(); + $permissions = array_merge($permissions, $plugin->permissions()); } return $permissions; } diff --git a/core/modules/rest/src/RestServiceProvider.php b/core/modules/rest/src/RestServiceProvider.php new file mode 100644 index 0000000..0783dc8 --- /dev/null +++ b/core/modules/rest/src/RestServiceProvider.php @@ -0,0 +1,65 @@ +findTaggedServiceIds('authentication_provider') as $service_id => $service_tags) { + $service_def = $container->findDefinition($service_id); + $service_class = $service_def->getClass(); + if (!empty($service_class)) { + // Extract module name based on class namespace. + list($namespace_base, $module_name) = explode('\\', $service_class); + if (!empty($namespace_base) && ucfirst(strtolower($namespace_base)) == 'Drupal') { + // Remove the 'authentication.' prefix from the provider ID. + $provider_id = substr($service_id, 15); + $auth[$provider_id] = $module_name; + } + } + } + if (!empty($auth)) { + // Register the dependency list in the container. + $container->setParameter('rest.dependencies.auth', $auth); + } + + $formats = []; + // Build a dependency list for available formats. + foreach ($container->findTaggedServiceIds('encoder') as $service_id => $service_tags) { + $service_def = $container->findDefinition($service_id); + $service_class = $service_def->getClass(); + if (!empty($service_class)) { + // Extract module name based on class namespace. + list($namespace_base, $module_name) = explode('\\', $service_class); + if (!empty($namespace_base) && ucfirst(strtolower($namespace_base)) == 'Drupal') { + foreach ($service_tags as $service_tag) { + if (isset($service_tag['format'])) { + $format = $service_tag['format']; + $formats[$format] = $module_name; + } + } + } + } + } + if (!empty($formats)) { + // Register the dependency list in the container. + $container->setParameter('rest.dependencies.format', $formats); + } + } +} diff --git a/core/modules/rest/src/Routing/ResourceRoutes.php b/core/modules/rest/src/Routing/ResourceRoutes.php index 468cc5c..850dffe 100644 --- a/core/modules/rest/src/Routing/ResourceRoutes.php +++ b/core/modules/rest/src/Routing/ResourceRoutes.php @@ -7,9 +7,10 @@ namespace Drupal\rest\Routing; -use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Routing\RouteSubscriberBase; use Drupal\rest\Plugin\Type\ResourcePluginManager; +use Drupal\rest\RestEndpointInterface; use Psr\Log\LoggerInterface; use Symfony\Component\Routing\RouteCollection; @@ -26,11 +27,11 @@ class ResourceRoutes extends RouteSubscriberBase { protected $manager; /** - * The Drupal configuration factory. + * The REST endpoint storage. * - * @var \Drupal\Core\Config\ConfigFactoryInterface + * @var \Drupal\Core\Entity\EntityManagerInterface */ - protected $config; + protected $endpointStorage; /** * A logger instance. @@ -44,14 +45,14 @@ class ResourceRoutes extends RouteSubscriberBase { * * @param \Drupal\rest\Plugin\Type\ResourcePluginManager $manager * The resource plugin manager. - * @param \Drupal\Core\Config\ConfigFactoryInterface $config - * The configuration factory holding resource settings. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager * @param \Psr\Log\LoggerInterface $logger * A logger instance. */ - public function __construct(ResourcePluginManager $manager, ConfigFactoryInterface $config, LoggerInterface $logger) { + public function __construct(ResourcePluginManager $manager, EntityTypeManagerInterface $entity_type_manager, LoggerInterface $logger) { $this->manager = $manager; - $this->config = $config; + $this->endpointStorage = $entity_type_manager->getStorage('rest_endpoint'); $this->logger = $logger; } @@ -63,47 +64,95 @@ public function __construct(ResourcePluginManager $manager, ConfigFactoryInterfa * @return array */ protected function alterRoutes(RouteCollection $collection) { - $routes = array(); - $enabled_resources = $this->config->get('rest.settings')->get('resources') ?: array(); - - // Iterate over all enabled resource plugins. - foreach ($enabled_resources as $id => $enabled_methods) { - $plugin = $this->manager->getInstance(array('id' => $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); + // Iterate over all enabled REST endpoints. + /** @var \Drupal\rest\RestEndpointInterface[] $endpoints */ + $endpoints = $this->endpointStorage->loadMultiple(); + foreach ($endpoints as $endpoint) { + $endpoint_routes = $this->getRoutesForEndpoint($endpoint); + $collection->addCollection($endpoint_routes); + } + } + + /** + * Provides all routes for a given REST endpoint. + * + * This method determines where a resource is reachable, what path + * replacements are used, the required HTTP method for the operation etc. + * + * @param \Drupal\rest\RestEndpointInterface $rest_endpoint + * The rest endpoint. + * + * @return \Symfony\Component\Routing\RouteCollection + * The route collection. + */ + protected function getRoutesForEndpoint(RestEndpointInterface $rest_endpoint) { + $collection = new RouteCollection(); + + $plugin = $rest_endpoint->getResourcePlugin(); + $plugin_id = $plugin->getPluginId(); + $plugin_def = $plugin->getPluginDefinition(); + + $canonical_path = isset($plugin_def['uri_paths']['canonical']) ? $plugin_def['uri_paths']['canonical'] : '/' . strtr($plugin_id, ':', '/') . '/{id}'; + $create_path = isset($definition['uri_paths']['https://www.drupal.org/link-relations/create']) ? $definition['uri_paths']['https://www.drupal.org/link-relations/create'] : '/' . strtr($plugin_id, ':', '/'); + + $route_name = strtr($plugin_id, ':', '.'); + + $methods = $plugin->availableMethods(); + foreach ($methods as $method) { + if ($rest_endpoint->isRequestMethodEnabled($method)) { + // Check that authentication providers are defined. + if (!$rest_endpoint->supportsAuthenticationProviders($method)) { + $this->logger->error('At least one authentication provider must be defined for resource @id', array(':id' => $plugin_id)); + continue; + } + + // Check that formats are defined. + if (!$rest_endpoint->hasSupportedFormats($method)) { + $this->logger->error('At least one format must be defined for resource @id', array(':id' => $plugin_id)); + continue; + } + + $route = $plugin->getBaseRoute($canonical_path, $method); + $route->setRequirement('_access_rest_csrf', 'TRUE'); + $route->setOption('_auth', $rest_endpoint->getSupportedAuthenticationProviders($method)); + $route->setDefault('_rest_endpoint', $rest_endpoint->id()); + + $supported_formats = $rest_endpoint->getSupportedFormats($method); + switch ($method) { + case 'POST': + $route->setPath($create_path); + // Restrict the incoming HTTP Content-type header to the known + // serialization formats. + $route->addRequirements(array('_content_type_format' => implode('|', $supported_formats))); + $collection->add("rest.$route_name.$method", $route); + break; + case 'PATCH': + // Restrict the incoming HTTP Content-type header to the known + // serialization formats. + $route->addRequirements(array('_content_type_format' => implode('|', $supported_formats))); + $collection->add("rest.$route_name.$method", $route); + break; + + case 'GET': + case 'HEAD': + // Restrict GET and HEAD requests to the media type specified in the + // HTTP Accept headers. + foreach ($supported_formats as $format_name) { + // Expose one route per available format. + $format_route = clone $route; + $format_route->addRequirements(array('_format' => $format_name)); + $collection->add("rest.$route_name.$method.$format_name", $format_route); + } + break; + + default: + $collection->add("rest.$route_name.$method", $route); + break; } } } + + return $collection; } } diff --git a/core/modules/rest/src/Tests/AuthTest.php b/core/modules/rest/src/Tests/AuthTest.php index ab0b3b1..864da58b 100644 --- a/core/modules/rest/src/Tests/AuthTest.php +++ b/core/modules/rest/src/Tests/AuthTest.php @@ -21,7 +21,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 77f70be..eccb5b0 100644 --- a/core/modules/rest/src/Tests/CreateTest.php +++ b/core/modules/rest/src/Tests/CreateTest.php @@ -28,7 +28,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 5bc69bb..b2e7212 100644 --- a/core/modules/rest/src/Tests/DeleteTest.php +++ b/core/modules/rest/src/Tests/DeleteTest.php @@ -21,7 +21,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 4948900..7c86370 100644 --- a/core/modules/rest/src/Tests/NodeTest.php +++ b/core/modules/rest/src/Tests/NodeTest.php @@ -24,7 +24,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 3ae0084..d7d9077 100644 --- a/core/modules/rest/src/Tests/PageCacheTest.php +++ b/core/modules/rest/src/Tests/PageCacheTest.php @@ -38,23 +38,23 @@ public function testConfigChangePageCache() { $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), '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.endpoint.entity__entity_test'); $this->assertCacheTag('entity_test:1'); // Read it again, should be page-cached now. $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), '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.endpoint.entity__entity_test'); $this->assertCacheTag('entity_test:1'); - // 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 an endpoint save which should clear the page cache, so we should + // get a cache miss now for the same request. + $this->endpointStorage->load('entity__entity_test')->save(); $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), '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.endpoint.entity__entity_test'); $this->assertCacheTag('entity_test:1'); } diff --git a/core/modules/rest/src/Tests/RESTTestBase.php b/core/modules/rest/src/Tests/RESTTestBase.php index db7d123..22fef66 100644 --- a/core/modules/rest/src/Tests/RESTTestBase.php +++ b/core/modules/rest/src/Tests/RESTTestBase.php @@ -16,6 +16,13 @@ abstract class RESTTestBase extends WebTestBase { /** + * The REST endpoint storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $endpointStorage; + + /** * The default serialization format to use for testing REST operations. * * @var string @@ -56,15 +63,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->endpointStorage = $this->container->get('entity_type.manager')->getStorage('rest_endpoint'); // 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')); + } } /** @@ -247,24 +257,32 @@ protected function entityValues($entity_type) { * @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. + $endpoint_id = str_replace(':', '__', $resource_type); + // get entity by id + /** @var \Drupal\rest\RestEndpointInterface $endpoint */ + $endpoint = $this->endpointStorage->load($endpoint_id); + $endpoint = ($endpoint !== NULL) ? $endpoint : $this->endpointStorage->create(['id' => $endpoint_id]); + if ($format == NULL) { $format = $this->defaultFormat; } - $settings[$resource_type][$method]['supported_formats'][] = $format; + $endpoint->addSupportedFormat($method, $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) { + $endpoint->addSupportedAuthenticationProvider($method, $auth_provider); + } + $endpoint->save(); + } else { + foreach ($this->endpointStorage->loadMultiple() as $endpoint) { + $endpoint->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 eb3f222..4b83b9a 100644 --- a/core/modules/rest/src/Tests/ReadTest.php +++ b/core/modules/rest/src/Tests/ReadTest.php @@ -22,7 +22,7 @@ class ReadTest 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 read requests on all entity types. diff --git a/core/modules/rest/src/Tests/ResourceTest.php b/core/modules/rest/src/Tests/ResourceTest.php index c20b751..22bbea1 100644 --- a/core/modules/rest/src/Tests/ResourceTest.php +++ b/core/modules/rest/src/Tests/ResourceTest.php @@ -35,9 +35,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(); @@ -50,19 +48,12 @@ 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( - 'basic_auth', - ), - ), - ), - ); - + /** @var \Drupal\rest\RestEndpointInterface $endpoint */ + $endpoint = $this->endpointStorage->create(['id' => 'entity__entity_test']); // Attempt to enable the resource. - $this->config->set('resources', $settings); - $this->config->save(); + $endpoint + ->addSupportedAuthenticationProvider('GET', 'basic_auth') + ->save(); $this->rebuildCache(); // Verify that accessing the resource returns 406. @@ -80,19 +71,12 @@ 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( - 'hal_json', - ), - ), - ), - ); - + /** @var \Drupal\rest\RestEndpointInterface $endpoint */ + $endpoint = $this->endpointStorage->create(['id' => 'entity__entity_test']); // Attempt to enable the resource. - $this->config->set('resources', $settings); - $this->config->save(); + $endpoint + ->addSupportedFormat('GET', 'hal_json') + ->save(); $this->rebuildCache(); // Verify that accessing the resource returns 401. diff --git a/core/modules/system/src/Tests/System/ResponseGeneratorTest.php b/core/modules/system/src/Tests/System/ResponseGeneratorTest.php index a2ed46c..a4141df 100644 --- a/core/modules/system/src/Tests/System/ResponseGeneratorTest.php +++ b/core/modules/system/src/Tests/System/ResponseGeneratorTest.php @@ -21,7 +21,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 e5780a1..114b4a0 100644 --- a/core/modules/system/src/Tests/Update/UpdatePathTestBase.php +++ b/core/modules/system/src/Tests/Update/UpdatePathTestBase.php @@ -270,7 +270,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 169921e..8733188 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1363,7 +1363,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); + } } }