Update to Drupal 8.2.0. For more information, see https://www.drupal.org/project/drupal/releases/8.2.0

This commit is contained in:
Pantheon Automation 2016-10-06 15:16:20 -07:00 committed by Greg Anderson
parent 2f563ab520
commit f1c8716f57
1732 changed files with 52334 additions and 11780 deletions

View file

@ -1,50 +1,11 @@
# 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: ~
# Before Drupal 8.2, EntityResource used permissions as well as the entity
# access system for access checking. This was confusing, and it only did this
# for historical reasons. New Drupal installations opt out from this by default
# (hence this is set to false), existing installations opt in to it.
# @see rest_update_8203()
# @see https://www.drupal.org/node/2664780
bc_entity_resource_permissions: false

View file

@ -0,0 +1,20 @@
langcode: en
status: true
dependencies:
module:
- basic_auth
- hal
- node
id: entity.node
plugin_id: 'entity:node'
granularity: resource
configuration:
methods:
- GET
- POST
- PATCH
- DELETE
formats:
- hal_json
authentication:
- basic_auth

View file

@ -1,20 +1,17 @@
# 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'
bc_entity_resource_permissions:
type: boolean
label: 'Whether the pre Drupal 8.2.x behavior of having permissions for EntityResource is enabled or not.'
rest_resource:
# Method-level granularity of REST resource configuration.
rest_resource.method:
type: mapping
mapping:
GET:
@ -30,6 +27,29 @@ rest_resource:
type: rest_request
label: 'DELETE method settings'
# Resource-level granularity of REST resource configuration.
rest_resource.resource:
type: mapping
mapping:
methods:
type: sequence
label: 'Supported methods'
sequence:
type: string
label: 'HTTP method'
formats:
type: sequence
label: 'Supported formats'
sequence:
type: string
label: 'Format'
authentication:
type: sequence
label: 'Supported authentication providers'
sequence:
type: string
label: 'Authentication provider'
rest_request:
type: mapping
mapping:
@ -45,3 +65,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'

View file

@ -3,6 +3,13 @@
views.display.rest_export:
type: views_display_path
label: 'REST display options'
mapping:
auth:
type: sequence
label: 'Authentication'
sequence:
type: string
label: 'Authentication Provider'
views.row.data_field:
type: views_row

View file

@ -5,6 +5,9 @@
* Install, update and uninstall functions for the rest module.
*/
use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\Core\StringTranslation\TranslatableMarkup;
/**
* Implements hook_requirements().
*/
@ -21,3 +24,73 @@ function rest_requirements($phase) {
}
return $requirements;
}
/**
* @defgroup updates-8.1.x-to-8.2.x
* @{
* Update functions from 8.1.x to 8.2.x.
*/
/**
* Install the REST config entity type and fix old settings-based config.
*
* @see rest_post_update_create_rest_resource_config_entities()
*/
function rest_update_8201() {
\Drupal::entityDefinitionUpdateManager()->installEntityType(new ConfigEntityType([
'id' => 'rest_resource_config',
'label' => new TranslatableMarkup('REST resource configuration'),
'config_prefix' => 'resource',
'admin_permission' => 'administer rest resources',
'label_callback' => 'getLabelFromPlugin',
'entity_keys' => ['id' => 'id'],
'config_export' => [
'id',
'plugin_id',
'granularity',
'configuration',
],
]));
\Drupal::state()->set('rest_update_8201_resources', \Drupal::config('rest.settings')->get('resources'));
\Drupal::configFactory()->getEditable('rest.settings')
->clear('resources')
->save();
}
/**
* Re-save all views with a REST display to add new auth defaults.
*/
function rest_update_8202() {
$config_factory = \Drupal::configFactory();
foreach ($config_factory->listAll('views.view.') as $view_config_name) {
$save = FALSE;
$view = $config_factory->getEditable($view_config_name);
$displays = $view->get('display');
foreach ($displays as $display_name => &$display) {
if ($display['display_plugin'] == 'rest_export') {
if (!isset($display['display_options']['auth'])) {
$display['display_options']['auth'] = [];
$save = TRUE;
}
}
}
if ($save) {
$view->set('display', $displays);
$view->save(TRUE);
}
}
}
/**
* Enable BC for EntityResource: continue to use permissions.
*/
function rest_update_8203() {
$config_factory = \Drupal::configFactory();
$rest_settings = $config_factory->getEditable('rest.settings');
$rest_settings->set('bc_entity_resource_permissions', TRUE)
->save(TRUE);
}
/**
* @} End of "defgroup updates-8.1.x-to-8.2.x".
*/

View file

@ -1,2 +1,5 @@
permission_callbacks:
- Drupal\rest\RestPermissions::permissions
administer rest resources:
title: 'Administer REST resource configuration'

View file

@ -0,0 +1,73 @@
<?php
/**
* @file
* Post update functions for Rest.
*/
use Drupal\rest\Entity\RestResourceConfig;
use Drupal\rest\RestResourceConfigInterface;
/**
* @addtogroup updates-8.1.x-to-8.2.x
* @{
*/
/**
* Create REST resource configuration entities.
*
* @see rest_update_8201()
* @see https://www.drupal.org/node/2308745
*/
function rest_post_update_create_rest_resource_config_entities() {
$resources = \Drupal::state()->get('rest_update_8201_resources', []);
foreach ($resources as $key => $resource) {
$resource = RestResourceConfig::create([
'id' => str_replace(':', '.', $key),
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
'configuration' => $resource,
]);
$resource->save();
}
}
/**
* Simplify method-granularity REST resource config to resource-granularity.
*
* @see https://www.drupal.org/node/2721595
*/
function rest_post_update_resource_granularity() {
/** @var \Drupal\rest\RestResourceConfigInterface[] $resource_config_entities */
$resource_config_entities = RestResourceConfig::loadMultiple();
foreach ($resource_config_entities as $resource_config_entity) {
if ($resource_config_entity->get('granularity') === RestResourceConfigInterface::METHOD_GRANULARITY) {
$configuration = $resource_config_entity->get('configuration');
$format_and_auth_configuration = [];
foreach (array_keys($configuration) as $method) {
$format_and_auth_configuration['format'][$method] = implode(',', $configuration[$method]['supported_formats']);
$format_and_auth_configuration['auth'][$method] = implode(',', $configuration[$method]['supported_auth']);
}
// If each method has the same formats and the same authentication
// providers configured, convert it to 'granularity: resource', which has
// a simpler/less verbose configuration.
if (count(array_unique($format_and_auth_configuration['format'])) === 1 && count(array_unique($format_and_auth_configuration['auth'])) === 1) {
$first_method = array_keys($configuration)[0];
$resource_config_entity->set('configuration', [
'methods' => array_keys($configuration),
'formats' => $configuration[$first_method]['supported_formats'],
'authentication' => $configuration[$first_method]['supported_auth']
]);
$resource_config_entity->set('granularity', RestResourceConfigInterface::RESOURCE_GRANULARITY);
$resource_config_entity->save();
}
}
}
}
/**
* @} End of "addtogroup updates-8.1.x-to-8.2.x".
*/

View file

@ -1,6 +1,9 @@
# @deprecated This route is deprecated, use the system.csrftoken route from the
# system module instead.
# @todo Remove this route in Drupal 9.0.0.
rest.csrftoken:
path: '/rest/session/token'
defaults:
_controller: '\Drupal\rest\RequestHandler::csrfToken'
_controller: '\Drupal\system\Controller\CsrfTokenController::csrfToken'
requirements:
_access: 'TRUE'

View file

@ -8,23 +8,21 @@ services:
- { name: cache.bin }
factory: cache_factory:get
arguments: [rest]
# @todo Remove this service in Drupal 9.0.0.
access_check.rest.csrf:
class: Drupal\rest\Access\CSRFAccessCheck
arguments: ['@session_configuration']
tags:
- { name: access_check }
alias: access_check.header.csrf
rest.link_manager:
class: Drupal\rest\LinkManager\LinkManager
arguments: ['@rest.link_manager.type', '@rest.link_manager.relation']
rest.link_manager.type:
class: Drupal\rest\LinkManager\TypeLinkManager
arguments: ['@cache.default', '@module_handler', '@config.factory', '@request_stack']
arguments: ['@cache.default', '@module_handler', '@config.factory', '@request_stack', '@entity_type.bundle.info']
rest.link_manager.relation:
class: Drupal\rest\LinkManager\RelationLinkManager
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:

View file

@ -1,88 +0,0 @@
<?php
namespace Drupal\rest\Access;
use Drupal\Core\Access\AccessCheckInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Session\SessionConfigurationInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\HttpFoundation\Request;
/**
* Access protection against CSRF attacks.
*/
class CSRFAccessCheck implements AccessCheckInterface {
/**
* The session configuration.
*
* @var \Drupal\Core\Session\SessionConfigurationInterface
*/
protected $sessionConfiguration;
/**
* Constructs a new rest CSRF access check.
*
* @param \Drupal\Core\Session\SessionConfigurationInterface $session_configuration
* The session configuration.
*/
public function __construct(SessionConfigurationInterface $session_configuration) {
$this->sessionConfiguration = $session_configuration;
}
/**
* {@inheritdoc}
*/
public function applies(Route $route) {
$requirements = $route->getRequirements();
if (array_key_exists('_access_rest_csrf', $requirements)) {
if (isset($requirements['_method'])) {
// There could be more than one method requirement separated with '|'.
$methods = explode('|', $requirements['_method']);
// CSRF protection only applies to write operations, so we can filter
// out any routes that require reading methods only.
$write_methods = array_diff($methods, array('GET', 'HEAD', 'OPTIONS', 'TRACE'));
if (empty($write_methods)) {
return FALSE;
}
}
// No method requirement given, so we run this access check to be on the
// safe side.
return TRUE;
}
}
/**
* Checks access.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\Core\Session\AccountInterface $account
* The currently logged in account.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result.
*/
public function access(Request $request, AccountInterface $account) {
$method = $request->getMethod();
// This check only applies if
// 1. this is a write operation
// 2. the user was successfully authenticated and
// 3. the request comes with a session cookie.
if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))
&& $account->isAuthenticated()
&& $this->sessionConfiguration->hasSession($request)
) {
$csrf_token = $request->headers->get('X-CSRF-Token');
if (!\Drupal::csrfToken()->validate($csrf_token, 'rest')) {
return AccessResult::forbidden()->setCacheMaxAge(0);
}
}
// Let other access checkers decide if the request is legit.
return AccessResult::allowed()->setCacheMaxAge(0);
}
}

View file

@ -38,4 +38,11 @@ class RestResource extends Plugin {
*/
public $label;
/**
* The serialization class to deserialize serialized data into.
*
* @var string (optional)
*/
public $serialization_class;
}

View file

@ -0,0 +1,271 @@
<?php
namespace Drupal\rest\Entity;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\rest\RestResourceConfigInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Calculates rest resource config dependencies.
*
* @internal
*/
class ConfigDependencies implements ContainerInjectionInterface {
/**
* The serialization format providers, keyed by format.
*
* @var string[]
*/
protected $formatProviders;
/**
* The authentication providers, keyed by ID.
*
* @var string[]
*/
protected $authProviders;
/**
* Creates a new ConfigDependencies instance.
*
* @param string[] $format_providers
* The serialization format providers, keyed by format.
* @param string[] $auth_providers
* The authentication providers, keyed by ID.
*/
public function __construct(array $format_providers, array $auth_providers) {
$this->formatProviders = $format_providers;
$this->authProviders = $auth_providers;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->getParameter('serializer.format_providers'),
$container->getParameter('authentication_providers')
);
}
/**
* Calculates dependencies of a specific rest resource configuration.
*
* This function returns dependencies in a non-sorted, non-unique manner. It
* is therefore the caller's responsibility to sort and remove duplicates
* from the result prior to saving it with the configuration or otherwise
* using it in a way that requires that. For example,
* \Drupal\rest\Entity\RestResourceConfig::calculateDependencies() does this
* via its \Drupal\Core\Entity\DependencyTrait::addDependency() method.
*
* @param \Drupal\rest\RestResourceConfigInterface $rest_config
* The rest configuration.
*
* @return string[][]
* Dependencies keyed by dependency type.
*
* @see \Drupal\rest\Entity\RestResourceConfig::calculateDependencies()
*/
public function calculateDependencies(RestResourceConfigInterface $rest_config) {
$granularity = $rest_config->get('granularity');
// Dependency calculation is the same for either granularity, the most
// notable difference is that for the 'resource' granularity, the same
// authentication providers and formats are supported for every method.
switch ($granularity) {
case RestResourceConfigInterface::METHOD_GRANULARITY:
$methods = $rest_config->getMethods();
break;
case RestResourceConfigInterface::RESOURCE_GRANULARITY:
$methods = array_slice($rest_config->getMethods(), 0, 1);
break;
default:
throw new \InvalidArgumentException('Invalid granularity specified.');
}
// The dependency lists for authentication providers and formats
// generated on container build.
$dependencies = [];
foreach ($methods 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');
switch ($granularity) {
case RestResourceConfigInterface::METHOD_GRANULARITY:
return $this->onDependencyRemovalForMethodGranularity($rest_config, $dependencies);
case RestResourceConfigInterface::RESOURCE_GRANULARITY:
return $this->onDependencyRemovalForResourceGranularity($rest_config, $dependencies);
default:
throw new \InvalidArgumentException('Invalid granularity specified.');
}
}
/**
* 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), TRUE)) {
$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), TRUE)) {
$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 ($configuration_before != $configuration && !empty($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;
}
/**
* 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 onDependencyRemovalForResourceGranularity(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)) {
// All methods support the same formats and authentication providers, so
// get those for whichever the first listed method is.
$first_method = $rest_config->getMethods()[0];
// Try to fix dependency problems by removing affected
// authentication providers and formats.
foreach ($removed_formats as $format) {
if (in_array($format, $rest_config->getFormats($first_method), TRUE)) {
$configuration['formats'] = array_diff($configuration['formats'], $removed_formats);
}
}
foreach ($removed_auth as $auth) {
if (in_array($auth, $rest_config->getAuthenticationProviders($first_method), TRUE)) {
$configuration['authentication'] = array_diff($configuration['authentication'], $removed_auth);
}
}
if (empty($configuration['authentication'])) {
// Remove the key if there are no more authentication providers
// supported.
unset($configuration['authentication']);
}
if (empty($configuration['formats'])) {
// Remove the key if there are no more formats supported.
unset($configuration['formats']);
}
if (empty($configuration['authentication']) || empty($configuration['formats'])) {
// If there no longer are any supported authentication providers or
// formats, this REST resource can no longer function, and so we
// cannot fix this config entity to keep it working.
$configuration = [];
}
}
if ($configuration_before != $configuration && !empty($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;
}
}

View file

@ -0,0 +1,266 @@
<?php
namespace Drupal\rest\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
use Drupal\rest\RestResourceConfigInterface;
/**
* Defines a RestResourceConfig configuration entity class.
*
* @ConfigEntityType(
* id = "rest_resource_config",
* label = @Translation("REST resource configuration"),
* config_prefix = "resource",
* admin_permission = "administer rest resources",
* label_callback = "getLabelFromPlugin",
* entity_keys = {
* "id" = "id"
* },
* config_export = {
* "id",
* "plugin_id",
* "granularity",
* "configuration"
* }
* )
*/
class RestResourceConfig extends ConfigEntityBase implements RestResourceConfigInterface {
/**
* The REST resource config id.
*
* @var string
*/
protected $id;
/**
* The REST resource plugin id.
*
* @var string
*/
protected $plugin_id;
/**
* The REST resource configuration granularity.
*
* Currently either:
* - \Drupal\rest\RestResourceConfigInterface::METHOD_GRANULARITY
* - \Drupal\rest\RestResourceConfigInterface::RESOURCE_GRANULARITY
*
* @var string
*/
protected $granularity;
/**
* The REST resource configuration.
*
* @var array
*/
protected $configuration;
/**
* The rest resource plugin manager.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $pluginManager;
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type) {
parent::__construct($values, $entity_type);
// The config entity id looks like the plugin id but uses __ instead of :
// because : is not valid for config entities.
if (!isset($this->plugin_id) && isset($this->id)) {
// Generate plugin_id on first entity creation.
$this->plugin_id = str_replace('.', ':', $this->id);
}
}
/**
* The label callback for this configuration entity.
*
* @return string The label.
*/
protected function getLabelFromPlugin() {
$plugin_definition = $this->getResourcePluginManager()
->getDefinition(['id' => $this->plugin_id]);
return $plugin_definition['label'];
}
/**
* Returns the resource plugin manager.
*
* @return \Drupal\Component\Plugin\PluginManagerInterface
*/
protected function getResourcePluginManager() {
if (!isset($this->pluginManager)) {
$this->pluginManager = \Drupal::service('plugin.manager.rest');
}
return $this->pluginManager;
}
/**
* {@inheritdoc}
*/
public function getResourcePlugin() {
return $this->getPluginCollections()['resource']->get($this->plugin_id);
}
/**
* {@inheritdoc}
*/
public function getMethods() {
switch ($this->granularity) {
case RestResourceConfigInterface::METHOD_GRANULARITY:
return $this->getMethodsForMethodGranularity();
case RestResourceConfigInterface::RESOURCE_GRANULARITY:
return $this->configuration['methods'];
default:
throw new \InvalidArgumentException('Invalid granularity specified.');
}
}
/**
* 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) {
switch ($this->granularity) {
case RestResourceConfigInterface::METHOD_GRANULARITY:
return $this->getAuthenticationProvidersForMethodGranularity($method);
case RestResourceConfigInterface::RESOURCE_GRANULARITY:
return $this->configuration['authentication'];
default:
throw new \InvalidArgumentException('Invalid granularity specified.');
}
}
/**
* 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) {
switch ($this->granularity) {
case RestResourceConfigInterface::METHOD_GRANULARITY:
return $this->getFormatsForMethodGranularity($method);
case RestResourceConfigInterface::RESOURCE_GRANULARITY:
return $this->configuration['formats'];
default:
throw new \InvalidArgumentException('Invalid granularity specified.');
}
}
/**
* 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;
}
}

View file

@ -5,6 +5,7 @@ namespace Drupal\rest\LinkManager;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
@ -24,6 +25,13 @@ class TypeLinkManager extends LinkManagerBase implements TypeLinkManagerInterfac
*/
protected $moduleHandler;
/**
* The bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfoService;
/**
* Constructor.
*
@ -35,12 +43,15 @@ class TypeLinkManager extends LinkManagerBase implements TypeLinkManagerInterfac
* The config factory service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info_service
* The bundle info service.
*/
public function __construct(CacheBackendInterface $cache, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, RequestStack $request_stack) {
public function __construct(CacheBackendInterface $cache, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, RequestStack $request_stack, EntityTypeBundleInfoInterface $bundle_info_service) {
$this->cache = $cache;
$this->configFactory = $config_factory;
$this->moduleHandler = $module_handler;
$this->requestStack = $request_stack;
$this->bundleInfoService = $bundle_info_service;
}
/**
@ -89,10 +100,12 @@ class TypeLinkManager extends LinkManagerBase implements TypeLinkManagerInterfac
$cid = 'rest:links:types';
$cache = $this->cache->get($cid);
if (!$cache) {
$this->writeCache($context);
$cache = $this->cache->get($cid);
$data = $this->writeCache($context);
}
return $cache->data;
else {
$data = $cache->data;
}
return $data;
}
/**
@ -100,6 +113,10 @@ class TypeLinkManager extends LinkManagerBase implements TypeLinkManagerInterfac
*
* @param array $context
* Context from the normalizer/serializer operation.
*
* @return array
* An array of typed data ids (entity_type and bundle) keyed by
* corresponding type URI.
*/
protected function writeCache($context = array()) {
$data = array();
@ -107,7 +124,7 @@ class TypeLinkManager extends LinkManagerBase implements TypeLinkManagerInterfac
// Type URIs correspond to bundles. Iterate through the bundles to get the
// URI and data for them.
$entity_types = \Drupal::entityManager()->getDefinitions();
foreach (entity_get_bundles() as $entity_type_id => $bundles) {
foreach ($this->bundleInfoService->getAllBundleInfo() as $entity_type_id => $bundles) {
// Only content entities are supported currently.
// @todo Consider supporting config entities.
if ($entity_types[$entity_type_id]->isSubclassOf('\Drupal\Core\Config\Entity\ConfigEntityInterface')) {
@ -125,6 +142,7 @@ class TypeLinkManager extends LinkManagerBase implements TypeLinkManagerInterfac
// These URIs only change when entity info changes, so cache it permanently
// and only clear it when entity_info is cleared.
$this->cache->set('rest:links:types', $data, Cache::PERMANENT, array('entity_types'));
return $data;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\rest;
use Symfony\Component\HttpFoundation\Response;
/**
* A response that does not contain cacheability metadata.
*
* Used when resources are modified by a request: responses to unsafe requests
* (POST/PATCH/DELETE) can never be cached.
*
* @see \Drupal\rest\ResourceResponse
*/
class ModifiedResourceResponse extends Response implements ResourceResponseInterface {
use ResourceResponseTrait;
/**
* Constructor for ModifiedResourceResponse objects.
*
* @param mixed $data
* Response data that should be serialized.
* @param int $status
* The response status code.
* @param array $headers
* An array of response headers.
*/
public function __construct($data = NULL, $status = 200, $headers = []) {
$this->responseData = $data;
parent::__construct('', $status, $headers);
}
}

View file

@ -12,6 +12,11 @@ use Symfony\Component\Routing\RouteCollection;
/**
* Common base class for resource plugins.
*
* Note that this base class' implementation of the permissions() method
* generates a permission for every method for a resource. If your resource
* already has its own access control mechanism, you should opt out from this
* default permissions() method by overriding it.
*
* @see \Drupal\rest\Annotation\RestResource
* @see \Drupal\rest\Plugin\Type\ResourcePluginManager
* @see \Drupal\rest\Plugin\ResourceInterface
@ -179,7 +184,7 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
}
/**
* Setups the base route for all HTTP methods.
* Gets the base route for a particular method.
*
* @param string $canonical_path
* The canonical path for the resource.
@ -190,22 +195,48 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
* The created base route.
*/
protected function getBaseRoute($canonical_path, $method) {
$lower_method = strtolower($method);
$route = new Route($canonical_path, array(
return 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",
),
$this->getBaseRouteRequirements($method),
array(),
'',
array(),
// The HTTP method is a requirement for this route.
array($method)
);
return $route;
}
/**
* Gets the base route requirements for a particular method.
*
* @param $method
* The HTTP method to be used for the route.
*
* @return array
* An array of requirements for parameters.
*/
protected function getBaseRouteRequirements($method) {
$lower_method = strtolower($method);
// Every route MUST have requirements that result in the access manager
// having access checks to check. If it does not, the route is made
// inaccessible. So, we default to granting access to everyone. If a
// permission exists, then we add that below. The access manager requires
// that ALL access checks must grant access, so this still results in
// correct behavior.
$requirements = [
'_access' => 'TRUE',
];
// Only specify route requirements if the default permission exists. For any
// more advanced route definition, resource plugins extending this base
// class must override this method.
$permission = "restful $lower_method $this->pluginId";
if (isset($this->permissions()[$permission])) {
$requirements['_permission'] = $permission;
}
return $requirements;
}
}

View file

@ -33,6 +33,10 @@ interface ResourceInterface extends PluginInspectionInterface {
* A resource plugin can define a set of user permissions that are used on the
* routes for this resource or for other purposes.
*
* It is not required for a resource plugin to specify permissions: if they
* have their own access control mechanism, they can use that, and return the
* empty array.
*
* @return array
* The permission array.
*/

View file

@ -36,6 +36,10 @@ class ResourcePluginManager extends DefaultPluginManager {
/**
* {@inheritdoc}
*
* @deprecated in Drupal 8.2.0.
* Use Drupal\rest\Plugin\Type\ResourcePluginManager::createInstance()
* instead.
*/
public function getInstance(array $options){
if (isset($options['id'])) {

View file

@ -2,10 +2,18 @@
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;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageException;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
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;
@ -26,7 +34,60 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
* }
* )
*/
class EntityResource extends ResourceBase {
class EntityResource extends ResourceBase implements DependentPluginInterface {
/**
* The entity type targeted by this resource.
*
* @var \Drupal\Core\Entity\EntityTypeInterface
*/
protected $entityType;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* 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.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, $serializer_formats, LoggerInterface $logger, ConfigFactoryInterface $config_factory) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
$this->entityType = $entity_type_manager->getDefinition($plugin_definition['entity_type']);
$this->configFactory = $config_factory;
}
/**
* {@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'),
$container->get('config.factory')
);
}
/**
* Responds to entity GET requests.
@ -48,13 +109,16 @@ class EntityResource extends ResourceBase {
$response = new ResourceResponse($entity, 200);
$response->addCacheableDependency($entity);
$response->addCacheableDependency($entity_access);
foreach ($entity as $field_name => $field) {
/** @var \Drupal\Core\Field\FieldItemListInterface $field */
$field_access = $field->access('view', NULL, TRUE);
$response->addCacheableDependency($field_access);
if (!$field_access->isAllowed()) {
$entity->set($field_name, NULL);
if ($entity instanceof FieldableEntityInterface) {
foreach ($entity as $field_name => $field) {
/** @var \Drupal\Core\Field\FieldItemListInterface $field */
$field_access = $field->access('view', NULL, TRUE);
$response->addCacheableDependency($field_access);
if (!$field_access->isAllowed()) {
$entity->set($field_name, NULL);
}
}
}
@ -67,7 +131,7 @@ class EntityResource extends ResourceBase {
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return \Drupal\rest\ResourceResponse
* @return \Drupal\rest\ModifiedResourceResponse
* The HTTP response object.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
@ -108,11 +172,10 @@ class EntityResource extends ResourceBase {
$this->logger->notice('Created entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
// 201 Created responses return the newly created entity in the response
// body.
// body. These responses are not cacheable, so we add no cacheability
// metadata here.
$url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
$response = new ResourceResponse($entity, 201, ['Location' => $url->getGeneratedUrl()]);
// Responses after creating an entity are not cacheable, so we add no
// cacheability metadata here.
$response = new ModifiedResourceResponse($entity, 201, ['Location' => $url->getGeneratedUrl()]);
return $response;
}
catch (EntityStorageException $e) {
@ -128,7 +191,7 @@ class EntityResource extends ResourceBase {
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
*
* @return \Drupal\rest\ResourceResponse
* @return \Drupal\rest\ModifiedResourceResponse
* The HTTP response object.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
@ -178,8 +241,8 @@ class EntityResource extends ResourceBase {
$original_entity->save();
$this->logger->notice('Updated entity %type with ID %id.', array('%type' => $original_entity->getEntityTypeId(), '%id' => $original_entity->id()));
// Update responses have an empty body.
return new ResourceResponse(NULL, 204);
// Return the updated entity in the response body.
return new ModifiedResourceResponse($original_entity, 200);
}
catch (EntityStorageException $e) {
throw new HttpException(500, 'Internal Server Error', $e);
@ -192,7 +255,7 @@ class EntityResource extends ResourceBase {
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
*
* @return \Drupal\rest\ResourceResponse
* @return \Drupal\rest\ModifiedResourceResponse
* The HTTP response object.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
@ -206,7 +269,7 @@ class EntityResource extends ResourceBase {
$this->logger->notice('Deleted entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
// Delete responses have an empty body.
return new ResourceResponse(NULL, 204);
return new ModifiedResourceResponse(NULL, 204);
}
catch (EntityStorageException $e) {
throw new HttpException(500, 'Internal Server Error', $e);
@ -223,6 +286,10 @@ class EntityResource extends ResourceBase {
* If validation errors are found.
*/
protected function validate(EntityInterface $entity) {
// @todo Remove when https://www.drupal.org/node/2164373 is committed.
if (!$entity instanceof FieldableEntityInterface) {
return;
}
$violations = $entity->validate();
// Remove violations of inaccessible fields as they cannot stem from our
@ -242,6 +309,21 @@ class EntityResource extends ResourceBase {
}
}
/**
* {@inheritdoc}
*/
public function permissions() {
// @see https://www.drupal.org/node/2664780
if ($this->configFactory->get('rest.settings')->get('bc_entity_resource_permissions')) {
// The default Drupal 8.0.x and 8.1.x behavior.
return parent::permissions();
}
else {
// The default Drupal 8.2.x behavior.
return [];
}
}
/**
* {@inheritdoc}
*/
@ -256,4 +338,37 @@ class EntityResource extends ResourceBase {
return $route;
}
/**
* {@inheritdoc}
*/
public function availableMethods() {
$methods = parent::availableMethods();
if ($this->isConfigEntityResource()) {
// Currently only GET is supported for Config Entities.
// @todo Remove when supported https://www.drupal.org/node/2300677
$unsupported_methods = ['POST', 'PUT', 'DELETE', 'PATCH'];
$methods = array_diff($methods, $unsupported_methods);
}
return $methods;
}
/**
* Checks if this resource is for a Config Entity.
*
* @return bool
* TRUE if the entity is a Config Entity, FALSE otherwise.
*/
protected function isConfigEntityResource() {
return $this->entityType instanceof ConfigEntityType;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
if (isset($this->entityType)) {
return ['module' => [$this->entityType->getProvider()]];
}
}
}

View file

@ -4,6 +4,7 @@ namespace Drupal\rest\Plugin\views\display;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\CacheableResponse;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteProviderInterface;
@ -77,6 +78,20 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
*/
protected $renderer;
/**
* The collector of authentication providers.
*
* @var \Drupal\Core\Authentication\AuthenticationCollectorInterface
*/
protected $authenticationCollector;
/**
* The authentication providers, keyed by ID.
*
* @var string[]
*/
protected $authenticationProviders;
/**
* Constructs a RestExport object.
*
@ -92,11 +107,14 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
* The state key value store.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param string[] $authentication_providers
* The authentication providers, keyed by ID.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer, array $authentication_providers) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state);
$this->renderer = $renderer;
$this->authenticationProviders = $authentication_providers;
}
/**
@ -109,7 +127,9 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
$plugin_definition,
$container->get('router.route_provider'),
$container->get('state'),
$container->get('renderer')
$container->get('renderer'),
$container->getParameter('authentication_providers')
);
}
/**
@ -199,12 +219,25 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
return $this->contentType;
}
/**
* Gets the auth options available.
*
* @return string[]
* An array to use as value for "#options" in the form element.
*/
public function getAuthOptions() {
return array_combine($this->authenticationProviders, $this->authenticationProviders);
}
/**
* {@inheritdoc}
*/
protected function defineOptions() {
$options = parent::defineOptions();
// Options for REST authentication.
$options['auth'] = ['default' => []];
// Set the default style plugin to 'json'.
$options['style']['contains']['type']['default'] = 'serializer';
$options['row']['contains']['type']['default'] = 'data_entity';
@ -225,6 +258,9 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
public function optionsSummary(&$categories, &$options) {
parent::optionsSummary($categories, $options);
// Authentication.
$auth = $this->getOption('auth') ? implode(', ', $this->getOption('auth')) : $this->t('No authentication is set');
unset($categories['page'], $categories['exposed']);
// Hide some settings, as they aren't useful for pure data output.
unset($options['show_admin_links'], $options['analyze-theme']);
@ -239,6 +275,11 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
$options['path']['category'] = 'path';
$options['path']['title'] = $this->t('Path');
$options['auth'] = array(
'category' => 'path',
'title' => $this->t('Authentication'),
'value' => views_ui_truncate($auth, 24),
);
// Remove css/exposed form settings, as they are not used for the data
// display.
@ -247,6 +288,34 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
unset($options['css_class']);
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
if ($form_state->get('section') === 'auth') {
$form['#title'] .= $this->t('The supported authentication methods for this view');
$form['auth'] = array(
'#type' => 'checkboxes',
'#title' => $this->t('Authentication methods'),
'#description' => $this->t('These are the supported authentication providers for this view. When this view is requested, the client will be forced to authenticate with one of the selected providers. Make sure you set the appropiate requirements at the <em>Access</em> section since the Authentication System will fallback to the anonymous user if it fails to authenticate. For example: require Access: Role | Authenticated User.'),
'#options' => $this->getAuthOptions(),
'#default_value' => $this->getOption('auth'),
);
}
}
/**
* {@inheritdoc}
*/
public function submitOptionsForm(&$form, FormStateInterface $form_state) {
parent::submitOptionsForm($form, $form_state);
if ($form_state->get('section') == 'auth') {
$this->setOption('auth', array_keys(array_filter($form_state->getValue('auth'))));
}
}
/**
* {@inheritdoc}
*/
@ -268,6 +337,13 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
// anyway.
$route->setRequirement('_format', implode('|', $formats + ['html']));
}
// Add authentication to the route if it was set. If no authentication was
// set, the default authentication will be used, which is cookie based by
// default.
$auth = $this->getOption('auth');
if (!empty($auth)) {
$route->setOption('_auth', $auth);
}
}
}
@ -277,12 +353,17 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
public static function buildResponse($view_id, $display_id, array $args = []) {
$build = static::buildBasicRenderable($view_id, $display_id, $args);
// Setup an empty response so headers can be added as needed during views
// rendering and processing.
$response = new CacheableResponse('', 200);
$build['#response'] = $response;
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$output = $renderer->renderRoot($build);
$output = (string) $renderer->renderRoot($build);
$response = new CacheableResponse($output, 200);
$response->setContent($output);
$cache_metadata = CacheableMetadata::createFromRenderArray($build);
$response->addCacheableDependency($cache_metadata);
@ -348,4 +429,19 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
return $this->view->render();
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
$dependencies += ['module' => []];
$modules = array_map(function ($authentication_provider) {
return $this->authenticationProviders[$authentication_provider];
}, $this->getOption('auth'));
$dependencies['module'] = array_merge($dependencies['module'], $modules);
return $dependencies;
}
}

View file

@ -47,6 +47,13 @@ class Serializer extends StylePluginBase implements CacheableDependencyInterface
*/
protected $formats = array();
/**
* The serialization format providers, keyed by format.
*
* @var string[]
*/
protected $formatProviders;
/**
* {@inheritdoc}
*/
@ -56,19 +63,21 @@ class Serializer extends StylePluginBase implements CacheableDependencyInterface
$plugin_id,
$plugin_definition,
$container->get('serializer'),
$container->getParameter('serializer.formats')
$container->getParameter('serializer.formats'),
$container->getParameter('serializer.format_providers')
);
}
/**
* Constructs a Plugin object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, array $serializer_formats) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, SerializerInterface $serializer, array $serializer_formats, array $serializer_format_providers) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->definition = $plugin_definition + $configuration;
$this->serializer = $serializer;
$this->formats = $serializer_formats;
$this->formatProviders = $serializer_format_providers;
}
/**
@ -91,7 +100,7 @@ class Serializer extends StylePluginBase implements CacheableDependencyInterface
'#type' => 'checkboxes',
'#title' => $this->t('Accepted request formats'),
'#description' => $this->t('Request formats that will be allowed in responses. If none are selected all formats will be allowed.'),
'#options' => array_combine($this->formats, $this->formats),
'#options' => $this->getFormatOptions(),
'#default_value' => $this->options['formats'],
);
}
@ -167,4 +176,30 @@ class Serializer extends StylePluginBase implements CacheableDependencyInterface
return [];
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
$formats = $this->getFormats();
$providers = array_intersect_key($this->formatProviders, array_flip($formats));
// The plugin always uses services from the serialization module.
$providers[] = 'serialization';
$dependencies += ['module' => []];
$dependencies['module'] = array_merge($dependencies['module'], $providers);
return $dependencies;
}
/**
* Returns an array of format options
*
* @return string[]
* An array of format options. Both key and value are the same.
*/
protected function getFormatOptions() {
$formats = array_keys($this->formatProviders);
return array_combine($formats, $formats);
}
}

View file

@ -2,23 +2,51 @@
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;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\SerializerInterface;
/**
* 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.
*
@ -31,8 +59,6 @@ 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());
// Symfony is built to transparently map HEAD requests to a GET request. In
@ -50,11 +76,13 @@ class RequestHandler implements ContainerAwareInterface {
$method = 'get';
}
$resource = $this->container
->get('plugin.manager.rest')
->getInstance(array('id' => $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 */
$serializer = $this->container->get('serializer');
$received = $request->getContent();
$unserialized = NULL;
@ -65,13 +93,18 @@ class RequestHandler implements ContainerAwareInterface {
// 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 {
$unserialized = $serializer->deserialize($received, $class, $format, array('request_method' => $method));
if (!empty($definition['serialization_class'])) {
$unserialized = $serializer->deserialize($received, $definition['serialization_class'], $format, array('request_method' => $method));
}
// If the plugin does not specify a serialization class just decode
// the received data.
else {
$unserialized = $serializer->decode($received, $format, array('request_method' => $method));
}
}
catch (UnexpectedValueException $e) {
$error['error'] = $e->getMessage();
@ -96,55 +129,119 @@ class RequestHandler implements ContainerAwareInterface {
}
// Invoke the operation on the resource plugin.
// All REST routes are restricted to exactly one format, so instead of
// parsing it out of the Accept headers again, we can simply retrieve the
// format requirement. If there is no format associated, just pick JSON.
$format = $route_match->getRouteObject()->getRequirement('_format') ?: 'json';
try {
$response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request)));
}
catch (HttpException $e) {
$error['error'] = $e->getMessage();
$content = $serializer->serialize($error, $format);
// Add the default content type, but only if the headers from the
// exception have not specified it already.
$headers = $e->getHeaders() + array('Content-Type' => $request->getMimeType($format));
return new Response($content, $e->getStatusCode(), $headers);
}
$format = $this->getResponseFormat($route_match, $request);
$response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request)));
if ($response instanceof ResourceResponse) {
$data = $response->getResponseData();
// Serialization can invoke rendering (e.g., generating URLs), but the
// serialization API does not provide a mechanism to collect the
// bubbleable metadata associated with that (e.g., language and other
// contexts), so instead, allow those to "leak" and collect them here in
// a render context.
// @todo Add test coverage for language negotiation contexts in
// https://www.drupal.org/node/2135829.
$context = new RenderContext();
$output = $this->container->get('renderer')->executeInRenderContext($context, function() use ($serializer, $data, $format) {
return $serializer->serialize($data, $format);
});
$response->setContent($output);
if (!$context->isEmpty()) {
$response->addCacheableDependency($context->pop());
}
$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'));
}
return $response;
return $response instanceof ResourceResponseInterface ?
$this->renderResponse($request, $response, $serializer, $format, $resource_config) :
$response;
}
/**
* Generates a CSRF protecting session token.
* Determines the format to respond in.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
* Respects the requested format if one is specified. However, it is common to
* forget to specify a request format in case of a POST or PATCH. Rather than
* simply throwing an error, we apply the robustness principle: when POSTing
* or PATCHing using a certain format, you probably expect a response in that
* same format.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request.
*
* @return string
* The response format.
*/
public function csrfToken() {
return new Response(\Drupal::csrfToken()->get('rest'), 200, array('Content-Type' => 'text/plain'));
protected function getResponseFormat(RouteMatchInterface $route_match, Request $request) {
$route = $route_match->getRouteObject();
$acceptable_request_formats = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : [];
$acceptable_content_type_formats = $route->hasRequirement('_content_type_format') ? explode('|', $route->getRequirement('_content_type_format')) : [];
$acceptable_formats = $request->isMethodSafe() ? $acceptable_request_formats : $acceptable_content_type_formats;
$requested_format = $request->getRequestFormat();
$content_type_format = $request->getContentType();
// If an acceptable format is requested, then use that. Otherwise, including
// and particularly when the client forgot to specify a format, then use
// heuristics to select the format that is most likely expected.
if (in_array($requested_format, $acceptable_formats)) {
return $requested_format;
}
// If a request body is present, then use the format corresponding to the
// request body's Content-Type for the response, if it's an acceptable
// format for the request.
elseif (!empty($request->getContent()) && in_array($content_type_format, $acceptable_content_type_formats)) {
return $content_type_format;
}
// Otherwise, use the first acceptable format.
elseif (!empty($acceptable_formats)) {
return $acceptable_formats[0];
}
// Sometimes, there are no acceptable formats, e.g. DELETE routes.
else {
return NULL;
}
}
/**
* Renders a resource response.
*
* Serialization can invoke rendering (e.g., generating URLs), but the
* serialization API does not provide a mechanism to collect the
* bubbleable metadata associated with that (e.g., language and other
* contexts), so instead, allow those to "leak" and collect them here in
* a render context.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
* @param \Drupal\rest\ResourceResponseInterface $response
* The response from the REST resource.
* @param \Symfony\Component\Serializer\SerializerInterface $serializer
* The serializer to use.
* @param string|null $format
* The response format, or NULL in case the response does not need a format,
* for example for the response to a DELETE request.
* @param \Drupal\rest\RestResourceConfigInterface $resource_config
* The resource config.
*
* @return \Drupal\rest\ResourceResponse
* The altered response.
*
* @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, RestResourceConfigInterface $resource_config) {
$data = $response->getResponseData();
if ($response instanceof CacheableResponseInterface) {
// Add rest config's cache tags.
$response->addCacheableDependency($resource_config);
}
// If there is data to send, serialize and set it as the response body.
if ($data !== NULL) {
if ($response instanceof CacheableResponseInterface) {
$context = new RenderContext();
$output = $this->container->get('renderer')
->executeInRenderContext($context, function () use ($serializer, $data, $format) {
return $serializer->serialize($data, $format);
});
if (!$context->isEmpty()) {
$response->addCacheableDependency($context->pop());
}
}
else {
$output = $serializer->serialize($data, $format);
}
$response->setContent($output);
$response->headers->set('Content-Type', $request->getMimeType($format));
}
return $response;
}
}

View file

@ -13,17 +13,13 @@ use Symfony\Component\HttpFoundation\Response;
* our response data. $content implies that the provided data must either be a
* string or an object with a __toString() method, which is not a requirement
* for data used here.
*
* @see \Drupal\rest\ModifiedResourceResponse
*/
class ResourceResponse extends Response implements CacheableResponseInterface {
class ResourceResponse extends Response implements CacheableResponseInterface, ResourceResponseInterface {
use CacheableResponseTrait;
/**
* Response data that should be serialized.
*
* @var mixed
*/
protected $responseData;
use ResourceResponseTrait;
/**
* Constructor for ResourceResponse objects.
@ -40,14 +36,4 @@ class ResourceResponse extends Response implements CacheableResponseInterface {
parent::__construct('', $status, $headers);
}
/**
* Returns response data that should be serialized.
*
* @return mixed
* Response data that should be serialized.
*/
public function getResponseData() {
return $this->responseData;
}
}

View file

@ -0,0 +1,18 @@
<?php
namespace Drupal\rest;
/**
* Defines a common interface for resource responses.
*/
interface ResourceResponseInterface {
/**
* Returns response data that should be serialized.
*
* @return mixed
* Response data that should be serialized.
*/
public function getResponseData();
}

View file

@ -0,0 +1,25 @@
<?php
namespace Drupal\rest;
trait ResourceResponseTrait {
/**
* Response data that should be serialized.
*
* @var mixed
*/
protected $responseData;
/**
* Returns response data that should be serialized.
*
* @return mixed
* Response data that should be serialized.
*/
public function getResponseData() {
return $this->responseData;
}
}

View file

@ -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 @@ class RestPermissions implements ContainerInjectionInterface {
*/
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\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;
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\rest;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
/**
* Defines a configuration entity to store enabled REST resources.
*/
interface RestResourceConfigInterface extends ConfigEntityInterface, EntityWithPluginCollectionInterface {
/**
* Granularity value for per-method configuration.
*/
const METHOD_GRANULARITY = 'method';
/**
* Granularity value for per-resource configuration.
*/
const RESOURCE_GRANULARITY = 'resource';
/**
* Retrieves the REST resource plugin.
*
* @return \Drupal\rest\Plugin\ResourceInterface
* The resource plugin
*/
public function getResourcePlugin();
/**
* Retrieves a list of supported HTTP methods.
*
* @return string[]
* A list of supported HTTP methods.
*/
public function getMethods();
/**
* 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 getAuthenticationProviders($method);
/**
* 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.
*/
public function getFormats($method);
}

View file

@ -2,9 +2,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\RestResourceConfigInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Routing\RouteCollection;
@ -21,11 +22,11 @@ class ResourceRoutes extends RouteSubscriberBase {
protected $manager;
/**
* The Drupal configuration factory.
* The REST resource config storage.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $config;
protected $resourceConfigStorage;
/**
* A logger instance.
@ -39,14 +40,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->resourceConfigStorage = $entity_type_manager->getStorage('rest_resource_config');
$this->logger = $logger;
}
@ -58,57 +59,68 @@ class ResourceRoutes extends RouteSubscriberBase {
* @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 ($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);
}
}
foreach ($resource_configs as $resource_config) {
$resource_routes = $this->getRoutesForResourceConfig($resource_config);
$collection->addCollection($resource_routes);
}
}
/**
* 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('_csrf_request_header_token', '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;
}
}

View file

@ -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.
@ -43,7 +43,6 @@ class AuthTest extends RESTTestBase {
// resources via the REST API, but the request is authenticated
// with session cookies.
$permissions = $this->entityPermissions($entity_type, 'view');
$permissions[] = 'restful get entity:' . $entity_type;
$account = $this->drupalCreateUser($permissions);
$this->drupalLogin($account);

View file

@ -5,6 +5,7 @@ namespace Drupal\rest\Tests;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\node\Entity\Node;
use Drupal\user\Entity\User;
@ -23,7 +24,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.
@ -49,8 +50,6 @@ class CreateTest extends RESTTestBase {
// Get the necessary user permissions to create the current entity type.
$permissions = $this->entityPermissions($entity_type, 'create');
// POST method must be allowed for the current entity type.
$permissions[] = 'restful post entity:' . $entity_type;
// Create the user.
$account = $this->drupalCreateUser($permissions);
@ -77,7 +76,11 @@ class CreateTest extends RESTTestBase {
/**
* Ensure that an entity cannot be created without the restful permission.
*/
public function testCreateWithoutPermission() {
public function testCreateWithoutPermissionIfBcFlagIsOn() {
$rest_settings = $this->config('rest.settings');
$rest_settings->set('bc_entity_resource_permissions', TRUE)
->save(TRUE);
$entity_type = 'entity_test';
// Enables the REST service for 'entity_test' entity type.
$this->enableService('entity:' . $entity_type, 'POST');
@ -96,6 +99,14 @@ class CreateTest extends RESTTestBase {
$this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
$this->assertResponse(403);
$this->assertFalse(EntityTest::loadMultiple(), 'No entity has been created in the database.');
// Create a user with the 'restful post entity:entity_test permission and
// try again. This time, we should be able to create an entity.
$permissions[] = 'restful post entity:' . $entity_type;
$account = $this->drupalCreateUser($permissions);
$this->drupalLogin($account);
$this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
$this->assertResponse(201);
}
/**
@ -331,8 +342,6 @@ class CreateTest extends RESTTestBase {
$accounts = array();
// Get the necessary user permissions for the current $entity_type creation.
$permissions = $this->entityPermissions($entity_type, 'create');
// POST method must be allowed for the current entity type.
$permissions[] = 'restful post entity:' . $entity_type;
// Create user without administrative permissions.
$accounts[] = $this->drupalCreateUser($permissions);
// Add administrative permissions for nodes and users.
@ -356,6 +365,10 @@ class CreateTest extends RESTTestBase {
public function assertCreateEntityOverRestApi($entity_type, $serialized = NULL) {
// Note: this will fail with PHP 5.6 when always_populate_raw_post_data is
// set to something other than -1. See https://www.drupal.org/node/2456025.
// Try first without the CSRF token, which should fail.
$this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType, TRUE);
$this->assertResponse(403, 'X-CSRF-Token request header is missing');
// Then try with the CSRF token.
$response = $this->httpRequest('entity/' . $entity_type, 'POST', $serialized, $this->defaultMimeType);
$this->assertResponse(201);
@ -436,14 +449,14 @@ class CreateTest extends RESTTestBase {
$entity->set('uuid', $this->randomMachineName(129));
$invalid_serialized = $this->serializer->serialize($entity, $this->defaultFormat, $context);
$response = $this->httpRequest('entity/' . $entity_type, 'POST', $invalid_serialized, $this->defaultMimeType);
$response = $this->httpRequest(Url::fromRoute("rest.entity.$entity_type.POST")->setRouteParameter('_format', $this->defaultFormat), 'POST', $invalid_serialized, $this->defaultMimeType);
// Unprocessable Entity as response.
$this->assertResponse(422);
// Verify that the text of the response is correct.
$error = Json::decode($response);
$this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
$this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
}
/**

View file

@ -43,7 +43,6 @@ class CsrfTest extends RESTTestBase {
// Create a user account that has the required permissions to create
// resources via the REST API.
$permissions = $this->entityPermissions($this->testEntityType, 'create');
$permissions[] = 'restful post entity:' . $this->testEntityType;
$this->account = $this->drupalCreateUser($permissions);
// Serialize an entity to a string to use in the content body of the POST
@ -72,6 +71,10 @@ class CsrfTest extends RESTTestBase {
/**
* Tests that CSRF check is triggered for Cookie Auth requests.
*
* @deprecated as of Drupal 8.2.x, will be removed before Drupal 9.0.0. Use
* \Drupal\Tests\system\Functional\CsrfRequestHeaderTest::testRouteAccess
* instead.
*/
public function testCookieAuth() {
$this->drupalLogin($this->account);
@ -84,7 +87,10 @@ class CsrfTest extends RESTTestBase {
$this->curlExec($curl_options);
$this->assertResponse(403);
// Ensure that the entity was not created.
$this->assertFalse(entity_load_multiple($this->testEntityType, NULL, TRUE), 'No entity has been created in the database.');
$storage = $this->container->get('entity_type.manager')
->getStorage($this->testEntityType);
$storage->resetCache();
$this->assertFalse($storage->loadMultiple(), 'No entity has been created in the database.');
// Create an entity with the CSRF token.
$token = $this->drupalGet('rest/session/token');

View file

@ -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.
@ -31,18 +31,23 @@ class DeleteTest extends RESTTestBase {
// Create a user account that has the required permissions to delete
// resources via the REST API.
$permissions = $this->entityPermissions($entity_type, 'delete');
$permissions[] = 'restful delete entity:' . $entity_type;
$account = $this->drupalCreateUser($permissions);
$this->drupalLogin($account);
// Create an entity programmatically.
$entity = $this->entityCreate($entity_type);
$entity->save();
// Try first to delete over REST API without the CSRF token.
$this->httpRequest($entity->urlInfo(), 'DELETE', NULL, NULL, TRUE);
$this->assertResponse(403, 'X-CSRF-Token request header is missing');
// Delete it over the REST API.
$response = $this->httpRequest($entity->urlInfo(), 'DELETE');
// Clear the static cache with entity_load(), otherwise we won't see the
// update.
$entity = entity_load($entity_type, $entity->id(), TRUE);
$storage = $this->container->get('entity_type.manager')
->getStorage($entity_type);
$storage->resetCache([$entity->id()]);
$entity = $storage->load($entity->id());
$this->assertFalse($entity, $entity_type . ' entity is not in the DB anymore.');
$this->assertResponse('204', 'HTTP response code is correct.');
$this->assertEqual($response, '', 'Response body is empty.');
@ -59,7 +64,9 @@ class DeleteTest extends RESTTestBase {
$entity->save();
$this->httpRequest($entity->urlInfo(), 'DELETE');
$this->assertResponse(403);
$this->assertNotIdentical(FALSE, entity_load($entity_type, $entity->id(), TRUE), 'The ' . $entity_type . ' entity is still in the database.');
$storage->resetCache([$entity->id()]);
$this->assertNotIdentical(FALSE, $storage->load($entity->id()),
'The ' . $entity_type . ' entity is still in the database.');
}
// Try to delete a resource which is not REST API enabled.
$this->enableService(FALSE);

View file

@ -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.
@ -32,7 +32,6 @@ class NodeTest extends RESTTestBase {
protected function enableNodeConfiguration($method, $operation) {
$this->enableService('entity:node', $method);
$permissions = $this->entityPermissions('node', $operation);
$permissions[] = 'restful ' . strtolower($method) . ' entity:node';
$account = $this->drupalCreateUser($permissions);
$this->drupalLogin($account);
}
@ -110,7 +109,7 @@ class NodeTest extends RESTTestBase {
);
$serialized = $this->container->get('serializer')->serialize($data, $this->defaultFormat);
$this->httpRequest($node->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(204);
$this->assertResponse(200);
// Reload the node from the DB and check if the title was correctly updated.
$node_storage->resetCache(array($node->id()));

View file

@ -4,6 +4,7 @@ namespace Drupal\rest\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Url;
use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
/**
* Tests page caching for REST GET requests.
@ -12,6 +13,8 @@ use Drupal\Core\Url;
*/
class PageCacheTest extends RESTTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* Modules to install.
*
@ -19,45 +22,114 @@ class PageCacheTest extends RESTTestBase {
*/
public static $modules = array('hal');
/**
* The 'serializer' service.
*
* @var \Symfony\Component\Serializer\Serializer
*/
protected $serializer;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Get the 'serializer' service.
$this->serializer = $this->container->get('serializer');
}
/**
* Tests that configuration changes also clear the page cache.
*/
public function testConfigChangePageCache() {
$this->enableService('entity:entity_test', 'GET');
// Allow anonymous users to issue GET requests.
$permissions = $this->entityPermissions('entity_test', 'view');
$permissions[] = 'restful get entity:entity_test';
user_role_grant_permissions('anonymous', $permissions);
user_role_grant_permissions('anonymous', ['view test entity', 'restful get entity:entity_test']);
// Create an entity programmatically.
$this->enableService('entity:entity_test', 'POST');
$permissions = [
'administer entity_test content',
];
$account = $this->drupalCreateUser($permissions);
// Create an entity and test that the response from a post request is not
// cacheable.
$entity = $this->entityCreate('entity_test');
$entity->set('field_test_text', 'custom cache tag value');
$entity->save();
$serialized = $this->serializer->serialize($entity, $this->defaultFormat);
// Log in for creating the entity.
$this->drupalLogin($account);
$this->httpRequest('entity/entity_test', 'POST', $serialized, $this->defaultMimeType);
$this->assertResponse(201);
if ($this->getCacheHeaderValues('x-drupal-cache')) {
$this->fail('Post request is cached.');
}
$this->drupalLogout();
$url = Url::fromUri('internal:/entity_test/1?_format=' . $this->defaultFormat);
// Read it over the REST API.
$this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET', NULL, $this->defaultMimeType);
$this->enableService('entity:entity_test', 'GET');
$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');
// Read it again, should be page-cached now.
$this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET', NULL, $this->defaultMimeType);
$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();
$this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET', NULL, $this->defaultMimeType);
// 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');
// Log in for deleting / updating entity.
$this->drupalLogin($account);
// Test that updating an entity is not cacheable.
$this->enableService('entity:entity_test', 'PATCH');
// Create a second stub entity for overwriting a field.
$patch_values['field_test_text'] = [0 => [
'value' => 'patched value',
'format' => 'plain_text',
]];
$patch_entity = $this->container->get('entity_type.manager')
->getStorage('entity_test')
->create($patch_values);
// We don't want to overwrite the UUID.
$patch_entity->set('uuid', NULL);
$serialized = $this->container->get('serializer')
->serialize($patch_entity, $this->defaultFormat);
// Update the entity over the REST API.
$this->httpRequest($url, 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(200);
if ($this->getCacheHeaderValues('x-drupal-cache')) {
$this->fail('Patch request is cached.');
}
// Test that the response from a delete request is not cacheable.
$this->enableService('entity:entity_test', 'DELETE');
$this->httpRequest($url, 'DELETE');
$this->assertResponse(204);
if ($this->getCacheHeaderValues('x-drupal-cache')) {
$this->fail('Patch request is cached.');
}
}
/**
@ -81,7 +153,7 @@ class PageCacheTest extends RESTTestBase {
$response = $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');
$data = Json::decode($response);
$this->assertEqual($data['type'][0]['value'], 'entity_test');

View file

@ -2,7 +2,9 @@
namespace Drupal\rest\Tests;
use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\node\NodeInterface;
use Drupal\rest\RestResourceConfigInterface;
use Drupal\simpletest\WebTestBase;
/**
@ -10,6 +12,13 @@ use Drupal\simpletest\WebTestBase;
*/
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.
*
@ -51,15 +60,18 @@ abstract class RESTTestBase extends WebTestBase {
*
* @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'));
}
}
/**
@ -73,17 +85,19 @@ abstract class RESTTestBase extends WebTestBase {
* The body for POST and PUT.
* @param string $mime_type
* The MIME type of the transmitted content.
* @param bool $forget_xcsrf_token
* If TRUE, the CSRF token won't be included in request.
*
* @return string
* The content returned from the request.
*/
protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL) {
protected function httpRequest($url, $method, $body = NULL, $mime_type = NULL, $forget_xcsrf_token = FALSE) {
if (!isset($mime_type)) {
$mime_type = $this->defaultMimeType;
}
if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) {
// GET the CSRF token first for writing requests.
$token = $this->drupalGet('rest/session/token');
$token = $this->drupalGet('session/token');
}
$url = $this->buildUrl($url);
@ -101,15 +115,15 @@ abstract class RESTTestBase extends WebTestBase {
);
break;
case 'HEAD':
$curl_options = array(
CURLOPT_HTTPGET => FALSE,
CURLOPT_CUSTOMREQUEST => 'HEAD',
CURLOPT_URL => $url,
CURLOPT_NOBODY => TRUE,
CURLOPT_HTTPHEADER => array('Accept: ' . $mime_type),
);
break;
case 'HEAD':
$curl_options = array(
CURLOPT_HTTPGET => FALSE,
CURLOPT_CUSTOMREQUEST => 'HEAD',
CURLOPT_URL => $url,
CURLOPT_NOBODY => TRUE,
CURLOPT_HTTPHEADER => array('Accept: ' . $mime_type),
);
break;
case 'POST':
$curl_options = array(
@ -118,9 +132,11 @@ abstract class RESTTestBase extends WebTestBase {
CURLOPT_POSTFIELDS => $body,
CURLOPT_URL => $url,
CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => array(
CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array(
'Content-Type: ' . $mime_type,
'X-CSRF-Token: ' . $token,
) : array(
'Content-Type: ' . $mime_type,
),
);
break;
@ -132,9 +148,11 @@ abstract class RESTTestBase extends WebTestBase {
CURLOPT_POSTFIELDS => $body,
CURLOPT_URL => $url,
CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => array(
CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array(
'Content-Type: ' . $mime_type,
'X-CSRF-Token: ' . $token,
) : array(
'Content-Type: ' . $mime_type,
),
);
break;
@ -146,9 +164,11 @@ abstract class RESTTestBase extends WebTestBase {
CURLOPT_POSTFIELDS => $body,
CURLOPT_URL => $url,
CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => array(
CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array(
'Content-Type: ' . $mime_type,
'X-CSRF-Token: ' . $token,
) : array(
'Content-Type: ' . $mime_type,
),
);
break;
@ -159,11 +179,15 @@ abstract class RESTTestBase extends WebTestBase {
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_URL => $url,
CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => array('X-CSRF-Token: ' . $token),
CURLOPT_HTTPHEADER => !$forget_xcsrf_token ? array('X-CSRF-Token: ' . $token) : array(),
);
break;
}
if ($mime_type === 'none') {
unset($curl_options[CURLOPT_HTTPHEADER]['Content-Type']);
}
$this->responseBody = $this->curlExec($curl_options);
// Ensure that any changes to variables in the other thread are picked up.
@ -200,14 +224,14 @@ abstract class RESTTestBase extends WebTestBase {
* Required properties differ from entity type to entity type, so we keep a
* minimum mapping here.
*
* @param string $entity_type
* The type of the entity that should be created.
* @param string $entity_type_id
* The ID of the type of entity that should be created.
*
* @return array
* An array of values keyed by property name.
*/
protected function entityValues($entity_type) {
switch ($entity_type) {
protected function entityValues($entity_type_id) {
switch ($entity_type_id) {
case 'entity_test':
return array(
'name' => $this->randomMachineName(),
@ -217,6 +241,11 @@ abstract class RESTTestBase extends WebTestBase {
'format' => 'plain_text',
)),
);
case 'config_test':
return [
'id' => $this->randomMachineName(),
'label' => 'Test label',
];
case 'node':
return array('title' => $this->randomString(), 'type' => 'resttest');
case 'node_type':
@ -236,8 +265,15 @@ abstract class RESTTestBase extends WebTestBase {
'entity_id' => 'invalid',
'field_name' => 'comment',
];
case 'taxonomy_vocabulary':
return [
'vid' => 'tags',
'name' => $this->randomMachineName(),
];
default:
if ($this->isConfigEntity($entity_type_id)) {
return $this->configEntityValues($entity_type_id);
}
return array();
}
}
@ -245,7 +281,7 @@ abstract class RESTTestBase extends WebTestBase {
/**
* Enables the REST service interface for a specific entity type.
*
* @param string|FALSE $resource_type
* @param string|false $resource_type
* The resource type that should get REST API enabled or FALSE to disable all
* resource types.
* @param string $method
@ -255,29 +291,49 @@ abstract class RESTTestBase extends WebTestBase {
* @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();
}
@ -311,7 +367,7 @@ abstract class RESTTestBase extends WebTestBase {
/**
* Provides the necessary user permissions for entity operations.
*
* @param string $entity_type
* @param string $entity_type_id
* The entity type.
* @param string $operation
* The operation, one of 'view', 'create', 'update' or 'delete'.
@ -319,8 +375,8 @@ abstract class RESTTestBase extends WebTestBase {
* @return array
* The set of user permission strings.
*/
protected function entityPermissions($entity_type, $operation) {
switch ($entity_type) {
protected function entityPermissions($entity_type_id, $operation) {
switch ($entity_type_id) {
case 'entity_test':
switch ($operation) {
case 'view':
@ -365,9 +421,17 @@ abstract class RESTTestBase extends WebTestBase {
default:
return ['administer users'];
}
default:
if ($this->isConfigEntity($entity_type_id)) {
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
if ($admin_permission = $entity_type->getAdminPermission()) {
return [$admin_permission];
}
}
}
return [];
}
/**
@ -376,13 +440,14 @@ abstract class RESTTestBase extends WebTestBase {
* @param string $location_url
* The URL returned in the Location header.
*
* @return \Drupal\Core\Entity\Entity|FALSE.
* @return \Drupal\Core\Entity\Entity|false
* The entity or FALSE if there is no matching entity.
*/
protected function loadEntityFromLocationHeader($location_url) {
$url_parts = explode('/', $location_url);
$id = end($url_parts);
return entity_load($this->testEntityType, $id);
return $this->container->get('entity_type.manager')
->getStorage($this->testEntityType)->load($id);
}
/**
@ -431,4 +496,49 @@ abstract class RESTTestBase extends WebTestBase {
return $this->assertIdentical($expected, $this->responseBody, $message ? $message : strtr('Response body @expected (expected) is equal to @response (actual).', array('@expected' => var_export($expected, TRUE), '@response' => var_export($this->responseBody, TRUE))), $group);
}
/**
* Checks if an entity type id is for a Config Entity.
*
* @param string $entity_type_id
* The entity type ID to check.
*
* @return bool
* TRUE if the entity is a Config Entity, FALSE otherwise.
*/
protected function isConfigEntity($entity_type_id) {
return \Drupal::entityTypeManager()->getDefinition($entity_type_id) instanceof ConfigEntityType;
}
/**
* Provides an array of suitable property values for a config entity type.
*
* Config entities have some common keys that need to be created. Required
* properties differ among config entity types, so we keep a minimum mapping
* here.
*
* @param string $entity_type_id
* The ID of the type of entity that should be created.
*
* @return array
* An array of values keyed by property name.
*/
protected function configEntityValues($entity_type_id) {
$entity_type = \Drupal::entityTypeManager()->getDefinition($entity_type_id);
$keys = $entity_type->getKeys();
$values = [];
// Fill out known key values that are shared across entity types.
foreach ($keys as $key) {
if ($key === 'id' || $key === 'label') {
$values[$key] = $this->randomMachineName();
}
}
// Add extra values for particular entity types.
switch ($entity_type_id) {
case 'block':
$values['plugin'] = 'system_powered_by_block';
break;
}
return $values;
}
}

View file

@ -3,6 +3,8 @@
namespace Drupal\rest\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
/**
@ -17,7 +19,15 @@ class ReadTest extends RESTTestBase {
*
* @var array
*/
public static $modules = array('hal', 'rest', 'entity_test');
public static $modules = [
'hal',
'rest',
'node',
'entity_test',
'config_test',
'taxonomy',
'block',
];
/**
* Tests several valid and invalid read requests on all entity types.
@ -25,13 +35,19 @@ class ReadTest extends RESTTestBase {
public function testRead() {
// @todo Expand this at least to users.
// Define the entity types we want to test.
$entity_types = array('entity_test', 'node');
$entity_types = [
'entity_test',
'node',
'config_test',
'taxonomy_vocabulary',
'block',
'user_role',
];
foreach ($entity_types as $entity_type) {
$this->enableService('entity:' . $entity_type, 'GET');
// Create a user account that has the required permissions to read
// resources via the REST API.
$permissions = $this->entityPermissions($entity_type, 'view');
$permissions[] = 'restful get entity:' . $entity_type;
$account = $this->drupalCreateUser($permissions);
$this->drupalLogin($account);
@ -40,12 +56,12 @@ class ReadTest extends RESTTestBase {
$entity->save();
// Verify that it exists: use a HEAD request.
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'HEAD');
$this->httpRequest($this->getReadUrl($entity), 'HEAD');
$this->assertResponseBody('');
$head_headers = $this->drupalGetHeaders();
// Read it over the REST API.
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
$response = $this->httpRequest($this->getReadUrl($entity), 'GET');
$get_headers = $this->drupalGetHeaders();
$this->assertResponse('200', 'HTTP response code is correct.');
@ -64,17 +80,33 @@ class ReadTest extends RESTTestBase {
$data = Json::decode($response);
// Only assert one example property here, other properties should be
// checked in serialization tests.
$this->assertEqual($data['uuid'][0]['value'], $entity->uuid(), 'Entity UUID is correct');
if ($entity instanceof ConfigEntityInterface) {
$this->assertEqual($data['uuid'], $entity->uuid(), 'Entity UUID is correct');
}
else {
$this->assertEqual($data['uuid'][0]['value'], $entity->uuid(), 'Entity UUID is correct');
}
// Try to read the entity with an unsupported mime format.
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', 'wrongformat'), 'GET');
$this->httpRequest($this->getReadUrl($entity, 'wrongformat'), 'GET');
$this->assertResponse(406);
$this->assertHeader('Content-type', 'application/json');
// Try to read an entity that does not exist.
$response = $this->httpRequest(Url::fromUri('base://' . $entity_type . '/9999', ['query' => ['_format' => $this->defaultFormat]]), 'GET');
$response = $this->httpRequest($this->getReadUrl($entity, $this->defaultFormat, 9999), 'GET');
$this->assertResponse(404);
$path = $entity_type == 'node' ? '/node/{node}' : '/entity_test/{entity_test}';
switch ($entity_type) {
case 'node':
$path = '/node/{node}';
break;
case 'entity_test':
$path = '/entity_test/{entity_test}';
break;
default:
$path = "/entity/$entity_type/{" . $entity_type . '}';
}
$expected_message = Json::encode(['message' => 'The "' . $entity_type . '" parameter was not converted for the path "' . $path . '" (route name: "rest.entity.' . $entity_type . '.GET.hal_json")']);
$this->assertIdentical($expected_message, $response, 'Response message is correct.');
@ -84,23 +116,18 @@ class ReadTest extends RESTTestBase {
if ($entity_type == 'entity_test') {
$entity->field_test_text->value = 'no access value';
$entity->save();
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
$response = $this->httpRequest($this->getReadUrl($entity), 'GET');
$this->assertResponse(200);
$this->assertHeader('content-type', $this->defaultMimeType);
$data = Json::decode($response);
$this->assertFalse(isset($data['field_test_text']), 'Field access protected field is not visible in the response.');
}
// Try to read an entity without proper permissions.
$this->drupalLogout();
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
$this->assertResponse(403);
$this->assertIdentical('{"message":""}', $response);
}
// Try to read a resource which is not REST API enabled.
// Try to read a resource, the user entity, which is not REST API enabled.
$account = $this->drupalCreateUser();
$this->drupalLogin($account);
$response = $this->httpRequest($account->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
$response = $this->httpRequest($this->getReadUrl($account), 'GET');
// \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical,
// non-REST route a match, but a lower quality one: no format restrictions
// means there's always a match and hence when there is no matching REST
@ -121,7 +148,6 @@ class ReadTest extends RESTTestBase {
// Create a user account that has the required permissions to read
// resources via the REST API.
$permissions = $this->entityPermissions('node', 'view');
$permissions[] = 'restful get entity:node';
$account = $this->drupalCreateUser($permissions);
$this->drupalLogin($account);
@ -130,8 +156,50 @@ class ReadTest extends RESTTestBase {
$entity->save();
// Read it over the REST API.
$response = $this->httpRequest($entity->urlInfo()->setRouteParameter('_format', 'json'), 'GET');
$this->httpRequest($this->getReadUrl($entity, 'json'), 'GET');
$this->assertResponse('200', 'HTTP response code is correct.');
}
/**
* Gets the read URL object for the entity.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to get the URL for.
* @param string $format
* The format to request the entity in.
* @param string $entity_id
* The entity ID to use in the URL, defaults to the entity's ID if know
* given.
*
* @return \Drupal\Core\Url
* The Url object.
*/
protected function getReadUrl(EntityInterface $entity, $format = NULL, $entity_id = NULL) {
if (!$format) {
$format = $this->defaultFormat;
}
if (!$entity_id) {
$entity_id = $entity->id();
}
$entity_type = $entity->getEntityTypeId();
if ($entity->hasLinkTemplate('canonical')) {
$url = $entity->toUrl('canonical');
}
else {
$route_name = 'rest.entity.' . $entity_type . ".GET.";
// If testing unsupported format don't use the format to construct route
// name. This would give a RouteNotFoundException.
if ($format == 'wrongformat') {
$route_name .= $this->defaultFormat;
}
else {
$route_name .= $format;
}
$url = Url::fromRoute($route_name);
}
$url->setRouteParameter($entity_type, $entity_id);
$url->setRouteParameter('_format', $format);
return $url;
}
}

View file

@ -2,9 +2,10 @@
namespace Drupal\rest\Tests;
use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Core\Session\AccountInterface;
use Drupal\rest\RestResourceConfigInterface;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests the structure of a REST resource.
@ -18,7 +19,7 @@ class ResourceTest extends RESTTestBase {
*
* @var array
*/
public static $modules = array('hal', 'rest', 'entity_test');
public static $modules = array('hal', 'rest', 'entity_test', 'rest_test');
/**
* The entity.
@ -32,9 +33,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 +46,17 @@ class ResourceTest extends RESTTestBase {
* 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 +73,17 @@ class ResourceTest extends RESTTestBase {
* 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');
@ -103,6 +96,22 @@ class ResourceTest extends RESTTestBase {
$this->curlClose();
}
/**
* Tests that serialization_class is optional.
*/
public function testSerializationClassIsOptional() {
$this->enableService('serialization_test', 'POST', 'json');
Role::load(RoleInterface::ANONYMOUS_ID)
->grantPermission('restful post serialization_test')
->save();
$serialized = $this->container->get('serializer')->serialize(['foo', 'bar'], 'json');
$this->httpRequest('serialization_test', 'POST', $serialized, 'application/json');
$this->assertResponse(200);
$this->assertResponseBody('["foo","bar"]');
}
/**
* Tests that resource URI paths are formatted properly.
*/
@ -118,30 +127,4 @@ class ResourceTest extends RESTTestBase {
}
}
/**
* 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.');
}
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace Drupal\rest\Tests\Update;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Tests that existing sites continue to use permissions for EntityResource.
*
* @see https://www.drupal.org/node/2664780
*
* @group rest
*/
class EntityResourcePermissionsUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['rest', 'serialization'];
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../../rest/tests/fixtures/update/drupal-8.rest-rest_update_8203.php',
];
}
/**
* Tests rest_update_8203().
*/
public function testBcEntityResourcePermissionSettingAdded() {
$permission_handler = $this->container->get('user.permissions');
$is_rest_resource_permission = function ($permission) {
return $permission['provider'] === 'rest' && (string) $permission['title'] !== 'Administer REST resource configuration';
};
// Make sure we have the expected values before the update.
$rest_settings = $this->config('rest.settings');
$this->assertFalse(array_key_exists('bc_entity_resource_permissions', $rest_settings->getRawData()));
$this->assertEqual([], array_filter($permission_handler->getPermissions(), $is_rest_resource_permission));
$this->runUpdates();
// Make sure we have the expected values after the update.
$rest_settings = $this->config('rest.settings');
$this->assertTrue(array_key_exists('bc_entity_resource_permissions', $rest_settings->getRawData()));
$this->assertTrue($rest_settings->get('bc_entity_resource_permissions'));
$rest_permissions = array_keys(array_filter($permission_handler->getPermissions(), $is_rest_resource_permission));
$this->assertEqual(['restful delete entity:node', 'restful get entity:node', 'restful patch entity:node', 'restful post entity:node'], $rest_permissions);
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace Drupal\rest\Tests\Update;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Tests method-granularity REST config is simplified to resource-granularity.
*
* @see https://www.drupal.org/node/2721595
* @see rest_post_update_resource_granularity()
*
* @group rest
*/
class ResourceGranularityUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['rest', 'serialization'];
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../../rest/tests/fixtures/update/drupal-8.rest-rest_post_update_resource_granularity.php',
];
}
/**
* Tests rest_post_update_simplify_resource_granularity().
*/
public function testMethodGranularityConvertedToResourceGranularity() {
/** @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.
$resource_config_entities = $resource_config_storage->loadMultiple();
$this->assertIdentical(['entity.comment', 'entity.node', 'entity.user'], array_keys($resource_config_entities));
$this->assertIdentical('method', $resource_config_entities['entity.node']->get('granularity'));
$this->assertIdentical('method', $resource_config_entities['entity.comment']->get('granularity'));
$this->assertIdentical('method', $resource_config_entities['entity.user']->get('granularity'));
// Read the existing 'entity:comment' and 'entity:user' resource
// configuration so we can verify it after the update.
$comment_resource_configuration = $resource_config_entities['entity.comment']->get('configuration');
$user_resource_configuration = $resource_config_entities['entity.user']->get('configuration');
$this->runUpdates();
// Make sure we have the expected values after the update.
$resource_config_entities = $resource_config_storage->loadMultiple();
$this->assertIdentical(['entity.comment', 'entity.node', 'entity.user'], array_keys($resource_config_entities));
// 'entity:node' should be updated.
$this->assertIdentical('resource', $resource_config_entities['entity.node']->get('granularity'));
$this->assertidentical($resource_config_entities['entity.node']->get('configuration'), [
'methods' => ['GET', 'POST', 'PATCH', 'DELETE'],
'formats' => ['hal_json'],
'authentication' => ['basic_auth'],
]);
// 'entity:comment' should be unchanged.
$this->assertIdentical('method', $resource_config_entities['entity.comment']->get('granularity'));
$this->assertIdentical($comment_resource_configuration, $resource_config_entities['entity.comment']->get('configuration'));
// 'entity:user' should be unchanged.
$this->assertIdentical('method', $resource_config_entities['entity.user']->get('granularity'));
$this->assertIdentical($user_resource_configuration, $resource_config_entities['entity.user']->get('configuration'));
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace Drupal\rest\Tests\Update;
use Drupal\rest\RestResourceConfigInterface;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Tests that rest.settings is converted to rest_resource_config entities.
*
* @see https://www.drupal.org/node/2308745
* @see rest_update_8201()
* @see rest_post_update_create_rest_resource_config_entities()
*
* @group rest
*/
class RestConfigurationEntitiesUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['rest', 'serialization'];
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->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));
$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::RESOURCE_GRANULARITY, $node_resource_config_entity->get('granularity'));
$this->assertIdentical([
'methods' => ['GET'],
'formats' => ['json'],
'authentication' => ['basic_auth'],
], $node_resource_config_entity->get('configuration'));
$this->assertIdentical(['module' => ['basic_auth', 'node', 'serialization']], $node_resource_config_entity->getDependencies());
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace Drupal\rest\Tests\Update;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Ensures that update hook is run properly for REST Export config.
*
* @group Update
*/
class RestExportAuthUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../tests/fixtures/update/rest-export-with-authentication.php',
];
}
/**
* Ensures that update hook is run for rest module.
*/
public function testUpdate() {
$this->runUpdates();
// Get particular view.
$view = \Drupal::entityTypeManager()->getStorage('view')->load('rest_export_with_authorization');
$displays = $view->get('display');
$this->assertIdentical($displays['rest_export_1']['display_options']['auth']['basic_auth'], 'basic_auth', 'Basic authentication is set as authentication method.');
}
}

View file

@ -8,6 +8,7 @@ use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\entity_test\Entity\EntityTest;
use Symfony\Component\HttpFoundation\Response;
/**
* Tests the update of resources.
@ -23,7 +24,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}
@ -45,7 +46,6 @@ class UpdateTest extends RESTTestBase {
// Create a user account that has the required permissions to create
// resources via the REST API.
$permissions = $this->entityPermissions($entity_type, 'update');
$permissions[] = 'restful patch entity:' . $entity_type;
$account = $this->drupalCreateUser($permissions);
$this->drupalLogin($account);
@ -67,12 +67,33 @@ class UpdateTest extends RESTTestBase {
$patch_entity->set('uuid', NULL);
$serialized = $serializer->serialize($patch_entity, $this->defaultFormat, $context);
// Update the entity over the REST API but forget to specify a Content-Type
// header, this should throw the proper exception.
$this->httpRequest($entity->toUrl(), 'PATCH', $serialized, 'none');
$this->assertResponse(Response::HTTP_UNSUPPORTED_MEDIA_TYPE);
$this->assertRaw('No route found that matches &quot;Content-Type: none&quot;');
// Update the entity over the REST API.
$this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(204);
$response = $this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(200);
// Make sure that the response includes an entity in the body, check the
// updated field as an example.
$request = Json::decode($serialized);
$response = Json::decode($response);
$this->assertEqual($request['field_test_text'][0]['value'], $response['field_test_text'][0]['value']);
unset($request['_links']);
unset($response['_links']);
unset($response['id']);
unset($response['uuid']);
unset($response['name']);
$this->assertEqual($request, $response);
// Re-load updated entity from the database.
$entity = entity_load($entity_type, $entity->id(), TRUE);
$storage = $this->container->get('entity_type.manager')
->getStorage($entity_type);
$storage->resetCache([$entity->id()]);
$entity = $storage->load($entity->id());
$this->assertEqual($entity->field_test_text->value, $patch_entity->field_test_text->value, 'Field was successfully updated.');
// Make sure that the field does not get deleted if it is not present in the
@ -81,9 +102,10 @@ class UpdateTest extends RESTTestBase {
unset($normalized['field_test_text']);
$serialized = $serializer->encode($normalized, $this->defaultFormat);
$this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(204);
$this->assertResponse(200);
$entity = entity_load($entity_type, $entity->id(), TRUE);
$storage->resetCache([$entity->id()]);
$entity = $storage->load($entity->id());
$this->assertNotNull($entity->field_test_text->value . 'Test field has not been deleted.');
// Try to empty a field.
@ -92,10 +114,11 @@ class UpdateTest extends RESTTestBase {
// Update the entity over the REST API.
$this->httpRequest($entity->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(204);
$this->assertResponse(200);
// Re-load updated entity from the database.
$entity = entity_load($entity_type, $entity->id(), TRUE);
$storage->resetCache([$entity->id()]);
$entity = $storage->load($entity->id(), TRUE);
$this->assertNull($entity->field_test_text->value, 'Test field has been cleared.');
// Enable access protection for the text field.
@ -109,7 +132,8 @@ class UpdateTest extends RESTTestBase {
$this->assertResponse(403);
// Re-load the entity from the database.
$entity = entity_load($entity_type, $entity->id(), TRUE);
$storage->resetCache([$entity->id()]);
$entity = $storage->load($entity->id());
$this->assertEqual($entity->field_test_text->value, 'no edit access value', 'Text field was not deleted.');
// Try to update an access protected field.
@ -120,7 +144,8 @@ class UpdateTest extends RESTTestBase {
$this->assertResponse(403);
// Re-load the entity from the database.
$entity = entity_load($entity_type, $entity->id(), TRUE);
$storage->resetCache([$entity->id()]);
$entity = $storage->load($entity->id());
$this->assertEqual($entity->field_test_text->value, 'no edit access value', 'Text field was not updated.');
// Try to update the field with a text format this user has no access to.
@ -136,7 +161,8 @@ class UpdateTest extends RESTTestBase {
$this->assertResponse(422);
// Re-load the entity from the database.
$entity = entity_load($entity_type, $entity->id(), TRUE);
$storage->resetCache([$entity->id()]);
$entity = $storage->load($entity->id());
$this->assertEqual($entity->field_test_text->format, 'plain_text', 'Text format was not updated.');
// Restore the valid test value.
@ -150,17 +176,18 @@ class UpdateTest extends RESTTestBase {
// Try to update a non-existing entity with ID 9999.
$this->httpRequest($entity_type . '/9999', 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(404);
$loaded_entity = entity_load($entity_type, 9999, TRUE);
$storage->resetCache([9999]);
$loaded_entity = $storage->load(9999);
$this->assertFalse($loaded_entity, 'Entity 9999 was not created.');
// Try to send invalid data to trigger the entity validation constraints.
// Send a UUID that is too long.
$entity->set('uuid', $this->randomMachineName(129));
$invalid_serialized = $serializer->serialize($entity, $this->defaultFormat, $context);
$response = $this->httpRequest($entity->urlInfo(), 'PATCH', $invalid_serialized, $this->defaultMimeType);
$response = $this->httpRequest($entity->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $invalid_serialized, $this->defaultMimeType);
$this->assertResponse(422);
$error = Json::decode($response);
$this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
$this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
// Try to update an entity without proper permissions.
$this->drupalLogout();
@ -183,7 +210,6 @@ class UpdateTest extends RESTTestBase {
// Enables the REST service for 'user' entity type.
$this->enableService('entity:' . $entity_type, 'PATCH');
$permissions = $this->entityPermissions($entity_type, 'update');
$permissions[] = 'restful patch entity:' . $entity_type;
$account = $this->drupalCreateUser($permissions);
$account->set('mail', 'old-email@example.com');
$this->drupalLogin($account);
@ -197,40 +223,40 @@ class UpdateTest extends RESTTestBase {
$context = ['account' => $account];
$normalized = $serializer->normalize($account, $this->defaultFormat, $context);
$serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
$response = $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
$response = $this->httpRequest($account->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(422);
$error = Json::decode($response);
$this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Email</em>.\n");
$this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Email</em>.\n");
// Try and send the new email with a password.
$normalized['pass'][0]['existing'] = 'wrong';
$serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
$response = $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
$response = $this->httpRequest($account->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(422);
$error = Json::decode($response);
$this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Email</em>.\n");
$this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Email</em>.\n");
// Try again with the password.
$normalized['pass'][0]['existing'] = $account->pass_raw;
$serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
$this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(204);
$this->assertResponse(200);
// Try to change the password without providing the current password.
$new_password = $this->randomString();
$normalized = $serializer->normalize($account, $this->defaultFormat, $context);
$normalized['pass'][0]['value'] = $new_password;
$serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
$response = $this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
$response = $this->httpRequest($account->toUrl()->setRouteParameter('_format', $this->defaultFormat), 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(422);
$error = Json::decode($response);
$this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Password</em>.\n");
$this->assertEqual($error['message'], "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the <em class=\"placeholder\">Password</em>.\n");
// Try again with the password.
$normalized['pass'][0]['existing'] = $account->pass_raw;
$serialized = $serializer->serialize($normalized, $this->defaultFormat, $context);
$this->httpRequest($account->urlInfo(), 'PATCH', $serialized, $this->defaultMimeType);
$this->assertResponse(204);
$this->assertResponse(200);
// Verify that we can log in with the new password.
$account->pass_raw = $new_password;
@ -245,7 +271,6 @@ class UpdateTest extends RESTTestBase {
// Enables the REST service for 'comment' entity type.
$this->enableService('entity:' . $entity_type, 'PATCH', ['hal_json', 'json']);
$permissions = $this->entityPermissions($entity_type, 'update');
$permissions[] = 'restful patch entity:' . $entity_type;
$account = $this->drupalCreateUser($permissions);
$account->set('mail', 'old-email@example.com');
$this->drupalLogin($account);
@ -317,7 +342,7 @@ class UpdateTest extends RESTTestBase {
protected function patchEntity(EntityInterface $entity, array $read_only_fields, AccountInterface $account, $format, $mime_type) {
$serializer = $this->container->get('serializer');
$url = $entity->toUrl();
$url = $entity->toUrl()->setRouteParameter('_format', $this->defaultFormat);
$context = ['account' => $account];
// Certain fields are always read-only, others this user simply is not
// allowed to modify. For all of them, ensure they are not serialized, else
@ -340,7 +365,7 @@ class UpdateTest extends RESTTestBase {
$this->httpRequest($url, 'PATCH', $serialized, $mime_type);
$this->assertResponse(403);
$this->assertResponseBody('{"error":"Access denied on updating field \'' . $field . '\'."}');
$this->assertResponseBody('{"message":"Access denied on updating field \\u0027' . $field . '\\u0027."}');
if ($format === 'hal_json') {
// We've just tried with this read-only field, now unset it.
@ -355,9 +380,13 @@ class UpdateTest extends RESTTestBase {
}
$serialized = $serializer->serialize($normalized, $format, $context);
// Try first without CSRF token which should fail.
$this->httpRequest($url, 'PATCH', $serialized, $mime_type, TRUE);
$this->assertResponse(403, 'X-CSRF-Token request header is missing');
// Then try with CSRF token.
$this->httpRequest($url, 'PATCH', $serialized, $mime_type);
$this->assertResponse(204);
$this->assertResponseBody('');
$this->assertResponse(200);
}
}

View file

@ -3,6 +3,7 @@
namespace Drupal\rest\Tests\Views;
use Drupal\Core\Cache\Cache;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
@ -39,7 +40,7 @@ class StyleSerializerTest extends PluginTestBase {
*
* @var array
*/
public static $modules = array('views_ui', 'entity_test', 'hal', 'rest_test_views', 'node', 'text', 'field', 'language');
public static $modules = array('views_ui', 'entity_test', 'hal', 'rest_test_views', 'node', 'text', 'field', 'language', 'basic_auth');
/**
* Views used by this test.
@ -68,6 +69,39 @@ class StyleSerializerTest extends PluginTestBase {
$this->enableViewsTestModule();
}
/**
* Checks that the auth options restricts access to a REST views display.
*/
public function testRestViewsAuthentication() {
// Assume the view is hidden behind a permission.
$this->drupalGetWithFormat('test/serialize/auth_with_perm', 'json');
$this->assertResponse(401);
// Not even logging in would make it possible to see the view, because then
// we are denied based on authentication method (cookie).
$this->drupalLogin($this->adminUser);
$this->drupalGetWithFormat('test/serialize/auth_with_perm', 'json');
$this->assertResponse(403);
$this->drupalLogout();
// But if we use the basic auth authentication strategy, we should be able
// to see the page.
$url = $this->buildUrl('test/serialize/auth_with_perm');
$response = \Drupal::httpClient()->get($url, [
'auth' => [$this->adminUser->getUsername(), $this->adminUser->pass_raw],
]);
// Ensure that any changes to variables in the other thread are picked up.
$this->refreshVariables();
$headers = $response->getHeaders();
$this->verbose('GET request to: ' . $url .
'<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
'<hr />Response headers: ' . nl2br(print_r($headers, TRUE)) .
'<hr />Response body: ' . (string) $response->getBody());
$this->assertResponse(200);
}
/**
* Checks the behavior of the Serializer callback paths and row plugins.
*/
@ -319,7 +353,7 @@ class StyleSerializerTest extends PluginTestBase {
$this->drupalGetWithFormat('test/serialize/field', 'json');
$this->assertHeader('content-type', 'application/json');
$this->assertResponse(406, 'A 406 response was returned when JSON was requested.');
// Should return a 200.
// Should return a 200.
$this->drupalGetWithFormat('test/serialize/field', 'xml');
$this->assertHeader('content-type', 'text/xml; charset=UTF-8');
$this->assertResponse(200, 'A 200 response was returned when XML was requested.');
@ -516,9 +550,8 @@ class StyleSerializerTest extends PluginTestBase {
public function testLivePreview() {
// We set up a request so it looks like an request in the live preview.
$request = new Request();
$request->setFormat('drupal_ajax', 'application/vnd.drupal-ajax');
$request->headers->set('Accept', 'application/vnd.drupal-ajax');
/** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
$request->query->add([MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']);
/** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
$request_stack = \Drupal::service('request_stack');
$request_stack->push($request);

View file

@ -0,0 +1,69 @@
<?php
/**
* @file
* Contains database additions to drupal-8.bare.standard.php.gz for testing the
* upgrade path of rest_update_8201().
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Set the schema version.
$connection->insert('key_value')
->fields([
'collection' => 'system.schema',
'name' => 'rest',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'serialization',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'basic_auth',
'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']['basic_auth'] = 8000;
$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' => ['basic_auth'],
],
],
],
'link_domain' => '~',
];
$data = $connection->insert('config')
->fields([
'name' => 'rest.settings',
'data' => serialize($config),
'collection' => ''
])
->execute();

View file

@ -0,0 +1,63 @@
<?php
/**
* @file
* Contains database additions to drupal-8.bare.standard.php.gz for testing the
* upgrade path of rest_update_8203().
*/
use Drupal\Core\Database\Database;
$connection = Database::getConnection();
// Set the schema version.
$connection->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' => ['basic_auth'],
],
],
],
'link_domain' => '~',
];
$data = $connection->insert('config')
->fields([
'name' => 'rest.settings',
'data' => serialize($config),
'collection' => ''
])
->execute();

View file

@ -0,0 +1,75 @@
<?php
/**
* @file
* Test fixture for \Drupal\rest\Tests\Update\RestExportAuthUpdateTest.
*/
use Drupal\Core\Database\Database;
use Drupal\Core\Serialization\Yaml;
$connection = Database::getConnection();
$config = $connection;
// Set the schema version.
$connection->insert('key_value')
->fields([
'collection' => 'system.schema',
'name' => 'rest',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'serialization',
'value' => 'i:8000;',
])
->fields([
'collection' => 'system.schema',
'name' => 'basic_auth',
'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'] = 0;
$extensions['module']['serialization'] = 0;
$extensions['module']['basic_auth'] = 0;
$connection->update('config')
->fields([
'data' => serialize($extensions),
])
->condition('collection', '')
->condition('name', 'core.extension')
->execute();
$config = [
'link_domain' => '~',
];
$data = $connection->insert('config')
->fields([
'name' => 'rest.settings',
'data' => serialize($config),
'collection' => '',
])
->execute();
$connection->insert('config')
->fields([
'name' => 'views.view.rest_export_with_authorization',
])
->execute();
$connection->merge('config')
->condition('name', 'views.view.rest_export_with_authorization')
->condition('collection', '')
->fields([
'data' => serialize(Yaml::decode(file_get_contents('core/modules/views/tests/modules/views_test_config/test_views/views.view.rest_export_with_authorization.yml'))),
])
->execute();

View file

@ -0,0 +1,32 @@
id: entity.comment
plugin_id: 'entity:comment'
granularity: method
configuration:
GET:
supported_formats:
- hal_json
# This resource has a method-specific format.
# @see \Drupal\rest\Tests\Update\ResourceGranularityUpdateTest
- xml
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

View file

@ -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

View file

@ -0,0 +1,32 @@
id: entity.user
plugin_id: 'entity:user'
granularity: method
configuration:
GET:
supported_formats:
- hal_json
supported_auth:
- basic_auth
# This resource has a method-specific authentication.
# @see \Drupal\rest\Tests\Update\ResourceGranularityUpdateTest
- oauth
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

View file

@ -1,6 +1,6 @@
name: 'REST test'
type: module
description: 'Provides test hooks for REST module.'
description: 'Provides test hooks and resources for REST module.'
package: Testing
version: VERSION
core: 8.x

View file

@ -0,0 +1,32 @@
<?php
namespace Drupal\rest_test\Plugin\rest\resource;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
/**
* Class used to test that serialization_class is optional.
*
* @RestResource(
* id = "serialization_test",
* label = @Translation("Optional serialization_class"),
* serialization_class = "",
* uri_paths = {}
* )
*/
class NoSerializationClassTestResource extends ResourceBase {
/**
* Responds to a POST request.
*
* @param array $data
* An array with the payload.
*
* @return \Drupal\rest\ResourceResponse
*/
public function post(array $data = []) {
return new ResourceResponse($data);
}
}

View file

@ -0,0 +1,21 @@
<?php
/**
* @file
* Test hook implementations for the REST views test module.
*/
use Drupal\views\ViewExecutable;
/**
* Implements hook_views_post_execute().
*/
function rest_test_views_views_post_execute(ViewExecutable $view) {
// Attach a custom header to the test_data_export view.
if ($view->id() === 'test_serializer_display_entity') {
if ($value = \Drupal::state()->get('rest_test_views_set_header', FALSE)) {
$view->getResponse()->headers->set('Custom-Header', $value);
}
}
}

View file

@ -27,7 +27,7 @@ display:
access:
type: perm
options:
perm: 'access content'
perm: 'administer views'
cache:
type: tag
query:
@ -149,3 +149,24 @@ display:
type: serializer
row:
type: data_field
rest_export_2:
display_plugin: rest_export
id: rest_export_2
display_title: 'REST export 2'
position: 2
display_options:
display_extenders: { }
auth:
basic_auth: basic_auth
path: test/serialize/auth_with_perm
cache_metadata:
max-age: -1
contexts:
- 'languages:language_content'
- 'languages:language_interface'
- request_format
- 'user.node_grants:view'
- user.permissions
tags:
- 'config:field.storage.node.body'

View file

@ -0,0 +1,289 @@
<?php
namespace Drupal\Tests\rest\Kernel\Entity;
use Drupal\KernelTests\KernelTestBase;
use Drupal\rest\Entity\ConfigDependencies;
use Drupal\rest\Entity\RestResourceConfig;
use Drupal\rest\RestResourceConfigInterface;
/**
* @coversDefaultClass \Drupal\rest\Entity\ConfigDependencies
*
* @group rest
*/
class ConfigDependenciesTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['rest', 'entity_test', 'serialization'];
/**
* @covers ::calculateDependencies
*
* @dataProvider providerBasicDependencies
*/
public function testCalculateDependencies(array $configuration) {
$config_dependencies = new ConfigDependencies(['hal_json' => 'hal', 'json' => 'serialization'], ['basic_auth' => 'basic_auth']);
$rest_config = RestResourceConfig::create($configuration);
$result = $config_dependencies->calculateDependencies($rest_config);
$this->assertEquals(['module' => [
'basic_auth', 'serialization', 'hal',
]], $result);
}
/**
* @covers ::onDependencyRemoval
* @covers ::onDependencyRemovalForMethodGranularity
* @covers ::onDependencyRemovalForResourceGranularity
*
* @dataProvider providerBasicDependencies
*/
public function testOnDependencyRemovalRemoveUnrelatedDependency(array $configuration) {
$config_dependencies = new ConfigDependencies(['hal_json' => 'hal', 'json' => 'serialization'], ['basic_auth' => 'basic_auth']);
$rest_config = RestResourceConfig::create($configuration);
$this->assertFalse($config_dependencies->onDependencyRemoval($rest_config, ['module' => ['node']]));
$this->assertEquals($configuration['configuration'], $rest_config->get('configuration'));
}
/**
* @return array
* An array with numerical keys:
* 0. The original REST resource configuration.
*/
public function providerBasicDependencies() {
return [
'method' => [
[
'plugin_id' => 'entity:entity_test',
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
'configuration' => [
'GET' => [
'supported_auth' => ['basic_auth'],
'supported_formats' => ['json'],
],
'POST' => [
'supported_auth' => ['cookie'],
'supported_formats' => ['hal_json'],
],
],
],
],
'resource' => [
[
'plugin_id' => 'entity:entity_test',
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
'configuration' => [
'methods' => ['GET', 'POST'],
'formats' => ['json', 'hal_json'],
'authentication' => ['cookie', 'basic_auth'],
],
],
],
];
}
/**
* @covers ::onDependencyRemoval
* @covers ::onDependencyRemovalForMethodGranularity
*/
public function testOnDependencyRemovalRemoveFormatForMethodGranularity() {
$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 ::onDependencyRemovalForMethodGranularity
*/
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 ::onDependencyRemovalForMethodGranularity
*/
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'));
}
/**
* @covers ::onDependencyRemoval
* @covers ::onDependencyRemovalForResourceGranularity
*
* @dataProvider providerOnDependencyRemovalForResourceGranularity
*/
public function testOnDependencyRemovalForResourceGranularity(array $configuration, $module, $expected_configuration) {
assert('is_string($module)');
assert('$expected_configuration === FALSE || is_array($expected_configuration)');
$config_dependencies = new ConfigDependencies(['hal_json' => 'hal', 'json' => 'serialization'], ['basic_auth' => 'basic_auth']);
$rest_config = RestResourceConfig::create($configuration);
$this->assertSame(!empty($expected_configuration), $config_dependencies->onDependencyRemoval($rest_config, ['module' => [$module]]));
if (!empty($expected_configuration)) {
$this->assertEquals($expected_configuration, $rest_config->get('configuration'));
}
}
/**
* @return array
* An array with numerical keys:
* 0. The original REST resource configuration.
* 1. The module to uninstall (the dependency that is about to be removed).
* 2. The expected configuration after uninstalling this module.
*/
public function providerOnDependencyRemovalForResourceGranularity() {
return [
'resource with multiple formats' => [
[
'plugin_id' => 'entity:entity_test',
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
'configuration' => [
'methods' => ['GET', 'POST'],
'formats' => ['json', 'hal_json'],
'authentication' => ['cookie', 'basic_auth'],
],
],
'hal',
[
'methods' => ['GET', 'POST'],
'formats' => ['json'],
'authentication' => ['cookie', 'basic_auth'],
]
],
'resource with only HAL+JSON format' => [
[
'plugin_id' => 'entity:entity_test',
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
'configuration' => [
'methods' => ['GET', 'POST'],
'formats' => ['hal_json'],
'authentication' => ['cookie', 'basic_auth'],
],
],
'hal',
FALSE
],
'resource with multiple authentication providers' => [
[
'plugin_id' => 'entity:entity_test',
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
'configuration' => [
'methods' => ['GET', 'POST'],
'formats' => ['json', 'hal_json'],
'authentication' => ['cookie', 'basic_auth'],
],
],
'basic_auth',
[
'methods' => ['GET', 'POST'],
'formats' => ['json', 'hal_json'],
'authentication' => ['cookie'],
]
],
'resource with only basic_auth authentication' => [
[
'plugin_id' => 'entity:entity_test',
'granularity' => RestResourceConfigInterface::RESOURCE_GRANULARITY,
'configuration' => [
'methods' => ['GET', 'POST'],
'formats' => ['json', 'hal_json'],
'authentication' => ['basic_auth'],
],
],
'basic_auth',
FALSE,
],
];
}
}

View file

@ -0,0 +1,44 @@
<?php
namespace Drupal\Tests\rest\Kernel\Entity;
use Drupal\KernelTests\KernelTestBase;
use Drupal\rest\Entity\RestResourceConfig;
use Drupal\rest\RestResourceConfigInterface;
/**
* @coversDefaultClass \Drupal\rest\Entity\RestResourceConfig
*
* @group rest
*/
class RestResourceConfigTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['rest', 'entity_test', 'serialization', 'basic_auth', 'user', 'hal'];
/**
* @covers ::calculateDependencies
*/
public function testCalculateDependencies() {
$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'],
],
],
]);
$rest_config->calculateDependencies();
$this->assertEquals(['module' => ['basic_auth', 'entity_test', 'hal', 'serialization', 'user']], $rest_config->getDependencies());
}
}

View file

@ -1,18 +1,17 @@
<?php
/**
* @file
* Contains \Drupal\Tests\rest\Kernel\RequestHandlerTest.
*/
namespace Drupal\Tests\rest\Kernel;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Routing\RouteMatch;
use Drupal\KernelTests\KernelTestBase;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\Plugin\Type\ResourcePluginManager;
use Drupal\rest\RequestHandler;
use Drupal\rest\ResourceResponse;
use Drupal\rest\RestResourceConfigInterface;
use Prophecy\Argument;
use Prophecy\Prophecy\MethodProphecy;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Route;
@ -31,12 +30,20 @@ class RequestHandlerTest extends KernelTestBase {
public static $modules = ['serialization', 'rest'];
/**
* The entity storage.
*
* @var \Prophecy\Prophecy\ObjectProphecy
*/
protected $entityStorage;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->requestHandler = new RequestHandler();
$this->entityStorage = $this->prophesize(EntityStorageInterface::class);
$this->requestHandler = new RequestHandler($this->entityStorage->reveal());
$this->requestHandler->setContainer($this->container);
}
@ -47,17 +54,19 @@ class RequestHandlerTest extends KernelTestBase {
*/
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->getInstance(['id' => '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.
@ -72,6 +81,7 @@ class RequestHandlerTest extends KernelTestBase {
$this->assertEquals($response, $handler_response);
// We will call the patch method this time.
$route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => 'restplugin'], ['_content_type_format' => 'json']));
$request->setMethod('PATCH');
$response = new ResourceResponse([]);
$resource->patch(NULL, $request)
@ -87,29 +97,32 @@ class RequestHandlerTest extends KernelTestBase {
* @dataProvider providerTestSerialization
* @covers ::handle
*/
public function testSerialization($data) {
public function testSerialization($data, $expected_response = FALSE) {
$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->getInstance(['id' => 'restplugin'])
->willReturn($resource->reveal());
$this->container->set('plugin.manager.rest', $stub->reveal());
// Mock 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)
->willReturn($response);
$handler_response = $this->requestHandler->handle($route_match, $request);
// Content is a serialized version of the data we provided.
$this->assertEquals(json_encode($data), $handler_response->getContent());
$this->assertEquals($expected_response !== FALSE ? $expected_response : json_encode($data), $handler_response->getContent());
}
public function providerTestSerialization() {
return [
[NULL],
// The default data for \Drupal\rest\ResourceResponse.
[NULL, ''],
[''],
['string'],
['Complex \ string $%^&@ with unicode ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ'],
@ -124,6 +137,203 @@ class RequestHandlerTest extends KernelTestBase {
];
}
/**
* @covers ::getResponseFormat
*
* Note this does *not* need to test formats being requested that are not
* accepted by the server, because the routing system would have already
* prevented those from reaching RequestHandler.
*
* @param string[] $methods
* The HTTP methods to test.
* @param string[] $supported_formats
* The supported formats for the REST route to be tested.
* @param string|false $request_format
* The value for the ?_format URL query argument, if any.
* @param string[] $request_headers
* The request headers to send, if any.
* @param string|null $request_body
* The request body to send, if any.
* @param string|null $expected_response_content_type
* The expected MIME type of the response, if any.
* @param string $expected_response_content
* The expected content of the response.
*
* @dataProvider providerTestResponseFormat
*/
public function testResponseFormat($methods, array $supported_formats, $request_format, array $request_headers, $request_body, $expected_response_content_type, $expected_response_content) {
$rest_config_name = $this->randomMachineName();
$parameters = [];
if ($request_format !== FALSE) {
$parameters['_format'] = $request_format;
}
foreach ($request_headers as $key => $value) {
unset($request_headers[$key]);
$key = strtoupper(str_replace('-', '_', $key));
$request_headers[$key] = $value;
}
foreach ($methods as $method) {
$request = Request::create('/rest/test', $method, $parameters, [], [], $request_headers, $request_body);
$route_requirement_key_format = $request->isMethodSafe() ? '_format' : '_content_type_format';
$route_match = new RouteMatch('test', new Route('/rest/test', ['_rest_resource_config' => $rest_config_name], [$route_requirement_key_format => implode('|', $supported_formats)]));
$resource = $this->prophesize(StubRequestHandlerResourcePlugin::class);
// Mock the configuration.
$config = $this->prophesize(RestResourceConfigInterface::class);
$config->getFormats($method)->willReturn($supported_formats);
$config->getResourcePlugin()->willReturn($resource->reveal());
$config->getCacheContexts()->willReturn([]);
$config->getCacheTags()->willReturn([]);
$config->getCacheMaxAge()->willReturn(12);
$this->entityStorage->load($rest_config_name)->willReturn($config->reveal());
// Mock the resource plugin.
$response = new ResourceResponse($method !== 'DELETE' ? ['REST' => 'Drupal'] : NULL);
$resource->getPluginDefinition()->willReturn([]);
$method_prophecy = new MethodProphecy($resource, strtolower($method), [Argument::any(), $request]);
$method_prophecy->willReturn($response);
$resource->addMethodProphecy($method_prophecy);
// Test the request handler.
$handler_response = $this->requestHandler->handle($route_match, $request);
$this->assertSame($expected_response_content_type, $handler_response->headers->get('Content-Type'));
$this->assertEquals($expected_response_content, $handler_response->getContent());
}
}
/**
* @return array
* 0. methods to test
* 1. supported formats for route requirements
* 2. request format
* 3. request headers
* 4. request body
* 5. expected response content type
* 6. expected response body
*/
public function providerTestResponseFormat() {
$json_encoded = Json::encode(['REST' => 'Drupal']);
$xml_encoded = "<?xml version=\"1.0\"?>\n<response><REST>Drupal</REST></response>\n";
$safe_method_test_cases = [
'safe methods: client requested format (JSON)' => [
// @todo add 'HEAD' in https://www.drupal.org/node/2752325
['GET'],
['xml', 'json'],
'json',
[],
NULL,
'application/json',
$json_encoded,
],
'safe methods: client requested format (XML)' => [
// @todo add 'HEAD' in https://www.drupal.org/node/2752325
['GET'],
['xml', 'json'],
'xml',
[],
NULL,
'text/xml',
$xml_encoded,
],
'safe methods: client requested no format: response should use the first configured format (JSON)' => [
// @todo add 'HEAD' in https://www.drupal.org/node/2752325
['GET'],
['json', 'xml'],
FALSE,
[],
NULL,
'application/json',
$json_encoded,
],
'safe methods: client requested no format: response should use the first configured format (XML)' => [
// @todo add 'HEAD' in https://www.drupal.org/node/2752325
['GET'],
['xml', 'json'],
FALSE,
[],
NULL,
'text/xml',
$xml_encoded,
],
];
$unsafe_method_bodied_test_cases = [
'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (JSON)' => [
['POST', 'PATCH'],
['xml', 'json'],
FALSE,
['Content-Type' => 'application/json'],
$json_encoded,
'application/json',
$json_encoded,
],
'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (XML)' => [
['POST', 'PATCH'],
['xml', 'json'],
FALSE,
['Content-Type' => 'text/xml'],
$xml_encoded,
'text/xml',
$xml_encoded,
],
'unsafe methods with response (POST, PATCH): client requested format other than request body format (JSON): response format should use requested format (XML)' => [
['POST', 'PATCH'],
['xml', 'json'],
'xml',
['Content-Type' => 'application/json'],
$json_encoded,
'text/xml',
$xml_encoded,
],
'unsafe methods with response (POST, PATCH): client requested format other than request body format (XML), but is allowed for the request body (JSON)' => [
['POST', 'PATCH'],
['xml', 'json'],
'json',
['Content-Type' => 'text/xml'],
$xml_encoded,
'application/json',
$json_encoded,
],
];
$unsafe_method_bodyless_test_cases = [
'unsafe methods with response bodies (DELETE): client requested no format, response should have no format' => [
['DELETE'],
['xml', 'json'],
FALSE,
['Content-Type' => 'application/json'],
$json_encoded,
NULL,
'',
],
'unsafe methods with response bodies (DELETE): client requested format (XML), response should have no format' => [
['DELETE'],
['xml', 'json'],
'xml',
['Content-Type' => 'application/json'],
$json_encoded,
NULL,
'',
],
'unsafe methods with response bodies (DELETE): client requested format (JSON), response should have no format' => [
['DELETE'],
['xml', 'json'],
'json',
['Content-Type' => 'application/json'],
$json_encoded,
NULL,
'',
],
];
return $safe_method_test_cases + $unsafe_method_bodied_test_cases + $unsafe_method_bodyless_test_cases;
}
}
/**
@ -132,6 +342,8 @@ class RequestHandlerTest extends KernelTestBase {
class StubRequestHandlerResourcePlugin extends ResourceBase {
function get() {}
function post() {}
function patch() {}
function delete() {}
}

View file

@ -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}

View file

@ -0,0 +1,69 @@
<?php
namespace Drupal\Tests\rest\Kernel\Views;
use Drupal\rest\Plugin\views\display\RestExport;
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
use Drupal\views\Entity\View;
use Drupal\views\Tests\ViewTestData;
/**
* Tests the REST export view display plugin.
*
* @coversDefaultClass \Drupal\rest\Plugin\views\display\RestExport
*
* @group rest
*/
class RestExportTest extends ViewsKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['test_serializer_display_entity'];
/**
* {@inheritdoc}
*/
public static $modules = ['rest_test_views', 'serialization', 'rest', 'entity_test'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE) {
parent::setUp($import_test_views);
ViewTestData::createTestViews(get_class($this), ['rest_test_views']);
$this->installEntitySchema('entity_test');
}
/**
* @covers ::buildResponse
*/
public function testBuildResponse() {
/** @var \Drupal\views\Entity\View $view */
$view = View::load('test_serializer_display_entity');
$display = &$view->getDisplay('rest_export_1');
$display['display_options']['defaults']['style'] = FALSE;
$display['display_options']['style']['type'] = 'serializer';
$display['display_options']['style']['options']['formats'] = ['json', 'xml'];
$view->save();
// No custom header should be set yet.
$response = RestExport::buildResponse('test_serializer_display_entity', 'rest_export_1', []);
$this->assertFalse($response->headers->get('Custom-Header'));
// Clear render cache.
/** @var \Drupal\Core\Cache\MemoryBackend $render_cache */
$render_cache = $this->container->get('cache_factory')->get('render');
$render_cache->deleteAll();
// A custom header should now be added.
// @see rest_test_views_views_post_execute()
$header = $this->randomString();
$this->container->get('state')->set('rest_test_views_set_header', $header);
$response = RestExport::buildResponse('test_serializer_display_entity', 'rest_export_1', []);
$this->assertEquals($header, $response->headers->get('Custom-Header'));
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace Drupal\Tests\rest\Kernel\Views;
use Drupal\Tests\views\Kernel\ViewsKernelTestBase;
use Drupal\views\Entity\View;
use Drupal\views\Tests\ViewTestData;
/**
* @coversDefaultClass \Drupal\rest\Plugin\views\style\Serializer
* @group views
*/
class StyleSerializerKernelTest extends ViewsKernelTestBase {
/**
* {@inheritdoc}
*/
public static $testViews = ['test_serializer_display_entity'];
/**
* {@inheritdoc}
*/
public static $modules = ['rest_test_views', 'serialization', 'rest'];
/**
* {@inheritdoc}
*/
protected function setUp($import_test_views = TRUE) {
parent::setUp($import_test_views);
ViewTestData::createTestViews(get_class($this), ['rest_test_views']);
}
/**
* @covers ::calculateDependencies
*/
public function testCalculateDepenencies() {
/** @var \Drupal\views\Entity\View $view */
$view = View::load('test_serializer_display_entity');
$display = &$view->getDisplay('rest_export_1');
$display['display_options']['defaults']['style'] = FALSE;
$display['display_options']['style']['type'] = 'serializer';
$display['display_options']['style']['options']['formats'] = ['json', 'xml'];
$view->save();
$view->calculateDependencies();
$this->assertEquals(['module' => ['rest', 'serialization', 'user']], $view->getDependencies());
\Drupal::service('module_installer')->install(['hal']);
$view = View::load('test_serializer_display_entity');
$display = &$view->getDisplay('rest_export_1');
$display['display_options']['style']['options']['formats'] = ['json', 'xml', 'hal_json'];
$view->save();
$view->calculateDependencies();
$this->assertEquals(['module' => ['hal', 'rest', 'serialization', 'user']], $view->getDependencies());
}
}

View file

@ -67,6 +67,8 @@ class CollectRoutesTest extends UnitTestCase {
->getMock();
$container->set('router.route_provider', $route_provider);
$container->setParameter('authentication_providers', ['basic_auth' => 'basic_auth']);
$state = $this->getMock('\Drupal\Core\State\StateInterface');
$container->set('state', $state);
@ -76,6 +78,12 @@ class CollectRoutesTest extends UnitTestCase {
$container->set('plugin.manager.views.style', $style_manager);
$container->set('renderer', $this->getMock('Drupal\Core\Render\RendererInterface'));
$authentication_collector = $this->getMock('\Drupal\Core\Authentication\AuthenticationCollectorInterface');
$container->set('authentication_collector', $authentication_collector);
$authentication_collector->expects($this->any())
->method('getSortedProviders')
->will($this->returnValue(['basic_auth' => 'data', 'cookie' => 'data']));
\Drupal::setContainer($container);
$this->restExport = RestExport::create($container, array(), "test_routes", array());
@ -87,6 +95,9 @@ class CollectRoutesTest extends UnitTestCase {
// Set the style option.
$this->restExport->setOption('style', array('type' => 'serializer'));
// Set the auth option.
$this->restExport->setOption('auth', ['basic_auth']);
$display_manager->expects($this->once())
->method('getDefinition')
->will($this->returnValue(array('id' => 'test', 'provider' => 'test')));
@ -132,6 +143,11 @@ class CollectRoutesTest extends UnitTestCase {
$this->assertEquals(count($requirements_1), 0, 'First route has no requirement.');
$this->assertEquals(count($requirements_2), 2, 'Views route with rest export had the format and method requirements added.');
// Check auth options.
$auth = $this->routes->get('view.test_view.page_1')->getOption('_auth');
$this->assertEquals(count($auth), 1, 'View route with rest export has an auth option added');
$this->assertEquals($auth[0], 'basic_auth', 'View route with rest export has the correct auth option added');
}
}

View file

@ -68,7 +68,7 @@ class SerializerTest extends UnitTestCase {
->willReturn()
->shouldBeCalled();
$view_serializer_style = new Serializer([], 'dummy_serializer', [], $mock_serializer->reveal(), ['json', 'xml']);
$view_serializer_style = new Serializer([], 'dummy_serializer', [], $mock_serializer->reveal(), ['json', 'xml'], ['json' => 'serialization', 'xml' => 'serialization']);
$view_serializer_style->options = ['formats' => ['xml', 'json']];
$view_serializer_style->view = $this->view;
$view_serializer_style->displayHandler = $this->displayHandler;