Update core 8.3.0

This commit is contained in:
Rob Davies 2017-04-13 15:53:35 +01:00
parent da7a7918f8
commit cd7a898e66
6144 changed files with 132297 additions and 87747 deletions

View file

@ -1,7 +1,3 @@
# 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

View file

@ -3,6 +3,7 @@ rest.settings:
type: config_object
label: 'REST settings'
mapping:
# @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
link_domain:
type: string
label: 'Domain of the relation'

View file

@ -31,6 +31,9 @@ function hook_rest_resource_alter(&$definitions) {
/**
* Alter the REST type URI.
*
* @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. Use
* hook_serialization_type_uri_alter() instead. This exists solely for BC.
*
* Modules may wish to alter the type URI generated for a resource based on the
* context of the serializer/normalizer operation.
*
@ -44,9 +47,9 @@ function hook_rest_resource_alter(&$definitions) {
* @see \Symfony\Component\Serializer\NormalizerInterface::normalize()
* @see \Symfony\Component\Serializer\DenormalizerInterface::denormalize()
*/
function hook_rest_type_uri_alter(&$uri, $context = array()) {
function hook_rest_type_uri_alter(&$uri, $context = []) {
if ($context['mymodule'] == TRUE) {
$base = \Drupal::config('rest.settings')->get('link_domain');
$base = \Drupal::config('serialization.settings')->get('link_domain');
$uri = str_replace($base, 'http://mymodule.domain', $uri);
}
}
@ -55,6 +58,9 @@ function hook_rest_type_uri_alter(&$uri, $context = array()) {
/**
* Alter the REST relation URI.
*
* @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. Use
* hook_serialization_relation_uri_alter() instead. This exists solely for BC.
*
* Modules may wish to alter the relation URI generated for a resource based on
* the context of the serializer/normalizer operation.
*
@ -68,9 +74,9 @@ function hook_rest_type_uri_alter(&$uri, $context = array()) {
* @see \Symfony\Component\Serializer\NormalizerInterface::normalize()
* @see \Symfony\Component\Serializer\DenormalizerInterface::denormalize()
*/
function hook_rest_relation_uri_alter(&$uri, $context = array()) {
function hook_rest_relation_uri_alter(&$uri, $context = []) {
if ($context['mymodule'] == TRUE) {
$base = \Drupal::config('rest.settings')->get('link_domain');
$base = \Drupal::config('serialization.settings')->get('link_domain');
$uri = str_replace($base, 'http://mymodule.domain', $uri);
}
}

View file

@ -12,25 +12,19 @@ use Drupal\Core\StringTranslation\TranslatableMarkup;
* Implements hook_requirements().
*/
function rest_requirements($phase) {
$requirements = array();
$requirements = [];
if (version_compare(PHP_VERSION, '5.6.0', '>=') && version_compare(PHP_VERSION, '7', '<') && ini_get('always_populate_raw_post_data') != -1) {
$requirements['always_populate_raw_post_data'] = array(
$requirements['always_populate_raw_post_data'] = [
'title' => t('always_populate_raw_post_data PHP setting'),
'value' => t('Not set to -1.'),
'severity' => REQUIREMENT_ERROR,
'description' => t('The always_populate_raw_post_data PHP setting should be set to -1 in PHP version 5.6. Please check the <a href="https://php.net/manual/en/ini.core.php#ini.always-populate-raw-post-data">PHP manual</a> for information on how to correct this.'),
);
];
}
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.
*
@ -90,7 +84,3 @@ function rest_update_8203() {
$rest_settings->set('bc_entity_resource_permissions', TRUE)
->save(TRUE);
}
/**
* @} End of "defgroup updates-8.1.x-to-8.2.x".
*/

View file

@ -15,13 +15,13 @@ function rest_help($route_name, RouteMatchInterface $route_match) {
case 'help.page.rest':
$output = '';
$output .= '<h3>' . t('About') . '</h3>';
$output .= '<p>' . t('The RESTful Web Services module provides a framework for exposing REST resources on your site. It provides support for content entities (see the <a href=":field">Field module help page</a> for more information about entities) such as content, users, taxonomy terms, etc.; REST support for content items of the Node module is enabled by default, and support for other types of content entities can be enabled. Other modules may add support for other types of REST resources. For more information, see the <a href=":rest">online documentation for the RESTful Web Services module</a>.', array(':rest' => 'https://www.drupal.org/documentation/modules/rest', ':field' => (\Drupal::moduleHandler()->moduleExists('field')) ? \Drupal::url('help.page', array('name' => 'field')) : '#')) . '</p>';
$output .= '<p>' . t('The RESTful Web Services module provides a framework for exposing REST resources on your site. It provides support for content entities (see the <a href=":field">Field module help page</a> for more information about entities) such as content, users, taxonomy terms, etc.; REST support for content items of the Node module is enabled by default, and support for other types of content entities can be enabled. Other modules may add support for other types of REST resources. For more information, see the <a href=":rest">online documentation for the RESTful Web Services module</a>.', [':rest' => 'https://www.drupal.org/documentation/modules/rest', ':field' => (\Drupal::moduleHandler()->moduleExists('field')) ? \Drupal::url('help.page', ['name' => 'field']) : '#']) . '</p>';
$output .= '<h3>' . t('Uses') . '</h3>';
$output .= '<dl>';
$output .= '<dt>' . t('Installing supporting modules') . '</dt>';
$output .= '<dd>' . t('In order to use REST on a web site, you need to install modules that provide serialization and authentication services. You can use the Core module <a href=":hal">HAL</a> for serialization and <a href=":basic_auth">HTTP Basic Authentication</a> for authentication, or install a contributed or custom module.', array(':hal' => (\Drupal::moduleHandler()->moduleExists('hal')) ? \Drupal::url('help.page', array('name' => 'hal')) : '#', ':basic_auth' => (\Drupal::moduleHandler()->moduleExists('basic_auth')) ? \Drupal::url('help.page', array('name' => 'basic_auth')) : '#')) . '</dd>';
$output .= '<dd>' . t('In order to use REST on a web site, you need to install modules that provide serialization and authentication services. You can use the Core module <a href=":hal">HAL</a> for serialization and <a href=":basic_auth">HTTP Basic Authentication</a> for authentication, or install a contributed or custom module.', [':hal' => (\Drupal::moduleHandler()->moduleExists('hal')) ? \Drupal::url('help.page', ['name' => 'hal']) : '#', ':basic_auth' => (\Drupal::moduleHandler()->moduleExists('basic_auth')) ? \Drupal::url('help.page', ['name' => 'basic_auth']) : '#']) . '</dd>';
$output .= '<dt>' . t('Enabling REST support for an entity type') . '</dt>';
$output .= '<dd>' . t('REST support for content items of the Node module is enabled by default, and support for other types of content entities can be enabled. To enable support, you can use a <a href=":config">process based on configuration editing</a> or the contributed <a href=":restui">Rest UI module</a>.', array(':config' => 'https://www.drupal.org/documentation/modules/rest', ':restui' => 'https://www.drupal.org/project/restui')) . '</dd>';
$output .= '<dd>' . t('REST support for content items of the Node module is enabled by default, and support for other types of content entities can be enabled. To enable support, you can use a <a href=":config">process based on configuration editing</a> or the contributed <a href=":restui">Rest UI module</a>.', [':config' => 'https://www.drupal.org/documentation/modules/rest', ':restui' => 'https://www.drupal.org/project/restui']) . '</dd>';
$output .= '<dd>' . t('You will also need to grant anonymous users permission to perform each of the REST operations you want to be available, and set up authentication properly to authorize web requests.') . '</dd>';
$output .= '</dl>';
return $output;

View file

@ -8,11 +8,6 @@
use Drupal\rest\Entity\RestResourceConfig;
use Drupal\rest\RestResourceConfigInterface;
/**
* @addtogroup updates-8.1.x-to-8.2.x
* @{
*/
/**
* Create REST resource configuration entities.
*
@ -66,8 +61,3 @@ function rest_post_update_resource_granularity() {
}
}
}
/**
* @} End of "addtogroup updates-8.1.x-to-8.2.x".
*/

View file

@ -11,15 +11,6 @@ services:
# @todo Remove this service in Drupal 9.0.0.
access_check.rest.csrf:
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', '@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', '@entity_type.manager', '@logger.channel.rest']
@ -28,3 +19,15 @@ services:
logger.channel.rest:
parent: logger.channel_base
arguments: ['rest']
# Event subscribers.
rest.resource_response.subscriber:
class: Drupal\rest\EventSubscriber\ResourceResponseSubscriber
tags:
- { name: event_subscriber }
arguments: ['@serializer', '@renderer', '@current_route_match']
rest.config_subscriber:
class: Drupal\rest\EventSubscriber\RestConfigSubscriber
arguments: ['@router.builder']
tags:
- { name: event_subscriber }

View file

@ -3,6 +3,7 @@
namespace Drupal\rest\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
use Drupal\rest\RestResourceConfigInterface;
@ -255,4 +256,22 @@ class RestResourceConfig extends ConfigEntityBase implements RestResourceConfigI
return strtoupper($method);
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
\Drupal::service('router.builder')->setRebuildNeeded();
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
\Drupal::service('router.builder')->setRebuildNeeded();
}
}

View file

@ -0,0 +1,204 @@
<?php
namespace Drupal\rest\EventSubscriber;
use Drupal\Core\Cache\CacheableResponse;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\rest\ResourceResponseInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Response subscriber that serializes and removes ResourceResponses' data.
*/
class ResourceResponseSubscriber implements EventSubscriberInterface {
/**
* The serializer.
*
* @var \Symfony\Component\Serializer\SerializerInterface
*/
protected $serializer;
/**
* The renderer.
*
* @var \Drupal\Core\Render\RendererInterface
*/
protected $renderer;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Constructs a ResourceResponseSubscriber object.
*
* @param \Symfony\Component\Serializer\SerializerInterface $serializer
* The serializer.
* @param \Drupal\Core\Render\RendererInterface $renderer
* The renderer.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(SerializerInterface $serializer, RendererInterface $renderer, RouteMatchInterface $route_match) {
$this->serializer = $serializer;
$this->renderer = $renderer;
$this->routeMatch = $route_match;
}
/**
* Serializes ResourceResponse responses' data, and removes that data.
*
* @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
* The event to process.
*/
public function onResponse(FilterResponseEvent $event) {
$response = $event->getResponse();
if (!$response instanceof ResourceResponseInterface) {
return;
}
$request = $event->getRequest();
$format = $this->getResponseFormat($this->routeMatch, $request);
$this->renderResponseBody($request, $response, $this->serializer, $format);
$event->setResponse($this->flattenResponse($response));
}
/**
* Determines the format to respond in.
*
* 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 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 body.
*
* 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.
*
* @todo Add test coverage for language negotiation contexts in
* https://www.drupal.org/node/2135829.
*/
protected function renderResponseBody(Request $request, ResourceResponseInterface $response, SerializerInterface $serializer, $format) {
$data = $response->getResponseData();
// If there is data to send, serialize and set it as the response body.
if ($data !== NULL) {
$context = new RenderContext();
$output = $this->renderer
->executeInRenderContext($context, function () use ($serializer, $data, $format) {
return $serializer->serialize($data, $format);
});
if ($response instanceof CacheableResponseInterface && !$context->isEmpty()) {
$response->addCacheableDependency($context->pop());
}
$response->setContent($output);
$response->headers->set('Content-Type', $request->getMimeType($format));
}
}
/**
* Flattens a fully rendered resource response.
*
* Ensures that complex data structures in ResourceResponse::getResponseData()
* are not serialized. Not doing this means that caching this response object
* requires unserializing the PHP data when reading this response object from
* cache, which can be very costly, and is unnecessary.
*
* @param \Drupal\rest\ResourceResponseInterface $response
* A fully rendered resource response.
*
* @return \Drupal\Core\Cache\CacheableResponse|\Symfony\Component\HttpFoundation\Response
* The flattened response.
*/
protected function flattenResponse(ResourceResponseInterface $response) {
$final_response = ($response instanceof CacheableResponseInterface) ? new CacheableResponse() : new Response();
$final_response->setContent($response->getContent());
$final_response->setStatusCode($response->getStatusCode());
$final_response->setProtocolVersion($response->getProtocolVersion());
$final_response->setCharset($response->getCharset());
$final_response->headers = clone $response->headers;
if ($final_response instanceof CacheableResponseInterface) {
$final_response->addCacheableDependency($response->getCacheableMetadata());
}
return $final_response;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Run shortly before \Drupal\Core\EventSubscriber\FinishResponseSubscriber.
$events[KernelEvents::RESPONSE][] = ['onResponse', 5];
return $events;
}
}

View file

@ -0,0 +1,53 @@
<?php
namespace Drupal\rest\EventSubscriber;
use Drupal\Core\Config\ConfigCrudEvent;
use Drupal\Core\Config\ConfigEvents;
use Drupal\Core\Routing\RouteBuilderInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* A subscriber triggering a route rebuild when certain configuration changes.
*/
class RestConfigSubscriber implements EventSubscriberInterface {
/**
* The router builder.
*
* @var \Drupal\Core\Routing\RouteBuilderInterface
*/
protected $routerBuilder;
/**
* Constructs the RestConfigSubscriber.
*
* @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
* The router builder service.
*/
public function __construct(RouteBuilderInterface $router_builder) {
$this->routerBuilder = $router_builder;
}
/**
* Informs the router builder a rebuild is needed when necessary.
*
* @param \Drupal\Core\Config\ConfigCrudEvent $event
* The Event to process.
*/
public function onSave(ConfigCrudEvent $event) {
$saved_config = $event->getConfig();
if ($saved_config->getName() === 'rest.settings' && $event->isChanged('bc_entity_resource_permissions')) {
$this->routerBuilder->setRebuildNeeded();
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[ConfigEvents::SAVE][] = ['onSave'];
return $events;
}
}

View file

@ -2,19 +2,10 @@
namespace Drupal\rest\LinkManager;
use Drupal\hal\LinkManager\ConfigurableLinkManagerInterface as MovedConfigurableLinkManagerInterface;
/**
* Defines an interface for a link manager with a configurable domain.
* @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. This has
* been moved to the hal module. This exists solely for BC.
*/
interface ConfigurableLinkManagerInterface {
/**
* Sets the link domain used in constructing link URIs.
*
* @param string $domain
* The link domain to use for constructing link URIs.
*
* @return $this
*/
public function setLinkDomain($domain);
}
interface ConfigurableLinkManagerInterface extends MovedConfigurableLinkManagerInterface {}

View file

@ -2,70 +2,10 @@
namespace Drupal\rest\LinkManager;
class LinkManager implements LinkManagerInterface {
use Drupal\hal\LinkManager\LinkManager as MovedLinkManager;
/**
* The type link manager.
*
* @var \Drupal\rest\LinkManager\TypeLinkManagerInterface
*/
protected $typeLinkManager;
/**
* The relation link manager.
*
* @var \Drupal\rest\LinkManager\RelationLinkManagerInterface
*/
protected $relationLinkManager;
/**
* Constructor.
*
* @param \Drupal\rest\LinkManager\TypeLinkManagerInterface $type_link_manager
* Manager for handling bundle URIs.
* @param \Drupal\rest\LinkManager\RelationLinkManagerInterface $relation_link_manager
* Manager for handling bundle URIs.
*/
public function __construct(TypeLinkManagerInterface $type_link_manager, RelationLinkManagerInterface $relation_link_manager) {
$this->typeLinkManager = $type_link_manager;
$this->relationLinkManager = $relation_link_manager;
}
/**
* {@inheritdoc}
*/
public function getTypeUri($entity_type, $bundle, $context = array()) {
return $this->typeLinkManager->getTypeUri($entity_type, $bundle, $context);
}
/**
* {@inheritdoc}
*/
public function getTypeInternalIds($type_uri, $context = array()) {
return $this->typeLinkManager->getTypeInternalIds($type_uri, $context);
}
/**
* {@inheritdoc}
*/
public function getRelationUri($entity_type, $bundle, $field_name, $context = array()) {
return $this->relationLinkManager->getRelationUri($entity_type, $bundle, $field_name, $context);
}
/**
* {@inheritdoc}
*/
public function getRelationInternalIds($relation_uri) {
return $this->relationLinkManager->getRelationInternalIds($relation_uri);
}
/**
* {@inheritdoc}
*/
public function setLinkDomain($domain) {
$this->relationLinkManager->setLinkDomain($domain);
$this->typeLinkManager->setLinkDomain($domain);
return $this;
}
}
/**
* @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. This has
* been moved to the hal module. This exists solely for BC.
*/
class LinkManager extends MovedLinkManager implements LinkManagerInterface {}

View file

@ -2,57 +2,10 @@
namespace Drupal\rest\LinkManager;
use Drupal\hal\LinkManager\LinkManagerBase as MovedLinkManagerBase;
/**
* Defines an abstract base-class for REST link manager objects.
* @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. This has
* been moved to the hal module. This exists solely for BC.
*/
abstract class LinkManagerBase {
/**
* Link domain used for type links URIs.
*
* @var string
*/
protected $linkDomain;
/**
* Config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* {@inheritdoc}
*/
public function setLinkDomain($domain) {
$this->linkDomain = rtrim($domain, '/');
return $this;
}
/**
* Gets the link domain.
*
* @return string
* The link domain.
*/
protected function getLinkDomain() {
if (empty($this->linkDomain)) {
if ($domain = $this->configFactory->get('rest.settings')->get('link_domain')) {
$this->linkDomain = rtrim($domain, '/');
}
else {
$request = $this->requestStack->getCurrentRequest();
$this->linkDomain = $request->getSchemeAndHttpHost() . $request->getBasePath();
}
}
return $this->linkDomain;
}
}
abstract class LinkManagerBase extends MovedLinkManagerBase {}

View file

@ -2,17 +2,10 @@
namespace Drupal\rest\LinkManager;
use Drupal\hal\LinkManager\LinkManagerInterface as MovedLinkManagerInterface;
/**
* Interface implemented by link managers.
*
* There are no explicit methods on the manager interface. Instead link managers
* broker the interactions of the different components, and therefore must
* implement each component interface, which is enforced by this interface
* extending all of the component ones.
*
* While a link manager may directly implement these interface methods with
* custom logic, it is expected to be more common for plugin managers to proxy
* the method invocations to the respective components.
* @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. This has
* been moved to the hal module. This exists solely for BC.
*/
interface LinkManagerInterface extends TypeLinkManagerInterface, RelationLinkManagerInterface {
}
interface LinkManagerInterface extends MovedLinkManagerInterface {}

View file

@ -2,141 +2,10 @@
namespace Drupal\rest\LinkManager;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\ContentEntityTypeInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Drupal\hal\LinkManager\RelationLinkManager as MovedLinkRelationManager;
class RelationLinkManager extends LinkManagerBase implements RelationLinkManagerInterface {
/**
* @var \Drupal\Core\Cache\CacheBackendInterface;
*/
protected $cache;
/**
* Entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructor.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache of relation URIs and their associated Typed Data IDs.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory service.
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(CacheBackendInterface $cache, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, ConfigFactoryInterface $config_factory, RequestStack $request_stack) {
$this->cache = $cache;
$this->entityManager = $entity_manager;
$this->configFactory = $config_factory;
$this->moduleHandler = $module_handler;
$this->requestStack = $request_stack;
}
/**
* {@inheritdoc}
*/
public function getRelationUri($entity_type, $bundle, $field_name, $context = array()) {
// Per the interface documention of this method, the returned URI may
// optionally also serve as the URL of a documentation page about this
// field. However, the REST module does not currently implement such
// a documentation page. Therefore, we return a URI assembled relative to
// the site's base URL, which is sufficient to uniquely identify the site's
// entity type + bundle + field for use in hypermedia formats, but we do
// not take into account unclean URLs, language prefixing, or anything else
// that would be required for Drupal to be able to respond with content
// at this URL. If a module is installed that adds such content, but
// requires this URL to be different (e.g., include a language prefix),
// then the module must also override the RelationLinkManager class/service
// to return the desired URL.
$uri = $this->getLinkDomain() . "/rest/relation/$entity_type/$bundle/$field_name";
$this->moduleHandler->alter('rest_relation_uri', $uri, $context);
return $uri;
}
/**
* {@inheritdoc}
*/
public function getRelationInternalIds($relation_uri, $context = array()) {
$relations = $this->getRelations($context);
if (isset($relations[$relation_uri])) {
return $relations[$relation_uri];
}
return FALSE;
}
/**
* Get the array of relation links.
*
* Any field can be handled as a relation simply by changing how it is
* normalized. Therefore, there is no prior knowledge that can be used here
* to determine which fields to assign relation URIs. Instead, each field,
* even primitives, are given a relation URI. It is up to the caller to
* determine which URIs to use.
*
* @param array $context
* Context from the normalizer/serializer operation.
*
* @return array
* An array of typed data ids (entity_type, bundle, and field name) keyed
* by corresponding relation URI.
*/
protected function getRelations($context = array()) {
$cid = 'rest:links:relations';
$cache = $this->cache->get($cid);
if (!$cache) {
$this->writeCache($context);
$cache = $this->cache->get($cid);
}
return $cache->data;
}
/**
* Writes the cache of relation links.
*
* @param array $context
* Context from the normalizer/serializer operation.
*/
protected function writeCache($context = array()) {
$data = array();
foreach ($this->entityManager->getDefinitions() as $entity_type) {
if ($entity_type instanceof ContentEntityTypeInterface) {
foreach ($this->entityManager->getBundleInfo($entity_type->id()) as $bundle => $bundle_info) {
foreach ($this->entityManager->getFieldDefinitions($entity_type->id(), $bundle) as $field_definition) {
$relation_uri = $this->getRelationUri($entity_type->id(), $bundle, $field_definition->getName(), $context);
$data[$relation_uri] = array(
'entity_type' => $entity_type,
'bundle' => $bundle,
'field_name' => $field_definition->getName(),
);
}
}
}
}
// These URIs only change when field info changes, so cache it permanently
// and only clear it when the fields cache is cleared.
$this->cache->set('rest:links:relations', $data, Cache::PERMANENT, array('entity_field_info'));
}
}
/**
* @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. This has
* been moved to the hal module. This exists solely for BC.
*/
class RelationLinkManager extends MovedLinkRelationManager implements RelationLinkManagerInterface {}

View file

@ -2,38 +2,10 @@
namespace Drupal\rest\LinkManager;
interface RelationLinkManagerInterface extends ConfigurableLinkManagerInterface {
use Drupal\hal\LinkManager\RelationLinkManagerInterface as MovedRelationLinkManagerInterface;
/**
* Gets the URI that corresponds to a field.
*
* When using hypermedia formats, this URI can be used to indicate which
* field the data represents. Documentation about this field can also be
* provided at this URI.
*
* @param string $entity_type
* The bundle's entity type.
* @param string $bundle
* The bundle name.
* @param string $field_name
* The field name.
* @param array $context
* (optional) Optional serializer/normalizer context.
*
* @return string
* The corresponding URI for the field.
*/
public function getRelationUri($entity_type, $bundle, $field_name, $context = array());
/**
* Translates a REST URI into internal IDs.
*
* @param string $relation_uri
* Relation URI to transform into internal IDs
*
* @return array
* Array with keys 'entity_type', 'bundle' and 'field_name'.
*/
public function getRelationInternalIds($relation_uri);
}
/**
* @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. This has
* been moved to the hal module. This exists solely for BC.
*/
interface RelationLinkManagerInterface extends MovedRelationLinkManagerInterface {}

View file

@ -2,147 +2,10 @@
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;
use Drupal\hal\LinkManager\TypeLinkManager as MovedTypeLinkManager;
class TypeLinkManager extends LinkManagerBase implements TypeLinkManagerInterface {
/**
* Injected cache backend.
*
* @var \Drupal\Core\Cache\CacheBackendInterface;
*/
protected $cache;
/**
* Module handler service.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The bundle info service.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $bundleInfoService;
/**
* Constructor.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The injected cache backend for caching type URIs.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* 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, EntityTypeBundleInfoInterface $bundle_info_service) {
$this->cache = $cache;
$this->configFactory = $config_factory;
$this->moduleHandler = $module_handler;
$this->requestStack = $request_stack;
$this->bundleInfoService = $bundle_info_service;
}
/**
* {@inheritdoc}
*/
public function getTypeUri($entity_type, $bundle, $context = array()) {
// Per the interface documention of this method, the returned URI may
// optionally also serve as the URL of a documentation page about this
// bundle. However, the REST module does not currently implement such
// a documentation page. Therefore, we return a URI assembled relative to
// the site's base URL, which is sufficient to uniquely identify the site's
// entity type and bundle for use in hypermedia formats, but we do not
// take into account unclean URLs, language prefixing, or anything else
// that would be required for Drupal to be able to respond with content
// at this URL. If a module is installed that adds such content, but
// requires this URL to be different (e.g., include a language prefix),
// then the module must also override the TypeLinkManager class/service to
// return the desired URL.
$uri = $this->getLinkDomain() . "/rest/type/$entity_type/$bundle";
$this->moduleHandler->alter('rest_type_uri', $uri, $context);
return $uri;
}
/**
* {@inheritdoc}
*/
public function getTypeInternalIds($type_uri, $context = array()) {
$types = $this->getTypes($context);
if (isset($types[$type_uri])) {
return $types[$type_uri];
}
return FALSE;
}
/**
* Get the array of type links.
*
* @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 getTypes($context = array()) {
$cid = 'rest:links:types';
$cache = $this->cache->get($cid);
if (!$cache) {
$data = $this->writeCache($context);
}
else {
$data = $cache->data;
}
return $data;
}
/**
* Writes the cache of type links.
*
* @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();
// Type URIs correspond to bundles. Iterate through the bundles to get the
// URI and data for them.
$entity_types = \Drupal::entityManager()->getDefinitions();
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')) {
continue;
}
foreach ($bundles as $bundle => $bundle_info) {
// Get a type URI for the bundle.
$bundle_uri = $this->getTypeUri($entity_type_id, $bundle, $context);
$data[$bundle_uri] = array(
'entity_type' => $entity_type_id,
'bundle' => $bundle,
);
}
}
// 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;
}
}
/**
* @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. This has
* been moved to the hal module. This exists solely for BC.
*/
class TypeLinkManager extends MovedTypeLinkManager implements TypeLinkManagerInterface {}

View file

@ -2,39 +2,10 @@
namespace Drupal\rest\LinkManager;
interface TypeLinkManagerInterface extends ConfigurableLinkManagerInterface {
use Drupal\hal\LinkManager\TypeLinkManagerInterface as MovedTypeLinkManagerInterface;
/**
* Gets the URI that corresponds to a bundle.
*
* When using hypermedia formats, this URI can be used to indicate which
* bundle the data represents. Documentation about required and optional
* fields can also be provided at this URI.
*
* @param $entity_type
* The bundle's entity type.
* @param $bundle
* The bundle name.
* @param array $context
* (optional) Optional serializer/normalizer context.
*
* @return string
* The corresponding URI for the bundle.
*/
public function getTypeUri($entity_type, $bundle, $context = array());
/**
* Get a bundle's Typed Data IDs based on a URI.
*
* @param string $type_uri
* The type URI.
* @param array $context
* Context from the normalizer/serializer operation.
*
* @return array | boolean
* If the URI matches a bundle, returns an array containing entity_type and
* bundle. Otherwise, returns false.
*/
public function getTypeInternalIds($type_uri, $context = array());
}
/**
* @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. This has
* been moved to the hal module. This exists solely for BC.
*/
interface TypeLinkManagerInterface extends MovedTypeLinkManagerInterface {}

View file

@ -65,17 +65,17 @@ class EntityDeriver implements ContainerDeriverInterface {
if (!isset($this->derivatives)) {
// Add in the default plugin configuration and the resource type.
foreach ($this->entityManager->getDefinitions() as $entity_type_id => $entity_type) {
$this->derivatives[$entity_type_id] = array(
$this->derivatives[$entity_type_id] = [
'id' => 'entity:' . $entity_type_id,
'entity_type' => $entity_type_id,
'serialization_class' => $entity_type->getClass(),
'label' => $entity_type->getLabel(),
);
];
$default_uris = array(
$default_uris = [
'canonical' => "/entity/$entity_type_id/" . '{' . $entity_type_id . '}',
'https://www.drupal.org/link-relations/create' => "/entity/$entity_type_id",
);
];
foreach ($default_uris as $link_relation => $default_uri) {
// Check if there are link templates defined for the entity type and

View file

@ -31,7 +31,7 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
*
* @var array
*/
protected $serializerFormats = array();
protected $serializerFormats = [];
/**
* A logger instance.
@ -81,13 +81,13 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
* resource".
*/
public function permissions() {
$permissions = array();
$permissions = [];
$definition = $this->getPluginDefinition();
foreach ($this->availableMethods() as $method) {
$lowered_method = strtolower($method);
$permissions["restful $lowered_method $this->pluginId"] = array(
'title' => $this->t('Access @method on %label resource', array('@method' => $method, '%label' => $definition['label'])),
);
$permissions["restful $lowered_method $this->pluginId"] = [
'title' => $this->t('Access @method on %label resource', ['@method' => $method, '%label' => $definition['label']]),
];
}
return $permissions;
}
@ -113,14 +113,14 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
$route->setPath($create_path);
// Restrict the incoming HTTP Content-type header to the known
// serialization formats.
$route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats)));
$route->addRequirements(['_content_type_format' => implode('|', $this->serializerFormats)]);
$collection->add("$route_name.$method", $route);
break;
case 'PATCH':
// Restrict the incoming HTTP Content-type header to the known
// serialization formats.
$route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats)));
$route->addRequirements(['_content_type_format' => implode('|', $this->serializerFormats)]);
$collection->add("$route_name.$method", $route);
break;
@ -131,7 +131,7 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
foreach ($this->serializerFormats as $format_name) {
// Expose one route per available format.
$format_route = clone $route;
$format_route->addRequirements(array('_format' => $format_name));
$format_route->addRequirements(['_format' => $format_name]);
$collection->add("$route_name.$method.$format_name", $format_route);
}
break;
@ -155,7 +155,7 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
* The list of allowed HTTP request method strings.
*/
protected function requestMethods() {
return array(
return [
'HEAD',
'GET',
'POST',
@ -165,7 +165,7 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
'OPTIONS',
'CONNECT',
'PATCH',
);
];
}
/**
@ -173,7 +173,7 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
*/
public function availableMethods() {
$methods = $this->requestMethods();
$available = array();
$available = [];
foreach ($methods as $method) {
// Only expose methods where the HTTP request method exists on the plugin.
if (method_exists($this, strtolower($method))) {
@ -195,15 +195,15 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
* The created base route.
*/
protected function getBaseRoute($canonical_path, $method) {
return new Route($canonical_path, array(
return new Route($canonical_path, [
'_controller' => 'Drupal\rest\RequestHandler::handle',
),
],
$this->getBaseRouteRequirements($method),
array(),
[],
'',
array(),
[],
// The HTTP method is a requirement for this route.
array($method)
[$method]
);
}

View file

@ -3,17 +3,22 @@
namespace Drupal\rest\Plugin\rest\resource;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Cache\CacheableResponseInterface;
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\Core\Field\FieldItemListInterface;
use Drupal\Core\TypedData\PrimitiveInterface;
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\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\HttpException;
@ -36,6 +41,9 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
*/
class EntityResource extends ResourceBase implements DependentPluginInterface {
use EntityResourceValidationTrait;
use EntityResourceAccessTrait;
/**
* The entity type targeted by this resource.
*
@ -50,6 +58,13 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
*/
protected $configFactory;
/**
* The link relation type manager used to create HTTP header links.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $linkRelationTypeManager;
/**
* Constructs a Drupal\rest\Plugin\rest\resource\EntityResource object.
*
@ -67,11 +82,14 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
* A logger instance.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Component\Plugin\PluginManagerInterface $link_relation_type_manager
* The link relation type manager.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, $serializer_formats, LoggerInterface $logger, ConfigFactoryInterface $config_factory) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, $serializer_formats, LoggerInterface $logger, ConfigFactoryInterface $config_factory, PluginManagerInterface $link_relation_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
$this->entityType = $entity_type_manager->getDefinition($plugin_definition['entity_type']);
$this->configFactory = $config_factory;
$this->linkRelationTypeManager = $link_relation_type_manager;
}
/**
@ -85,7 +103,8 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
$container->get('entity_type.manager'),
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest'),
$container->get('config.factory')
$container->get('config.factory'),
$container->get('plugin.manager.link_relation_type')
);
}
@ -103,7 +122,7 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
public function get(EntityInterface $entity) {
$entity_access = $entity->access('view', NULL, TRUE);
if (!$entity_access->isAllowed()) {
throw new AccessDeniedHttpException();
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
}
$response = new ResourceResponse($entity, 200);
@ -122,6 +141,8 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
}
}
$this->addLinkHeaders($entity, $response);
return $response;
}
@ -141,8 +162,9 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
throw new BadRequestHttpException('No entity content received.');
}
if (!$entity->access('create')) {
throw new AccessDeniedHttpException();
$entity_access = $entity->access('create', NULL, TRUE);
if (!$entity_access->isAllowed()) {
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'create'));
}
$definition = $this->getPluginDefinition();
// Verify that the deserialized entity is of the type that we expect to
@ -156,33 +178,64 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
throw new BadRequestHttpException('Only new entities can be created');
}
// Only check 'edit' permissions for fields that were actually
// submitted by the user. Field access makes no difference between 'create'
// and 'update', so the 'edit' operation is used here.
foreach ($entity->_restSubmittedFields as $key => $field_name) {
if (!$entity->get($field_name)->access('edit')) {
throw new AccessDeniedHttpException("Access denied on creating field '$field_name'");
}
}
$this->checkEditFieldAccess($entity);
// Validate the received data before saving.
$this->validate($entity);
try {
$entity->save();
$this->logger->notice('Created entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
$this->logger->notice('Created entity %type with ID %id.', ['%type' => $entity->getEntityTypeId(), '%id' => $entity->id()]);
// 201 Created responses return the newly created entity in the response
// body. These responses are not cacheable, so we add no cacheability
// metadata here.
$url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
$response = new ModifiedResourceResponse($entity, 201, ['Location' => $url->getGeneratedUrl()]);
return $response;
$headers = [];
if (in_array('canonical', $entity->uriRelationships(), TRUE)) {
$url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
$headers['Location'] = $url->getGeneratedUrl();
}
return new ModifiedResourceResponse($entity, 201, $headers);
}
catch (EntityStorageException $e) {
throw new HttpException(500, 'Internal Server Error', $e);
}
}
/**
* Gets the values from the field item list casted to the correct type.
*
* Values are casted to the correct type so we can determine whether or not
* something has changed. REST formats such as JSON support typed data but
* Drupal's database API will return values as strings. Currently, only
* primitive data types know how to cast their values to the correct type.
*
* @param \Drupal\Core\Field\FieldItemListInterface $field_item_list
* The field item list to retrieve its data from.
*
* @return mixed[][]
* The values from the field item list casted to the correct type. The array
* of values returned is a multidimensional array keyed by delta and the
* property name.
*/
protected function getCastedValueFromFieldItemList(FieldItemListInterface $field_item_list) {
$value = $field_item_list->getValue();
foreach ($value as $delta => $field_item_value) {
/** @var \Drupal\Core\Field\FieldItemInterface $field_item */
$field_item = $field_item_list->get($delta);
$properties = $field_item->getProperties(TRUE);
// Foreach field value we check whether we know the underlying property.
// If we exists we try to cast the value.
foreach ($field_item_value as $property_name => $property_value) {
if (isset($properties[$property_name]) && ($property = $field_item->get($property_name)) && $property instanceof PrimitiveInterface) {
$value[$delta][$property_name] = $property->getCastedValue();
}
}
}
return $value;
}
/**
* Responds to entity PATCH requests.
*
@ -204,8 +257,9 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
if ($entity->getEntityTypeId() != $definition['entity_type']) {
throw new BadRequestHttpException('Invalid entity type');
}
if (!$original_entity->access('update')) {
throw new AccessDeniedHttpException();
$entity_access = $original_entity->access('update', NULL, TRUE);
if (!$entity_access->isAllowed()) {
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'update'));
}
// Overwrite the received properties.
@ -218,8 +272,15 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
// them. However, rather than throwing an error, we just ignore them as
// long as their specified values match their current values.
if (in_array($field_name, $entity_keys, TRUE)) {
// @todo Work around the wrong assumption that entity keys need special
// treatment, when only read-only fields need it.
// This will be fixed in https://www.drupal.org/node/2824851.
if ($entity->getEntityTypeId() == 'comment' && $field_name == 'status' && !$original_entity->get($field_name)->access('edit')) {
throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
}
// Unchanged values for entity keys don't need access checking.
if ($original_entity->get($field_name)->getValue() === $entity->get($field_name)->getValue()) {
if ($this->getCastedValueFromFieldItemList($original_entity->get($field_name)) === $this->getCastedValueFromFieldItemList($entity->get($field_name))) {
continue;
}
// It is not possible to set the language to NULL as it is automatically
@ -239,7 +300,7 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
$this->validate($original_entity);
try {
$original_entity->save();
$this->logger->notice('Updated entity %type with ID %id.', array('%type' => $original_entity->getEntityTypeId(), '%id' => $original_entity->id()));
$this->logger->notice('Updated entity %type with ID %id.', ['%type' => $original_entity->getEntityTypeId(), '%id' => $original_entity->id()]);
// Return the updated entity in the response body.
return new ModifiedResourceResponse($original_entity, 200);
@ -261,12 +322,13 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
*/
public function delete(EntityInterface $entity) {
if (!$entity->access('delete')) {
throw new AccessDeniedHttpException();
$entity_access = $entity->access('delete', NULL, TRUE);
if (!$entity_access->isAllowed()) {
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'delete'));
}
try {
$entity->delete();
$this->logger->notice('Deleted entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
$this->logger->notice('Deleted entity %type with ID %id.', ['%type' => $entity->getEntityTypeId(), '%id' => $entity->id()]);
// DELETE responses have an empty body.
return new ModifiedResourceResponse(NULL, 204);
@ -277,36 +339,23 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
}
/**
* Verifies that the whole entity does not violate any validation constraints.
* Generates a fallback access denied message, when no specific reason is set.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity object.
* @param string $operation
* The disallowed entity operation.
*
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
* If validation errors are found.
* @return string
* The proper message to display in the AccessDeniedHttpException.
*/
protected function validate(EntityInterface $entity) {
// @todo Remove when https://www.drupal.org/node/2164373 is committed.
if (!$entity instanceof FieldableEntityInterface) {
return;
}
$violations = $entity->validate();
protected function generateFallbackAccessDeniedMessage(EntityInterface $entity, $operation) {
$message = "You are not authorized to {$operation} this {$entity->getEntityTypeId()} entity";
// Remove violations of inaccessible fields as they cannot stem from our
// changes.
$violations->filterByFieldAccess();
if (count($violations) > 0) {
$message = "Unprocessable Entity: validation failed.\n";
foreach ($violations as $violation) {
$message .= $violation->getPropertyPath() . ': ' . $violation->getMessage() . "\n";
}
// Instead of returning a generic 400 response we use the more specific
// 422 Unprocessable Entity code from RFC 4918. That way clients can
// distinguish between general syntax errors in bad serializations (code
// 400) and semantic errors in well-formed requests (code 422).
throw new HttpException(422, $message);
if ($entity->bundle() !== $entity->getEntityTypeId()) {
$message .= " of bundle {$entity->bundle()}";
}
return "{$message}.";
}
/**
@ -331,7 +380,7 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
$route = parent::getBaseRoute($canonical_path, $method);
$definition = $this->getPluginDefinition();
$parameters = $route->getOption('parameters') ?: array();
$parameters = $route->getOption('parameters') ?: [];
$parameters[$definition['entity_type']]['type'] = 'entity:' . $definition['entity_type'];
$route->setOption('parameters', $parameters);
@ -371,4 +420,35 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
}
}
/**
* Adds link headers to a response.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity.
* @param \Symfony\Component\HttpFoundation\Response $response
* The response.
*
* @see https://tools.ietf.org/html/rfc5988#section-5
*/
protected function addLinkHeaders(EntityInterface $entity, Response $response) {
foreach ($entity->getEntityType()->getLinkTemplates() as $relation_name => $link_template) {
if ($definition = $this->linkRelationTypeManager->getDefinition($relation_name, FALSE)) {
$generator_url = $entity->toUrl($relation_name)
->setAbsolute(TRUE)
->toString(TRUE);
if ($response instanceof CacheableResponseInterface) {
$response->addCacheableDependency($generator_url);
}
$uri = $generator_url->getGeneratedUrl();
$relationship = $relation_name;
if (!empty($definition['uri'])) {
$relationship = $definition['uri'];
}
$link_header = '<' . $uri . '>; rel="' . $relationship . '"';
$response->headers->set('Link', $link_header, FALSE);
}
}
}
}

View file

@ -0,0 +1,35 @@
<?php
namespace Drupal\rest\Plugin\rest\resource;
use Drupal\Core\Entity\EntityInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
/**
* @internal
* @todo Consider making public in https://www.drupal.org/node/2300677
*/
trait EntityResourceAccessTrait {
/**
* Performs edit access checks for fields.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity whose fields edit access should be checked for.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Throws access denied when the user does not have permissions to edit a
* field.
*/
protected function checkEditFieldAccess(EntityInterface $entity) {
// Only check 'edit' permissions for fields that were actually submitted by
// the user. Field access makes no difference between 'create' and 'update',
// so the 'edit' operation is used here.
foreach ($entity->_restSubmittedFields as $key => $field_name) {
if (!$entity->get($field_name)->access('edit')) {
throw new AccessDeniedHttpException("Access denied on creating field '$field_name'.");
}
}
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace Drupal\rest\Plugin\rest\resource;
use Drupal\Component\Render\PlainTextOutput;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
/**
* @internal
* @todo Consider making public in https://www.drupal.org/node/2300677
*/
trait EntityResourceValidationTrait {
/**
* Verifies that the whole entity does not violate any validation constraints.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to validate.
*
* @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
* 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
// changes.
$violations->filterByFieldAccess();
if ($violations->count() > 0) {
$message = "Unprocessable Entity: validation failed.\n";
foreach ($violations as $violation) {
// We strip every HTML from the error message to have a nicer to read
// message on REST responses.
$message .= $violation->getPropertyPath() . ': ' . PlainTextOutput::renderFromHtml($violation->getMessage()) . "\n";
}
throw new UnprocessableEntityHttpException($message);
}
}
}

View file

@ -14,6 +14,7 @@ use Drupal\views\Render\ViewsRenderPipelineMarkup;
use Drupal\views\ViewExecutable;
use Drupal\views\Plugin\views\display\PathPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
use Symfony\Component\Routing\RouteCollection;
/**
@ -265,21 +266,21 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
// Hide some settings, as they aren't useful for pure data output.
unset($options['show_admin_links'], $options['analyze-theme']);
$categories['path'] = array(
$categories['path'] = [
'title' => $this->t('Path settings'),
'column' => 'second',
'build' => array(
'build' => [
'#weight' => -10,
),
);
],
];
$options['path']['category'] = 'path';
$options['path']['title'] = $this->t('Path');
$options['auth'] = array(
$options['auth'] = [
'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.
@ -295,13 +296,13 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
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(
$form['auth'] = [
'#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.'),
'#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 appropriate 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'),
);
];
}
}
@ -347,6 +348,26 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
}
}
/**
* Determines whether the view overrides the given route.
*
* @param string $view_path
* The path of the view.
* @param \Symfony\Component\Routing\Route $view_route
* The route of the view.
* @param \Symfony\Component\Routing\Route $route
* The route itself.
*
* @return bool
* TRUE, when the view should override the given route.
*/
protected function overrideApplies($view_path, Route $view_route, Route $route) {
$route_formats = explode('|', $route->getRequirement('_format'));
$view_route_formats = explode('|', $view_route->getRequirement('_format'));
return $this->overrideAppliesPathAndMethod($view_path, $view_route, $route)
&& (!$route->hasRequirement('_format') || array_intersect($route_formats, $view_route_formats) != []);
}
/**
* {@inheritdoc}
*/
@ -385,7 +406,7 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
* {@inheritdoc}
*/
public function render() {
$build = array();
$build = [];
$build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function() {
return $this->view->style_plugin->render();
});

View file

@ -31,14 +31,14 @@ class DataFieldRow extends RowPluginBase {
*
* @var array
*/
protected $replacementAliases = array();
protected $replacementAliases = [];
/**
* Stores an array of options to determine if the raw field output is used.
*
* @var array
*/
protected $rawOutputOptions = array();
protected $rawOutputOptions = [];
/**
* {@inheritdoc}
@ -61,7 +61,7 @@ class DataFieldRow extends RowPluginBase {
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['field_options'] = array('default' => array());
$options['field_options'] = ['default' => []];
return $options;
}
@ -72,12 +72,12 @@ class DataFieldRow extends RowPluginBase {
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$form['field_options'] = array(
$form['field_options'] = [
'#type' => 'table',
'#header' => array($this->t('Field'), $this->t('Alias'), $this->t('Raw output')),
'#header' => [$this->t('Field'), $this->t('Alias'), $this->t('Raw output')],
'#empty' => $this->t('You have no fields. Add some to your view.'),
'#tree' => TRUE,
);
];
$options = $this->options['field_options'];
@ -87,22 +87,22 @@ class DataFieldRow extends RowPluginBase {
if (!empty($field['exclude'])) {
continue;
}
$form['field_options'][$id]['field'] = array(
$form['field_options'][$id]['field'] = [
'#markup' => $id,
);
$form['field_options'][$id]['alias'] = array(
'#title' => $this->t('Alias for @id', array('@id' => $id)),
];
$form['field_options'][$id]['alias'] = [
'#title' => $this->t('Alias for @id', ['@id' => $id]),
'#title_display' => 'invisible',
'#type' => 'textfield',
'#default_value' => isset($options[$id]['alias']) ? $options[$id]['alias'] : '',
'#element_validate' => array(array($this, 'validateAliasName')),
);
$form['field_options'][$id]['raw_output'] = array(
'#title' => $this->t('Raw output for @id', array('@id' => $id)),
'#element_validate' => [[$this, 'validateAliasName']],
];
$form['field_options'][$id]['raw_output'] = [
'#title' => $this->t('Raw output for @id', ['@id' => $id]),
'#title_display' => 'invisible',
'#type' => 'checkbox',
'#default_value' => isset($options[$id]['raw_output']) ? $options[$id]['raw_output'] : '',
);
];
}
}
}
@ -121,7 +121,7 @@ class DataFieldRow extends RowPluginBase {
*/
public function validateOptionsForm(&$form, FormStateInterface $form_state) {
// Collect an array of aliases to validate.
$aliases = static::extractFromOptionsArray('alias', $form_state->getValue(array('row_options', 'field_options')));
$aliases = static::extractFromOptionsArray('alias', $form_state->getValue(['row_options', 'field_options']));
// If array filter returns empty, no values have been entered. Unique keys
// should only be validated if we have some.
@ -134,7 +134,7 @@ class DataFieldRow extends RowPluginBase {
* {@inheritdoc}
*/
public function render($row) {
$output = array();
$output = [];
foreach ($this->view->field as $id => $field) {
// If the raw output option has been set, just get the raw value.

View file

@ -45,7 +45,7 @@ class Serializer extends StylePluginBase implements CacheableDependencyInterface
*
* @var array
*/
protected $formats = array();
protected $formats = [];
/**
* The serialization format providers, keyed by format.
@ -85,7 +85,7 @@ class Serializer extends StylePluginBase implements CacheableDependencyInterface
*/
protected function defineOptions() {
$options = parent::defineOptions();
$options['formats'] = array('default' => array());
$options['formats'] = ['default' => []];
return $options;
}
@ -96,13 +96,13 @@ class Serializer extends StylePluginBase implements CacheableDependencyInterface
public function buildOptionsForm(&$form, FormStateInterface $form_state) {
parent::buildOptionsForm($form, $form_state);
$form['formats'] = array(
$form['formats'] = [
'#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' => $this->getFormatOptions(),
'#default_value' => $this->options['formats'],
);
];
}
/**
@ -111,15 +111,15 @@ class Serializer extends StylePluginBase implements CacheableDependencyInterface
public function submitOptionsForm(&$form, FormStateInterface $form_state) {
parent::submitOptionsForm($form, $form_state);
$formats = $form_state->getValue(array('style_options', 'formats'));
$form_state->setValue(array('style_options', 'formats'), array_filter($formats));
$formats = $form_state->getValue(['style_options', 'formats']);
$form_state->setValue(['style_options', 'formats'], array_filter($formats));
}
/**
* {@inheritdoc}
*/
public function render() {
$rows = array();
$rows = [];
// If the Data Entity row plugin is used, this will be an array of entities
// which will pass through Serializer to one of the registered Normalizers,
// which will transform it to arrays/scalars. If the Data field row plugin

View file

@ -2,22 +2,22 @@
namespace Drupal\rest;
use Drupal\Core\Cache\CacheableResponseInterface;
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\BadRequestHttpException;
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.
*
* @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
*/
class RequestHandler implements ContainerAwareInterface, ContainerInjectionInterface {
@ -98,18 +98,16 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
$definition = $resource->getPluginDefinition();
try {
if (!empty($definition['serialization_class'])) {
$unserialized = $serializer->deserialize($received, $definition['serialization_class'], $format, array('request_method' => $method));
$unserialized = $serializer->deserialize($received, $definition['serialization_class'], $format, ['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));
$unserialized = $serializer->decode($received, $format, ['request_method' => $method]);
}
}
catch (UnexpectedValueException $e) {
$error['error'] = $e->getMessage();
$content = $serializer->serialize($error, $format);
return new Response($content, 400, array('Content-Type' => $request->getMimeType($format)));
throw new BadRequestHttpException($e->getMessage());
}
}
else {
@ -120,7 +118,7 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
// Determine the request parameters that should be passed to the resource
// plugin.
$route_parameters = $route_match->getParameters();
$parameters = array();
$parameters = [];
// Filter out all internal parameters starting with "_".
foreach ($route_parameters as $key => $parameter) {
if ($key{0} !== '_') {
@ -129,118 +127,13 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
}
// Invoke the operation on the resource plugin.
$format = $this->getResponseFormat($route_match, $request);
$response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request)));
return $response instanceof ResourceResponseInterface ?
$this->renderResponse($request, $response, $serializer, $format, $resource_config) :
$response;
}
/**
* Determines the format to respond in.
*
* 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.
*/
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();
$response = call_user_func_array([$resource, $method], array_merge($parameters, [$unserialized, $request]));
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

@ -31,7 +31,7 @@ class ResourceResponse extends Response implements CacheableResponseInterface, R
* @param array $headers
* An array of response headers.
*/
public function __construct($data = NULL, $status = 200, $headers = array()) {
public function __construct($data = NULL, $status = 200, $headers = []) {
$this->responseData = $data;
parent::__construct('', $status, $headers);
}

View file

@ -0,0 +1,48 @@
<?php
namespace Drupal\rest;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\DependencyInjection\ServiceProviderInterface;
use Drupal\rest\LinkManager\LinkManager;
use Drupal\rest\LinkManager\RelationLinkManager;
use Drupal\rest\LinkManager\TypeLinkManager;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;
/**
* Provides BC services.
*
* These services are not added via rest.services.yml because the service
* classes extend classes from the HAL module. They also have no use without
* that module.
*/
class RestServiceProvider implements ServiceProviderInterface {
/**
* {@inheritdoc}
*/
public function register(ContainerBuilder $container) {
$modules = $container->getParameter(('container.modules'));
if (isset($modules['hal'])) {
// @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
// Use hal.link_manager instead.
$service_definition = new DefinitionDecorator(new Reference('hal.link_manager'));
$service_definition->setClass(LinkManager::class);
$container->setDefinition('rest.link_manager', $service_definition);
// @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
// Use hal.link_manager.type instead.
$service_definition = new DefinitionDecorator(new Reference('hal.link_manager.type'));
$service_definition->setClass(TypeLinkManager::class);
$container->setDefinition('rest.link_manager.type', $service_definition);
// @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0.
// Use hal.link_manager.relation instead.
$service_definition = new DefinitionDecorator(new Reference('hal.link_manager.relation'));
$service_definition->setClass(RelationLinkManager::class);
$container->setDefinition('rest.link_manager.relation', $service_definition);
}
}
}

View file

@ -59,13 +59,14 @@ class ResourceRoutes extends RouteSubscriberBase {
* @return array
*/
protected function alterRoutes(RouteCollection $collection) {
// Iterate over all enabled REST resource configs.
// Iterate over all enabled REST resource config entities.
/** @var \Drupal\rest\RestResourceConfigInterface[] $resource_configs */
$resource_configs = $this->resourceConfigStorage->loadMultiple();
// Iterate over all enabled resource plugins.
foreach ($resource_configs as $resource_config) {
$resource_routes = $this->getRoutesForResourceConfig($resource_config);
$collection->addCollection($resource_routes);
if ($resource_config->status()) {
$resource_routes = $this->getRoutesForResourceConfig($resource_config);
$collection->addCollection($resource_routes);
}
}
}
@ -95,13 +96,13 @@ class ResourceRoutes extends RouteSubscriberBase {
// 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()));
$this->logger->error('At least one authentication provider must be defined for resource @id', [':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()));
$this->logger->error('At least one format must be defined for resource @id', [':id' => $rest_resource_config->id()]);
continue;
}

View file

@ -2,10 +2,13 @@
namespace Drupal\rest\Tests;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\node\NodeInterface;
use Drupal\rest\RestResourceConfigInterface;
use Drupal\simpletest\WebTestBase;
use GuzzleHttp\Cookie\FileCookieJar;
use GuzzleHttp\Cookie\SetCookie;
/**
* Test helper class that provides a REST client method to send HTTP requests.
@ -62,18 +65,57 @@ abstract class RESTTestBase extends WebTestBase {
*
* @var array
*/
public static $modules = array('rest', 'entity_test');
public static $modules = ['rest', 'entity_test'];
/**
* The last response.
*
* @var \Psr\Http\Message\ResponseInterface
*/
protected $response;
protected function setUp() {
parent::setUp();
$this->defaultFormat = 'hal_json';
$this->defaultMimeType = 'application/hal+json';
$this->defaultAuth = array('cookie');
$this->defaultAuth = ['cookie'];
$this->resourceConfigStorage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config');
// Create a test content type for node testing.
if (in_array('node', static::$modules)) {
$this->drupalCreateContentType(array('name' => 'resttest', 'type' => 'resttest'));
$this->drupalCreateContentType(['name' => 'resttest', 'type' => 'resttest']);
}
$this->cookieFile = $this->publicFilesDirectory . '/cookie.jar';
}
/**
* Calculates cookies used by guzzle later.
*
* @return \GuzzleHttp\Cookie\CookieJarInterface
* The used CURL options in guzzle.
*/
protected function cookies() {
$cookies = [];
foreach ($this->cookies as $key => $cookie) {
$cookies[$key][] = $cookie['value'];
}
$request = \Drupal::request();
$cookies = NestedArray::mergeDeep($cookies, $this->extractCookiesFromRequest($request));
$cookie_jar = new FileCookieJar($this->cookieFile);
foreach ($cookies as $key => $cookie_values) {
foreach ($cookie_values as $cookie_value) {
// setcookie() sets the value of a cookie to be deleted, when its gonna
// be removed.
if ($cookie_value !== 'deleted') {
$cookie_jar->setCookie(new SetCookie(['Name' => $key, 'Value' => $cookie_value, 'Domain' => $request->getHost()]));
}
}
}
return $cookie_jar;
}
/**
@ -100,118 +142,137 @@ abstract class RESTTestBase extends WebTestBase {
if (!isset($mime_type)) {
$mime_type = $this->defaultMimeType;
}
if (!in_array($method, array('GET', 'HEAD', 'OPTIONS', 'TRACE'))) {
if (!in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'])) {
// GET the CSRF token first for writing requests.
$requested_token = $this->drupalGet('session/token');
}
$client = \Drupal::httpClient();
$url = $this->buildUrl($url);
$curl_options = array();
$options = [
'http_errors' => FALSE,
'cookies' => $this->cookies(),
'curl' => [
CURLOPT_HEADERFUNCTION => [&$this, 'curlHeaderCallback'],
],
];
switch ($method) {
case 'GET':
// Set query if there are additional GET parameters.
$curl_options = array(
CURLOPT_HTTPGET => TRUE,
CURLOPT_CUSTOMREQUEST => 'GET',
CURLOPT_URL => $url,
CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => array('Accept: ' . $mime_type),
);
$options += [
'headers' => [
'Accept' => $mime_type,
],
];
$response = $client->get($url, $options);
break;
case 'HEAD':
$curl_options = array(
CURLOPT_HTTPGET => FALSE,
CURLOPT_CUSTOMREQUEST => 'HEAD',
CURLOPT_URL => $url,
CURLOPT_NOBODY => TRUE,
CURLOPT_HTTPHEADER => array('Accept: ' . $mime_type),
);
$response = $client->head($url, $options);
break;
case 'POST':
$curl_options = array(
CURLOPT_HTTPGET => FALSE,
CURLOPT_POST => TRUE,
CURLOPT_POSTFIELDS => $body,
CURLOPT_URL => $url,
CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => $csrf_token !== FALSE ? array(
'Content-Type: ' . $mime_type,
'X-CSRF-Token: ' . ($csrf_token === NULL ? $requested_token : $csrf_token),
) : array(
'Content-Type: ' . $mime_type,
),
);
$options += [
'headers' => $csrf_token !== FALSE ? [
'Content-Type' => $mime_type,
'X-CSRF-Token' => ($csrf_token === NULL ? $requested_token : $csrf_token),
] : [
'Content-Type' => $mime_type,
],
'body' => $body,
];
$response = $client->post($url, $options);
break;
case 'PUT':
$curl_options = array(
CURLOPT_HTTPGET => FALSE,
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_POSTFIELDS => $body,
CURLOPT_URL => $url,
CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => $csrf_token !== FALSE ? array(
'Content-Type: ' . $mime_type,
'X-CSRF-Token: ' . ($csrf_token === NULL ? $requested_token : $csrf_token),
) : array(
'Content-Type: ' . $mime_type,
),
);
$options += [
'headers' => $csrf_token !== FALSE ? [
'Content-Type' => $mime_type,
'X-CSRF-Token' => ($csrf_token === NULL ? $requested_token : $csrf_token),
] : [
'Content-Type' => $mime_type,
],
'body' => $body,
];
$response = $client->put($url, $options);
break;
case 'PATCH':
$curl_options = array(
CURLOPT_HTTPGET => FALSE,
CURLOPT_CUSTOMREQUEST => 'PATCH',
CURLOPT_POSTFIELDS => $body,
CURLOPT_URL => $url,
CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => $csrf_token !== FALSE ? array(
'Content-Type: ' . $mime_type,
'X-CSRF-Token: ' . ($csrf_token === NULL ? $requested_token : $csrf_token),
) : array(
'Content-Type: ' . $mime_type,
),
);
$options += [
'headers' => $csrf_token !== FALSE ? [
'Content-Type' => $mime_type,
'X-CSRF-Token' => ($csrf_token === NULL ? $requested_token : $csrf_token),
] : [
'Content-Type' => $mime_type,
],
'body' => $body,
];
$response = $client->patch($url, $options);
break;
case 'DELETE':
$curl_options = array(
CURLOPT_HTTPGET => FALSE,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_URL => $url,
CURLOPT_NOBODY => FALSE,
CURLOPT_HTTPHEADER => $csrf_token !== FALSE ? array(
'X-CSRF-Token: ' . ($csrf_token === NULL ? $requested_token : $csrf_token),
) : array(),
);
$options += [
'headers' => $csrf_token !== FALSE ? [
'Content-Type' => $mime_type,
'X-CSRF-Token' => ($csrf_token === NULL ? $requested_token : $csrf_token),
] : [],
];
$response = $client->delete($url, $options);
break;
}
if ($mime_type === 'none') {
unset($curl_options[CURLOPT_HTTPHEADER]['Content-Type']);
}
$this->responseBody = $this->curlExec($curl_options);
$this->response = $response;
$this->responseBody = (string) $response->getBody();
$this->setRawContent($this->responseBody);
// Ensure that any changes to variables in the other thread are picked up.
$this->refreshVariables();
$headers = $this->drupalGetHeaders();
$this->verbose($method . ' request to: ' . $url .
'<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
(isset($curl_options[CURLOPT_HTTPHEADER]) ? '<hr />Request headers: ' . nl2br(print_r($curl_options[CURLOPT_HTTPHEADER], TRUE)) : '' ) .
(isset($curl_options[CURLOPT_POSTFIELDS]) ? '<hr />Request body: ' . nl2br(print_r($curl_options[CURLOPT_POSTFIELDS], TRUE)) : '' ) .
'<hr />Response headers: ' . nl2br(print_r($headers, TRUE)) .
'<hr />Code: ' . $this->response->getStatusCode() .
(isset($options['headers']) ? '<hr />Request headers: ' . nl2br(print_r($options['headers'], TRUE)) : '') .
(isset($options['body']) ? '<hr />Request body: ' . nl2br(print_r($options['body'], TRUE)) : '') .
'<hr />Response headers: ' . nl2br(print_r($response->getHeaders(), TRUE)) .
'<hr />Response body: ' . $this->responseBody);
return $this->responseBody;
}
/**
* {@inheritdoc}
*/
protected function assertResponse($code, $message = '', $group = 'Browser') {
if (!isset($this->response)) {
return parent::assertResponse($code, $message, $group);
}
return $this->assertEqual($code, $this->response->getStatusCode(), $message ? $message : "HTTP response expected $code, actual {$this->response->getStatusCode()}", $group);
}
/**
* {@inheritdoc}
*/
protected function drupalGetHeaders($all_requests = FALSE) {
if (!isset($this->response)) {
return parent::drupalGetHeaders($all_requests);
}
$lowercased_keys = array_map('strtolower', array_keys($this->response->getHeaders()));
return array_map(function (array $header) {
return implode(', ', $header);
}, array_combine($lowercased_keys, array_values($this->response->getHeaders())));
}
/**
* {@inheritdoc}
*/
protected function drupalGetHeader($name, $all_requests = FALSE) {
if (!isset($this->response)) {
return parent::drupalGetHeader($name, $all_requests);
}
if ($header = $this->response->getHeader($name)) {
return implode(', ', $header);
}
}
/**
* Creates entity objects based on their types.
*
@ -242,28 +303,28 @@ abstract class RESTTestBase extends WebTestBase {
protected function entityValues($entity_type_id) {
switch ($entity_type_id) {
case 'entity_test':
return array(
return [
'name' => $this->randomMachineName(),
'user_id' => 1,
'field_test_text' => array(0 => array(
'field_test_text' => [0 => [
'value' => $this->randomString(),
'format' => 'plain_text',
)),
);
]],
];
case 'config_test':
return [
'id' => $this->randomMachineName(),
'label' => 'Test label',
];
case 'node':
return array('title' => $this->randomString(), 'type' => 'resttest');
return ['title' => $this->randomString(), 'type' => 'resttest'];
case 'node_type':
return array(
return [
'type' => 'article',
'name' => $this->randomMachineName(),
);
];
case 'user':
return array('name' => $this->randomMachineName());
return ['name' => $this->randomMachineName()];
case 'comment':
return [
@ -279,11 +340,20 @@ abstract class RESTTestBase extends WebTestBase {
'vid' => 'tags',
'name' => $this->randomMachineName(),
];
case 'block':
// Block placements depend on themes, ensure Bartik is installed.
$this->container->get('theme_installer')->install(['bartik']);
return [
'id' => strtolower($this->randomMachineName(8)),
'plugin' => 'system_powered_by_block',
'theme' => 'bartik',
'region' => 'header',
];
default:
if ($this->isConfigEntity($entity_type_id)) {
return $this->configEntityValues($entity_type_id);
}
return array();
return [];
}
}
@ -350,8 +420,7 @@ abstract class RESTTestBase extends WebTestBase {
* Rebuilds routing caches.
*/
protected function rebuildCache() {
// Rebuild routing cache, so that the REST API paths are available.
$this->container->get('router.builder')->rebuild();
$this->container->get('router.builder')->rebuildIfNeeded();
}
/**
@ -362,6 +431,8 @@ abstract class RESTTestBase extends WebTestBase {
* override it every time it is omitted.
*/
protected function curlExec($curl_options, $redirect = FALSE) {
unset($this->response);
if (!isset($curl_options[CURLOPT_CUSTOMREQUEST])) {
if (!empty($curl_options[CURLOPT_HTTPGET])) {
$curl_options[CURLOPT_CUSTOMREQUEST] = 'GET';
@ -389,22 +460,22 @@ abstract class RESTTestBase extends WebTestBase {
case 'entity_test':
switch ($operation) {
case 'view':
return array('view test entity');
return ['view test entity'];
case 'create':
case 'update':
case 'delete':
return array('administer entity_test content');
return ['administer entity_test content'];
}
case 'node':
switch ($operation) {
case 'view':
return array('access content');
return ['access content'];
case 'create':
return array('create resttest content');
return ['create resttest content'];
case 'update':
return array('edit any resttest content');
return ['edit any resttest content'];
case 'delete':
return array('delete any resttest content');
return ['delete any resttest content'];
}
case 'comment':
@ -502,7 +573,7 @@ abstract class RESTTestBase extends WebTestBase {
* TRUE if the assertion succeeded, FALSE otherwise.
*/
protected function assertResponseBody($expected, $message = '', $group = 'REST Response') {
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);
return $this->assertIdentical($expected, $this->responseBody, $message ? $message : strtr('Response body @expected (expected) is equal to @response (actual).', ['@expected' => var_export($expected, TRUE), '@response' => var_export($this->responseBody, TRUE)]), $group);
}
/**

View file

@ -19,7 +19,7 @@ class ResourceTest extends RESTTestBase {
*
* @var array
*/
public static $modules = array('hal', 'rest', 'entity_test', 'rest_test');
public static $modules = ['hal', 'rest', 'entity_test', 'rest_test'];
/**
* The entity.

View file

@ -84,5 +84,4 @@ class ExcludedFieldTokenTest extends ViewTestBase {
$this->assertIdentical($actual_json, json_encode($expected));
}
}

View file

@ -40,14 +40,14 @@ class StyleSerializerTest extends PluginTestBase {
*
* @var array
*/
public static $modules = array('views_ui', 'entity_test', 'hal', 'rest_test_views', 'node', 'text', 'field', 'language', 'basic_auth');
public static $modules = ['views_ui', 'entity_test', 'hal', 'rest_test_views', 'node', 'text', 'field', 'language', 'basic_auth'];
/**
* Views used by this test.
*
* @var array
*/
public static $testViews = array('test_serializer_display_field', 'test_serializer_display_entity', 'test_serializer_display_entity_translated', 'test_serializer_node_display_field', 'test_serializer_node_exposed_filter');
public static $testViews = ['test_serializer_display_field', 'test_serializer_display_entity', 'test_serializer_display_entity_translated', 'test_serializer_node_display_field', 'test_serializer_node_exposed_filter'];
/**
* A user with administrative privileges to look at test entity and configure views.
@ -57,13 +57,13 @@ class StyleSerializerTest extends PluginTestBase {
protected function setUp() {
parent::setUp();
ViewTestData::createTestViews(get_class($this), array('rest_test_views'));
ViewTestData::createTestViews(get_class($this), ['rest_test_views']);
$this->adminUser = $this->drupalCreateUser(array('administer views', 'administer entity_test content', 'access user profiles', 'view test entity'));
$this->adminUser = $this->drupalCreateUser(['administer views', 'administer entity_test content', 'access user profiles', 'view test entity']);
// Save some entity_test entities.
for ($i = 1; $i <= 10; $i++) {
EntityTest::create(array('name' => 'test_' . $i, 'user_id' => $this->adminUser->id()))->save();
EntityTest::create(['name' => 'test_' . $i, 'user_id' => $this->adminUser->id()])->save();
}
$this->enableViewsTestModule();
@ -122,9 +122,9 @@ class StyleSerializerTest extends PluginTestBase {
$headers = $this->drupalGetHeaders();
$this->assertEqual($headers['content-type'], 'application/json', 'The header Content-type is correct.');
$expected = array();
$expected = [];
foreach ($view->result as $row) {
$expected_row = array();
$expected_row = [];
foreach ($view->field as $id => $field) {
$expected_row[$id] = $field->render($row);
}
@ -154,7 +154,7 @@ class StyleSerializerTest extends PluginTestBase {
// Get the serializer service.
$serializer = $this->container->get('serializer');
$entities = array();
$entities = [];
foreach ($view->result as $row) {
$entities[] = $row->_entity;
}
@ -180,15 +180,15 @@ class StyleSerializerTest extends PluginTestBase {
// Change the default format to xml.
$view->setDisplay('rest_export_1');
$view->getDisplay()->setOption('style', array(
$view->getDisplay()->setOption('style', [
'type' => 'serializer',
'options' => array(
'options' => [
'uses_fields' => FALSE,
'formats' => array(
'formats' => [
'xml' => 'xml',
),
),
));
],
],
]);
$view->save();
$expected = $serializer->serialize($entities, 'xml');
$actual_xml = $this->drupalGet('test/serialize/entity');
@ -197,16 +197,16 @@ class StyleSerializerTest extends PluginTestBase {
// Allow multiple formats.
$view->setDisplay('rest_export_1');
$view->getDisplay()->setOption('style', array(
$view->getDisplay()->setOption('style', [
'type' => 'serializer',
'options' => array(
'options' => [
'uses_fields' => FALSE,
'formats' => array(
'formats' => [
'xml' => 'xml',
'json' => 'json',
),
),
));
],
],
]);
$view->save();
$expected = $serializer->serialize($entities, 'json');
$actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'json');
@ -219,7 +219,7 @@ class StyleSerializerTest extends PluginTestBase {
/**
* Verifies site maintenance mode functionality.
*/
protected function testSiteMaintenance() {
public function testSiteMaintenance() {
$view = Views::getView('test_serializer_display_field');
$view->initDisplay();
$this->executeView($view);
@ -346,8 +346,8 @@ class StyleSerializerTest extends PluginTestBase {
$style_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/style_options';
// Select only 'xml' as an accepted format.
$this->drupalPostForm($style_options, array('style_options[formats][xml]' => 'xml'), t('Apply'));
$this->drupalPostForm(NULL, array(), t('Save'));
$this->drupalPostForm($style_options, ['style_options[formats][xml]' => 'xml'], t('Apply'));
$this->drupalPostForm(NULL, [], t('Save'));
// Should return a 406.
$this->drupalGetWithFormat('test/serialize/field', 'json');
@ -359,8 +359,8 @@ class StyleSerializerTest extends PluginTestBase {
$this->assertResponse(200, 'A 200 response was returned when XML was requested.');
// Add 'json' as an accepted format, so we have multiple.
$this->drupalPostForm($style_options, array('style_options[formats][json]' => 'json'), t('Apply'));
$this->drupalPostForm(NULL, array(), t('Save'));
$this->drupalPostForm($style_options, ['style_options[formats][json]' => 'json'], t('Apply'));
$this->drupalPostForm(NULL, [], t('Save'));
// Should return a 200.
// @todo This should be fixed when we have better content negotiation.
@ -369,7 +369,7 @@ class StyleSerializerTest extends PluginTestBase {
$this->assertResponse(200, 'A 200 response was returned when any format was requested.');
// Should return a 200. Emulates a sample Firefox header.
$this->drupalGet('test/serialize/field', array(), array('Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'));
$this->drupalGet('test/serialize/field', [], ['Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8']);
$this->assertHeader('content-type', 'application/json');
$this->assertResponse(200, 'A 200 response was returned when a browser accept header was requested.');
@ -393,7 +393,7 @@ class StyleSerializerTest extends PluginTestBase {
$this->assertResponse(200, 'A 200 response was returned when HTML was requested.');
// Now configure now format, so all of them should be allowed.
$this->drupalPostForm($style_options, array('style_options[formats][json]' => '0', 'style_options[formats][xml]' => '0'), t('Apply'));
$this->drupalPostForm($style_options, ['style_options[formats][json]' => '0', 'style_options[formats][xml]' => '0'], t('Apply'));
// Should return a 200.
$this->drupalGetWithFormat('test/serialize/field', 'json');
@ -424,16 +424,16 @@ class StyleSerializerTest extends PluginTestBase {
// Test an empty string for an alias, this should not be used. This also
// tests that the form can be submitted with no aliases.
$this->drupalPostForm($row_options, array('row_options[field_options][name][alias]' => ''), t('Apply'));
$this->drupalPostForm(NULL, array(), t('Save'));
$this->drupalPostForm($row_options, ['row_options[field_options][name][alias]' => ''], t('Apply'));
$this->drupalPostForm(NULL, [], t('Save'));
$view = Views::getView('test_serializer_display_field');
$view->setDisplay('rest_export_1');
$this->executeView($view);
$expected = array();
$expected = [];
foreach ($view->result as $row) {
$expected_row = array();
$expected_row = [];
foreach ($view->field as $id => $field) {
$expected_row[$id] = $field->render($row);
}
@ -443,32 +443,32 @@ class StyleSerializerTest extends PluginTestBase {
$this->assertIdentical($this->drupalGetJSON('test/serialize/field'), $this->castSafeStrings($expected));
// Test a random aliases for fields, they should be replaced.
$alias_map = array(
$alias_map = [
'name' => $this->randomMachineName(),
// Use # to produce an invalid character for the validation.
'nothing' => '#' . $this->randomMachineName(),
'created' => 'created',
);
];
$edit = array('row_options[field_options][name][alias]' => $alias_map['name'], 'row_options[field_options][nothing][alias]' => $alias_map['nothing']);
$edit = ['row_options[field_options][name][alias]' => $alias_map['name'], 'row_options[field_options][nothing][alias]' => $alias_map['nothing']];
$this->drupalPostForm($row_options, $edit, t('Apply'));
$this->assertText(t('The machine-readable name must contain only letters, numbers, dashes and underscores.'));
// Change the map alias value to a valid one.
$alias_map['nothing'] = $this->randomMachineName();
$edit = array('row_options[field_options][name][alias]' => $alias_map['name'], 'row_options[field_options][nothing][alias]' => $alias_map['nothing']);
$edit = ['row_options[field_options][name][alias]' => $alias_map['name'], 'row_options[field_options][nothing][alias]' => $alias_map['nothing']];
$this->drupalPostForm($row_options, $edit, t('Apply'));
$this->drupalPostForm(NULL, array(), t('Save'));
$this->drupalPostForm(NULL, [], t('Save'));
$view = Views::getView('test_serializer_display_field');
$view->setDisplay('rest_export_1');
$this->executeView($view);
$expected = array();
$expected = [];
foreach ($view->result as $row) {
$expected_row = array();
$expected_row = [];
foreach ($view->field as $id => $field) {
$expected_row[$alias_map[$id]] = $field->render($row);
}
@ -491,12 +491,12 @@ class StyleSerializerTest extends PluginTestBase {
// Test an empty string for an alias, this should not be used. This also
// tests that the form can be submitted with no aliases.
$values = array(
$values = [
'row_options[field_options][created][raw_output]' => '1',
'row_options[field_options][name][raw_output]' => '1',
);
];
$this->drupalPostForm($row_options, $values, t('Apply'));
$this->drupalPostForm(NULL, array(), t('Save'));
$this->drupalPostForm(NULL, [], t('Save'));
$view = Views::getView('test_serializer_display_field');
$view->setDisplay('rest_export_1');
@ -562,7 +562,7 @@ class StyleSerializerTest extends PluginTestBase {
// Get the serializer service.
$serializer = $this->container->get('serializer');
$entities = array();
$entities = [];
foreach ($view->result as $row) {
$entities[] = $row->_entity;
}
@ -580,15 +580,15 @@ class StyleSerializerTest extends PluginTestBase {
// Change the request format to xml.
$view->setDisplay('rest_export_1');
$view->getDisplay()->setOption('style', array(
$view->getDisplay()->setOption('style', [
'type' => 'serializer',
'options' => array(
'options' => [
'uses_fields' => FALSE,
'formats' => array(
'formats' => [
'xml' => 'xml',
),
),
));
],
],
]);
$this->executeView($view);
$build = $view->preview();
@ -602,7 +602,7 @@ class StyleSerializerTest extends PluginTestBase {
public function testSerializerViewsUI() {
$this->drupalLogin($this->adminUser);
// Click the "Update preview button".
$this->drupalPostForm('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1', $edit = array(), t('Update preview'));
$this->drupalPostForm('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1', $edit = [], t('Update preview'));
$this->assertResponse(200);
// Check if we receive the expected result.
$result = $this->xpath('//div[@id="views-live-preview"]/pre');
@ -613,7 +613,7 @@ class StyleSerializerTest extends PluginTestBase {
* Tests the field row style using fieldapi fields.
*/
public function testFieldapiField() {
$this->drupalCreateContentType(array('type' => 'page'));
$this->drupalCreateContentType(['type' => 'page']);
$node = $this->drupalCreateNode();
$result = $this->drupalGetJSON('test/serialize/node-field');
@ -749,44 +749,44 @@ class StyleSerializerTest extends PluginTestBase {
* the value provided.
*/
public function testRestViewExposedFilter() {
$this->drupalCreateContentType(array('type' => 'page'));
$node0 = $this->drupalCreateNode(array('title' => 'Node 1'));
$node1 = $this->drupalCreateNode(array('title' => 'Node 11'));
$node2 = $this->drupalCreateNode(array('title' => 'Node 111'));
$this->drupalCreateContentType(['type' => 'page']);
$node0 = $this->drupalCreateNode(['title' => 'Node 1']);
$node1 = $this->drupalCreateNode(['title' => 'Node 11']);
$node2 = $this->drupalCreateNode(['title' => 'Node 111']);
// Test that no filter brings back all three nodes.
$result = $this->drupalGetJSON('test/serialize/node-exposed-filter');
$expected = array(
0 => array(
$expected = [
0 => [
'nid' => $node0->id(),
'body' => $node0->body->processed,
),
1 => array(
],
1 => [
'nid' => $node1->id(),
'body' => $node1->body->processed,
),
2 => array(
],
2 => [
'nid' => $node2->id(),
'body' => $node2->body->processed,
),
);
],
];
$this->assertEqual($result, $expected, 'Querying a view with no exposed filter returns all nodes.');
// Test that title starts with 'Node 11' query finds 2 of the 3 nodes.
$result = $this->drupalGetJSON('test/serialize/node-exposed-filter', ['query' => ['title' => 'Node 11']]);
$expected = array(
0 => array(
$expected = [
0 => [
'nid' => $node1->id(),
'body' => $node1->body->processed,
),
1 => array(
],
1 => [
'nid' => $node2->id(),
'body' => $node2->body->processed,
),
);
],
];
$cache_contexts = [
'languages:language_content',

View file

@ -10,24 +10,6 @@ use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Access\AccessResult;
/**
* Implements hook_rest_type_uri_alter().
*/
function rest_test_rest_type_uri_alter(&$uri, $context = array()) {
if (!empty($context['rest_test'])) {
$uri = 'rest_test_type';
}
}
/**
* Implements hook_rest_relation_uri_alter().
*/
function rest_test_rest_relation_uri_alter(&$uri, $context = array()) {
if (!empty($context['rest_test'])) {
$uri = 'rest_test_relation';
}
}
/**
* Implements hook_entity_field_access().
*

View file

@ -0,0 +1,9 @@
services:
rest_test.authentication.test_auth:
class: Drupal\rest_test\Authentication\Provider\TestAuth
tags:
- { name: authentication_provider, provider_id: 'rest_test_auth' }
rest_test.authentication.test_auth_global:
class: Drupal\rest_test\Authentication\Provider\TestAuthGlobal
tags:
- { name: authentication_provider, provider_id: 'rest_test_auth_global', global: TRUE }

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\rest_test\Authentication\Provider;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Authentication provider for testing purposes.
*/
class TestAuth implements AuthenticationProviderInterface {
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
return $request->headers->has('REST-test-auth');
}
/**
* {@inheritdoc}
*/
public function authenticate(Request $request) {
return NULL;
}
}

View file

@ -0,0 +1,27 @@
<?php
namespace Drupal\rest_test\Authentication\Provider;
use Drupal\Core\Authentication\AuthenticationProviderInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Global authentication provider for testing purposes.
*/
class TestAuthGlobal implements AuthenticationProviderInterface {
/**
* {@inheritdoc}
*/
public function applies(Request $request) {
return $request->headers->has('REST-test-auth-global');
}
/**
* {@inheritdoc}
*/
public function authenticate(Request $request) {
return NULL;
}
}

View file

@ -53,10 +53,8 @@ trait CookieResourceTestTrait {
* {@inheritdoc}
*/
protected function initAuthentication() {
// @todo Remove hardcoded use of the 'json' format, and use static::$format
// + static::$mimeType instead in https://www.drupal.org/node/2820888.
$user_login_url = Url::fromRoute('user.login.http')
->setRouteParameter('_format', 'json');
->setRouteParameter('_format', static::$format);
$request_body = [
'name' => $this->account->name->value,
@ -64,7 +62,9 @@ trait CookieResourceTestTrait {
];
$request_options[RequestOptions::BODY] = $this->serializer->encode($request_body, 'json');
$request_options[RequestOptions::HEADERS]['Accept'] = 'application/json';
$request_options[RequestOptions::HEADERS] = [
'Content-Type' => static::$mimeType,
];
$response = $this->request('POST', $user_login_url, $request_options);
// Parse and store the session cookie.
@ -92,7 +92,10 @@ trait CookieResourceTestTrait {
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) {
$this->assertResourceErrorResponse(403, '', $response);
// Requests needing cookie authentication but missing it results in a 403
// response. The cookie authentication mechanism sets no response message.
// @todo https://www.drupal.org/node/2847623
$this->assertResourceErrorResponse(403, FALSE, $response);
}
/**

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\Action;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class ActionJsonAnonTest extends ActionResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\Action;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ActionJsonBasicAuthTest extends ActionResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\Action;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class ActionJsonCookieTest extends ActionResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,89 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\Action;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\system\Entity\Action;
use Drupal\user\RoleInterface;
abstract class ActionResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['user'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'action';
/**
* @var \Drupal\system\ActionConfigEntityInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer actions']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$action = Action::create([
'id' => 'user_add_role_action.' . RoleInterface::ANONYMOUS_ID,
'type' => 'user',
'label' => t('Add the anonymous role to the selected users'),
'configuration' => [
'rid' => RoleInterface::ANONYMOUS_ID,
],
'plugin' => 'user_add_role_action',
]);
$action->save();
return $action;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'configuration' => [
'rid' => 'anonymous',
],
'dependencies' => [
'config' => ['user.role.anonymous'],
'module' => ['user'],
],
'id' => 'user_add_role_action.anonymous',
'label' => 'Add the anonymous role to the selected users',
'langcode' => 'en',
'plugin' => 'user_add_role_action',
'status' => TRUE,
'type' => 'user',
'uuid' => $this->entity->uuid(),
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return [
'user.permissions',
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
}

View file

@ -21,9 +21,4 @@ class BlockJsonAnonTest extends BlockResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
}

View file

@ -3,7 +3,6 @@
namespace Drupal\Tests\rest\Functional\EntityResource\Block;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\JsonBasicAuthWorkaroundFor2805281Trait;
/**
* @group rest
@ -27,19 +26,9 @@ class BlockJsonBasicAuthTest extends BlockResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
// @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage.
use JsonBasicAuthWorkaroundFor2805281Trait {
JsonBasicAuthWorkaroundFor2805281Trait::assertResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait;
}
}

View file

@ -21,11 +21,6 @@ class BlockJsonCookieTest extends BlockResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/

View file

@ -113,7 +113,7 @@ abstract class BlockResourceTestBase extends EntityResourceTestBase {
*/
protected function getExpectedCacheContexts() {
// @see ::createEntity()
return [];
return ['url.site'];
}
/**
@ -122,9 +122,25 @@ abstract class BlockResourceTestBase extends EntityResourceTestBase {
protected function getExpectedCacheTags() {
// Because the 'user.permissions' cache context is missing, the cache tag
// for the anonymous user role is never added automatically.
return array_filter(parent::getExpectedCacheTags(), function ($tag) {
return array_values(array_filter(parent::getExpectedCacheTags(), function ($tag) {
return $tag !== 'config:user.role.anonymous';
});
}));
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
switch ($method) {
case 'GET':
return "You are not authorized to view this block entity.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}

View file

@ -21,11 +21,6 @@ class CommentJsonAnonTest extends CommentResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*

View file

@ -3,7 +3,6 @@
namespace Drupal\Tests\rest\Functional\EntityResource\Comment;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\JsonBasicAuthWorkaroundFor2805281Trait;
/**
* @group rest
@ -27,19 +26,9 @@ class CommentJsonBasicAuthTest extends CommentResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
// @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage.
use JsonBasicAuthWorkaroundFor2805281Trait {
JsonBasicAuthWorkaroundFor2805281Trait::assertResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait;
}
}

View file

@ -21,11 +21,6 @@ class CommentJsonCookieTest extends CommentResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/

View file

@ -28,6 +28,7 @@ abstract class CommentResourceTestBase extends EntityResourceTestBase {
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
'status',
'pid',
'entity_id',
'uid',
@ -35,7 +36,6 @@ abstract class CommentResourceTestBase extends EntityResourceTestBase {
'homepage',
'created',
'changed',
'status',
'thread',
'entity_type',
'field_name',
@ -87,10 +87,10 @@ abstract class CommentResourceTestBase extends EntityResourceTestBase {
$this->addDefaultCommentField('entity_test', 'bar', 'comment');
// Create a "Camelids" test entity that the comment will be assigned to.
$commented_entity = EntityTest::create(array(
$commented_entity = EntityTest::create([
'name' => 'Camelids',
'type' => 'bar',
));
]);
$commented_entity->save();
// Create a "Llama" comment.
@ -144,17 +144,17 @@ abstract class CommentResourceTestBase extends EntityResourceTestBase {
],
'status' => [
[
'value' => 1,
'value' => TRUE,
],
],
'created' => [
[
'value' => '123456789',
'value' => 123456789,
],
],
'changed' => [
[
'value' => '123456789',
'value' => $this->entity->getChangedTime(),
],
],
'default_langcode' => [
@ -164,7 +164,7 @@ abstract class CommentResourceTestBase extends EntityResourceTestBase {
],
'uid' => [
[
'target_id' => $author->id(),
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => base_path() . 'user/' . $author->id(),
@ -178,7 +178,7 @@ abstract class CommentResourceTestBase extends EntityResourceTestBase {
],
'entity_id' => [
[
'target_id' => '1',
'target_id' => 1,
'target_type' => 'entity_test',
'target_uuid' => EntityTest::load(1)->uuid(),
'url' => base_path() . 'entity_test/1',
@ -278,8 +278,10 @@ abstract class CommentResourceTestBase extends EntityResourceTestBase {
// DX: 422 when missing 'entity_type' field.
$request_options[RequestOptions::BODY] = $this->serializer->encode(array_diff_key($this->getNormalizedPostEntity(), ['entity_type' => TRUE]), static::$format);
$response = $this->request('POST', $url, $request_options);
// @todo Uncomment, remove next line in https://www.drupal.org/node/2820364.
$this->assertResourceErrorResponse(500, 'A fatal error occurred: Internal Server Error', $response);
// @todo Uncomment, remove next 3 lines in https://www.drupal.org/node/2820364.
$this->assertSame(500, $response->getStatusCode());
$this->assertSame(['application/json'], $response->getHeader('Content-Type'));
$this->assertSame('{"message":"A fatal error occurred: Internal Server Error"}', (string) $response->getBody());
//$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nentity_type: This value should not be null.\n", $response);
// DX: 422 when missing 'entity_id' field.
@ -301,9 +303,29 @@ abstract class CommentResourceTestBase extends EntityResourceTestBase {
// DX: 422 when missing 'entity_type' field.
$request_options[RequestOptions::BODY] = $this->serializer->encode(array_diff_key($this->getNormalizedPostEntity(), ['field_name' => TRUE]), static::$format);
$response = $this->request('POST', $url, $request_options);
// @todo Uncomment, remove next line in https://www.drupal.org/node/2820364.
$this->assertResourceErrorResponse(500, 'A fatal error occurred: Field is unknown.', $response);
// @todo Uncomment, remove next 3 lines in https://www.drupal.org/node/2820364.
$this->assertSame(500, $response->getStatusCode());
$this->assertSame(['application/json'], $response->getHeader('Content-Type'));
$this->assertSame('{"message":"A fatal error occurred: Field is unknown."}', (string) $response->getBody());
//$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nfield_name: This value should not be null.\n", $response);
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
switch ($method) {
case 'GET';
return "The 'access comments' permission is required and the comment must be published.";
case 'POST';
return "The 'post comments' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\CommentType;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class CommentTypeJsonAnonTest extends CommentTypeResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\CommentType;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class CommentTypeJsonBasicAuthTest extends CommentTypeResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\CommentType;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class CommentTypeJsonCookieTest extends CommentTypeResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,77 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\CommentType;
use Drupal\comment\Entity\CommentType;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
* ResourceTestBase for CommentType entity.
*/
abstract class CommentTypeResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node', 'comment'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'comment_type';
/**
* The CommentType entity.
*
* @var \Drupal\comment\CommentTypeInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer comment types']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "Camelids" comment type.
$camelids = CommentType::create([
'id' => 'camelids',
'label' => 'Camelids',
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
'target_entity_type_id' => 'node',
]);
$camelids->save();
return $camelids;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'dependencies' => [],
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
'id' => 'camelids',
'label' => 'Camelids',
'langcode' => 'en',
'status' => TRUE,
'target_entity_type_id' => 'node',
'uuid' => $this->entity->uuid(),
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
}

View file

@ -21,9 +21,4 @@ class ConfigTestJsonAnonTest extends ConfigTestResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
}

View file

@ -3,7 +3,6 @@
namespace Drupal\Tests\rest\Functional\EntityResource\ConfigTest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\JsonBasicAuthWorkaroundFor2805281Trait;
/**
* @group rest
@ -27,19 +26,9 @@ class ConfigTestJsonBasicAuthTest extends ConfigTestResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
// @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage.
use JsonBasicAuthWorkaroundFor2805281Trait {
JsonBasicAuthWorkaroundFor2805281Trait::assertResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait;
}
}

View file

@ -21,11 +21,6 @@ class ConfigTestJsonCookieTest extends ConfigTestResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\ConfigurableLanguage;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class ConfigurableLanguageJsonAnonTest extends ConfigurableLanguageResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\ConfigurableLanguage;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ConfigurableLanguageJsonBasicAuthTest extends ConfigurableLanguageResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\ConfigurableLanguage;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class ConfigurableLanguageJsonCookieTest extends ConfigurableLanguageResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,77 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\ConfigurableLanguage;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\language\Entity\ConfigurableLanguage;
abstract class ConfigurableLanguageResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['language'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'configurable_language';
/**
* @var \Drupal\language\ConfigurableLanguageInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer languages']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$configurable_language = ConfigurableLanguage::create([
'id' => 'll',
'label' => 'Llama Language',
]);
$configurable_language->save();
return $configurable_language;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'dependencies' => [],
'direction' => 'ltr',
'id' => 'll',
'label' => 'Llama Language',
'langcode' => 'en',
'locked' => FALSE,
'status' => TRUE,
'uuid' => $this->entity->uuid(),
'weight' => 0,
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return Cache::mergeContexts(parent::getExpectedCacheContexts(), ['languages:language_interface']);
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
}

View file

@ -5,7 +5,6 @@ namespace Drupal\Tests\rest\Functional\EntityResource;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Url;
use Drupal\field\Entity\FieldConfig;
@ -43,10 +42,9 @@ use Psr\Http\Message\ResponseInterface;
* (permissions or perhaps custom access control handling, such as node
* grants), plus
* 2. a concrete subclass extending the abstract entity type-specific subclass
* that specifies the exact @code $format @endcode, @code $mimeType @endcode,
* @code $expectedErrorMimeType @endcode and @code $auth @endcode for this
* concrete test. Usually that's all that's necessary: most concrete
* subclasses will be very thin.
* that specifies the exact @code $format @endcode, @code $mimeType @endcode
* and @code $auth @endcode for this concrete test. Usually that's all that's
* necessary: most concrete subclasses will be very thin.
*
* For every of these concrete subclasses, a comprehensive test scenario will
* run per HTTP method:
@ -111,11 +109,6 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
*/
protected static $secondCreatedEntityId = 3;
/**
* @var \GuzzleHttp\ClientInterface
*/
protected $httpClient;
/**
* The main entity used for testing.
*
@ -138,13 +131,13 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
public static $modules = ['rest_test', 'text'];
/**
* {@inheritdoc}
* Provides an entity resource.
*/
protected function provisionEntityResource() {
// It's possible to not have any authentication providers enabled, when
// testing public (anonymous) usage of a REST resource.
$auth = isset(static::$auth) ? [static::$auth] : [];
$this->provisionResource('entity.' . static::$entityTypeId, [static::$format], $auth);
$this->provisionResource([static::$format], $auth);
}
/**
@ -153,14 +146,13 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
public function setUp() {
parent::setUp();
// Calculate REST Resource config entity ID.
static::$resourceConfigId = 'entity.' . static::$entityTypeId;
$this->serializer = $this->container->get('serializer');
$this->entityStorage = $this->container->get('entity_type.manager')
->getStorage(static::$entityTypeId);
// Set up a HTTP client that accepts relative URLs.
$this->httpClient = $this->container->get('http_client_factory')
->fromOptions(['base_uri' => $this->baseUrl]);
// Create an entity.
$this->entity = $this->createEntity();
@ -187,18 +179,8 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// Set a default value on the field.
$this->entity->set('field_rest_test', ['value' => 'All the faith he had had had had no effect on the outcome of his life.']);
// @todo Remove in this if-test in https://www.drupal.org/node/2808335.
if ($this->entity instanceof EntityChangedInterface) {
$changed = $this->entity->getChangedTime();
$this->entity->setChangedTime(42);
$this->entity->save();
$this->entity->setChangedTime($changed);
}
$this->entity->save();
}
// @todo Remove this in https://www.drupal.org/node/2815845.
drupal_flush_all_caches();
}
/**
@ -241,6 +223,43 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
return $this->getNormalizedPostEntity();
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return $this->getExpectedBCUnauthorizedAccessMessage($method);
}
$permission = $this->entity->getEntityType()->getAdminPermission();
if ($permission !== FALSE) {
return "The '{$permission}' permission is required.";
}
$http_method_to_entity_operation = [
'GET' => 'view',
'POST' => 'create',
'PATCH' => 'update',
'DELETE' => 'delete',
];
$operation = $http_method_to_entity_operation[$method];
$message = sprintf('You are not authorized to %s this %s entity', $operation, $this->entity->getEntityTypeId());
if ($this->entity->bundle() !== $this->entity->getEntityTypeId()) {
$message .= ' of bundle ' . $this->entity->bundle();
}
return "$message.";
}
/**
* {@inheritdoc}
*/
protected function getExpectedBcUnauthorizedAccessMessage($method) {
return "The 'restful " . strtolower($method) . " entity:" . $this->entity->getEntityTypeId() . "' permission is required.";
}
/**
* The expected cache tags for the GET/HEAD response of the test entity.
*
@ -255,6 +274,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
if (!static::$auth) {
$expected_cache_tags[] = 'config:user.role.anonymous';
}
$expected_cache_tags[] = 'http_response';
return Cache::mergeTags($expected_cache_tags, $this->entity->getCacheTags());
}
@ -267,6 +287,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
*/
protected function getExpectedCacheContexts() {
return [
'url.site',
'user.permissions',
];
}
@ -301,7 +322,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// response because ?_format query string is present.
$response = $this->request('GET', $url, $request_options);
if ($has_canonical_url) {
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
}
else {
$this->assertResourceErrorResponse(404, 'No route found for "GET ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response);
@ -335,13 +356,28 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$this->assertResponseWhenMissingAuthentication($response);
}
$request_options[RequestOptions::HEADERS]['REST-test-auth'] = '1';
// DX: 403 when attempting to use unallowed authentication provider.
$response = $this->request('GET', $url, $request_options);
$this->assertResourceErrorResponse(403, 'The used authentication method is not allowed on this route.', $response);
unset($request_options[RequestOptions::HEADERS]['REST-test-auth']);
$request_options[RequestOptions::HEADERS]['REST-test-auth-global'] = '1';
// DX: 403 when attempting to use unallowed global authentication provider.
$response = $this->request('GET', $url, $request_options);
$this->assertResourceErrorResponse(403, 'The used authentication method is not allowed on this route.', $response);
unset($request_options[RequestOptions::HEADERS]['REST-test-auth-global']);
$request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions('GET'));
// DX: 403 when unauthorized.
$response = $this->request('GET', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
$this->assertArrayNotHasKey('Link', $response->getHeaders());
$this->setUpAuthorization('GET');
@ -371,15 +407,38 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$this->assertEquals($this->getExpectedCacheTags(), empty($cache_tags_header_value) ? [] : explode(' ', $cache_tags_header_value));
$cache_contexts_header_value = $response->getHeader('X-Drupal-Cache-Contexts')[0];
$this->assertEquals($this->getExpectedCacheContexts(), empty($cache_contexts_header_value) ? [] : explode(' ', $cache_contexts_header_value));
// Comparing the exact serialization is pointless, because the order of
// fields does not matter (at least not yet). That's why we only compare the
// normalized entity with the decoded response: it's comparing PHP arrays
// instead of strings.
$this->assertEquals($this->getExpectedNormalizedEntity(), $this->serializer->decode((string) $response->getBody(), static::$format));
// Sort the serialization data first so we can do an identical comparison
// for the keys with the array order the same (it needs to match with
// identical comparison).
$expected = $this->getExpectedNormalizedEntity();
ksort($expected);
$actual = $this->serializer->decode((string) $response->getBody(), static::$format);
ksort($actual);
$this->assertSame($expected, $actual);
// Not only assert the normalization, also assert deserialization of the
// response results in the expected object.
$unserialized = $this->serializer->deserialize((string) $response->getBody(), get_class($this->entity), static::$format);
$this->assertSame($unserialized->uuid(), $this->entity->uuid());
// Finally, assert that the expected 'Link' headers are present.
if ($this->entity->getEntityType()->getLinkTemplates()) {
$this->assertArrayHasKey('Link', $response->getHeaders());
$link_relation_type_manager = $this->container->get('plugin.manager.link_relation_type');
$expected_link_relation_headers = array_map(function ($rel) use ($link_relation_type_manager) {
$definition = $link_relation_type_manager->getDefinition($rel, FALSE);
return (!empty($definition['uri']))
? $definition['uri']
: $rel;
}, array_keys($this->entity->getEntityType()->getLinkTemplates()));
$parse_rel_from_link_header = function ($value) use ($link_relation_type_manager) {
$matches = [];
if (preg_match('/rel="([^"]+)"/', $value, $matches) === 1) {
return $matches[1];
}
return FALSE;
};
$this->assertSame($expected_link_relation_headers, array_map($parse_rel_from_link_header, $response->getHeader('Link')));
}
$get_headers = $response->getHeaders();
// Verify that the GET and HEAD responses are the same. The only difference
@ -394,16 +453,42 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
}
$this->assertSame($get_headers, $head_headers);
// Only run this for fieldable entities. It doesn't make sense for config
// entities as config values are already casted. They also run through the
// ConfigEntityNormalizer, which doesn't deal with fields individually.
if ($this->entity instanceof FieldableEntityInterface) {
$this->config('serialization.settings')->set('bc_primitives_as_strings', TRUE)->save(TRUE);
// Rebuild the container so new config is reflected in the removal of the
// PrimitiveDataNormalizer.
$this->rebuildAll();
$response = $this->request('GET', $url, $request_options);
$this->assertResourceResponse(200, FALSE, $response);
// Again do an identical comparison, but this time transform the expected
// normalized entity's values to strings. This ensures the BC layer for
// bc_primitives_as_strings works as expected.
$expected = $this->getExpectedNormalizedEntity();
// Config entities are not affected.
// @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize()
$expected = static::castToString($expected);
ksort($expected);
$actual = $this->serializer->decode((string) $response->getBody(), static::$format);
ksort($actual);
$this->assertSame($expected, $actual);
}
// BC: rest_update_8203().
$this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
// @todo Remove this in https://www.drupal.org/node/2815845.
drupal_flush_all_caches();
$this->refreshTestStateAfterRestConfigChange();
// DX: 403 when unauthorized.
$response = $this->request('GET', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('GET'), $response);
$this->grantPermissionsToTestedRole(['restful get entity:' . static::$entityTypeId]);
@ -414,13 +499,39 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$this->assertResourceResponse(200, FALSE, $response);
$this->resourceConfigStorage->load(static::$resourceConfigId)->disable()->save();
$this->refreshTestStateAfterRestConfigChange();
// DX: upon disabling a resource, it's immediately no longer available.
$this->assertResourceNotAvailable($url, $request_options);
$this->resourceConfigStorage->load(static::$resourceConfigId)->enable()->save();
$this->refreshTestStateAfterRestConfigChange();
// DX: upon re-enabling a resource, immediate 200.
$response = $this->request('GET', $url, $request_options);
$this->assertResourceResponse(200, FALSE, $response);
$this->resourceConfigStorage->load(static::$resourceConfigId)->delete();
$this->refreshTestStateAfterRestConfigChange();
// DX: upon deleting a resource, it's immediately no longer available.
$this->assertResourceNotAvailable($url, $request_options);
$this->provisionEntityResource();
$url->setOption('query', ['_format' => 'non_existing_format']);
// DX: 406 when requesting unsupported format.
$response = $this->request('GET', $url, $request_options);
$this->assert406Response($response);
$this->assertNotSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type'));
$this->assertNotSame([static::$mimeType], $response->getHeader('Content-Type'));
$request_options[RequestOptions::HEADERS]['Accept'] = static::$mimeType;
@ -430,7 +541,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// @todo Update in https://www.drupal.org/node/2825347.
$response = $this->request('GET', $url, $request_options);
$this->assert406Response($response);
$this->assertSame([static::$expectedErrorMimeType], $response->getHeader('Content-Type'));
$this->assertSame(['application/json'], $response->getHeader('Content-Type'));
$url = Url::fromRoute('rest.entity.' . static::$entityTypeId . '.GET.' . static::$format);
@ -445,6 +556,30 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$this->assertResourceErrorResponse(404, $message, $response);
}
/**
* Transforms a normalization: casts all non-string types to strings.
*
* @param array $normalization
* A normalization to transform.
*
* @return array
* The transformed normalization.
*/
protected static function castToString(array $normalization) {
foreach ($normalization as $key => $value) {
if (is_bool($value)) {
$normalization[$key] = (string) (int) $value;
}
elseif (is_int($value) || is_float($value)) {
$normalization[$key] = (string) $value;
}
elseif (is_array($value)) {
$normalization[$key] = static::castToString($value);
}
}
return $normalization;
}
/**
* Tests a POST request for an entity, plus edge cases to ensure good DX.
*/
@ -463,8 +598,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$parseable_valid_request_body = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format);
$parseable_valid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity(), static::$format);
$parseable_invalid_request_body = $this->serializer->encode($this->makeNormalizationInvalid($this->getNormalizedPostEntity()), static::$format);
// @todo Change to ['uuid' => UUID] in https://www.drupal.org/node/2820743.
$parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [['value' => $this->randomMachineName(129)]]], static::$format);
$parseable_invalid_request_body_2 = $this->serializer->encode($this->getNormalizedPostEntity() + ['uuid' => [$this->randomMachineName(129)]], static::$format);
$parseable_invalid_request_body_3 = $this->serializer->encode($this->getNormalizedPostEntity() + ['field_rest_test' => [['value' => $this->randomString()]]], static::$format);
// The URL and Guzzle request options that will be used in this test. The
@ -476,15 +610,11 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$request_options = [];
// DX: 404 when resource not provisioned, but HTML if canonical route.
// DX: 404 when resource not provisioned. HTML response because missing
// ?_format query string.
$response = $this->request('POST', $url, $request_options);
if ($has_canonical_url) {
$this->assertSame(404, $response->getStatusCode());
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
}
else {
$this->assertResourceErrorResponse(404, 'No route found for "GET ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response);
}
$this->assertSame(404, $response->getStatusCode());
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
$url->setOption('query', ['_format' => static::$format]);
@ -500,16 +630,12 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$url->setOption('query', []);
// DX: 415 when no Content-Type request header, but HTML if canonical route.
// DX: 415 when no Content-Type request header. HTML response because
// missing ?_format query string.
$response = $this->request('POST', $url, $request_options);
if ($has_canonical_url) {
$this->assertSame(415, $response->getStatusCode());
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
$this->assertContains(htmlspecialchars('No "Content-Type" request header specified'), (string) $response->getBody());
}
else {
$this->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response);
}
$this->assertSame(415, $response->getStatusCode());
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
$this->assertContains(htmlspecialchars('No "Content-Type" request header specified'), (string) $response->getBody());
$url->setOption('query', ['_format' => static::$format]);
@ -533,12 +659,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 400 when unparseable request body.
$response = $this->request('POST', $url, $request_options);
// @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813853.
// $this->assertResourceErrorResponse(400, 'Syntax error', $response);
$this->assertSame(400, $response->getStatusCode());
$this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
$this->assertSame($this->serializer->encode(['error' => 'Syntax error'], static::$format), (string) $response->getBody());
$this->assertResourceErrorResponse(400, 'Syntax error', $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body;
@ -557,8 +678,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 403 when unauthorized.
$response = $this->request('POST', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
$this->setUpAuthorization('POST');
@ -567,24 +687,19 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 422 when invalid entity: multiple values sent for single-value field.
$response = $this->request('POST', $url, $request_options);
$label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
$label_field_capitalized = ucfirst($label_field);
// @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813755.
// $this->assertErrorResponse(422, "Unprocessable Entity: validation failed.\ntitle: <em class=\"placeholder\">Title</em>: this field cannot hold more than 1 values.\n", $response);
$this->assertSame(422, $response->getStatusCode());
$this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
$this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\n$label_field: <em class=\"placeholder\">$label_field_capitalized</em>: this field cannot hold more than 1 values.\n"], static::$format), (string) $response->getBody());
$label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
// DX: 422 when invalid entity: UUID field too long.
$response = $this->request('POST', $url, $request_options);
// @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813755.
// $this->assertErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n", $response);
$this->assertSame(422, $response->getStatusCode());
$this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
$this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n"], static::$format), (string) $response->getBody());
// @todo Fix this in https://www.drupal.org/node/2149851.
if ($this->entity->getEntityType()->hasKey('uuid')) {
$response = $this->request('POST', $url, $request_options);
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
@ -592,8 +707,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 403 when entity contains field without 'edit' access.
$response = $this->request('POST', $url, $request_options);
// @todo Add trailing period in https://www.drupal.org/node/2821013.
$this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'", $response);
$this->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'.", $response);
$request_options[RequestOptions::BODY] = $parseable_valid_request_body;
@ -622,20 +736,24 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// 201 for well-formed request.
$response = $this->request('POST', $url, $request_options);
$this->assertResourceResponse(201, FALSE, $response);
$this->assertSame([str_replace($this->entity->id(), static::$firstCreatedEntityId, $this->entity->toUrl('canonical')->setAbsolute(TRUE)->toString())], $response->getHeader('Location'));
if ($has_canonical_url) {
$location = $this->entityStorage->load(static::$firstCreatedEntityId)->toUrl('canonical')->setAbsolute(TRUE)->toString();
$this->assertSame([$location], $response->getHeader('Location'));
}
else {
$this->assertSame([], $response->getHeader('Location'));
}
$this->assertFalse($response->hasHeader('X-Drupal-Cache'));
$this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
$this->refreshTestStateAfterRestConfigChange();
$request_options[RequestOptions::BODY] = $parseable_valid_request_body_2;
// @todo Remove this in https://www.drupal.org/node/2815845.
drupal_flush_all_caches();
// DX: 403 when unauthorized.
$response = $this->request('POST', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('POST'), $response);
$this->grantPermissionsToTestedRole(['restful post entity:' . static::$entityTypeId]);
@ -644,7 +762,13 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// 201 for well-formed request.
$response = $this->request('POST', $url, $request_options);
$this->assertResourceResponse(201, FALSE, $response);
$this->assertSame([str_replace($this->entity->id(), static::$secondCreatedEntityId, $this->entity->toUrl('canonical')->setAbsolute(TRUE)->toString())], $response->getHeader('Location'));
if ($has_canonical_url) {
$location = $this->entityStorage->load(static::$secondCreatedEntityId)->toUrl('canonical')->setAbsolute(TRUE)->toString();
$this->assertSame([$location], $response->getHeader('Location'));
}
else {
$this->assertSame([], $response->getHeader('Location'));
}
$this->assertFalse($response->hasHeader('X-Drupal-Cache'));
}
@ -677,23 +801,31 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$request_options = [];
// DX: 405 when resource not provisioned, but HTML if canonical route.
// DX: 404 when resource not provisioned, 405 if canonical route. Plain text
// or HTML response because missing ?_format query string.
$response = $this->request('PATCH', $url, $request_options);
if ($has_canonical_url) {
$this->assertSame(405, $response->getStatusCode());
$this->assertSame(['GET, POST, HEAD'], $response->getHeader('Allow'));
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
}
else {
$this->assertResourceErrorResponse(404, 'No route found for "PATCH ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response);
$this->assertSame(404, $response->getStatusCode());
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
}
$url->setOption('query', ['_format' => static::$format]);
// DX: 405 when resource not provisioned.
// DX: 404 when resource not provisioned, 405 if canonical route.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(405, 'No route found for "PATCH ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '": Method Not Allowed (Allow: GET, POST, HEAD)', $response);
if ($has_canonical_url) {
$this->assertResourceErrorResponse(405, 'No route found for "PATCH ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '": Method Not Allowed (Allow: GET, POST, HEAD)', $response);
}
else {
$this->assertResourceErrorResponse(404, 'No route found for "PATCH ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response);
}
$this->provisionEntityResource();
@ -701,16 +833,11 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$url->setOption('query', []);
// DX: 415 when no Content-Type request header, but HTML if canonical route.
// DX: 415 when no Content-Type request header.
$response = $this->request('PATCH', $url, $request_options);
if ($has_canonical_url) {
$this->assertSame(415, $response->getStatusCode());
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
$this->assertTrue(FALSE !== strpos((string) $response->getBody(), htmlspecialchars('No "Content-Type" request header specified')));
}
else {
$this->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response);
}
$this->assertSame(415, $response->getStatusCode());
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
$this->assertTrue(FALSE !== strpos((string) $response->getBody(), htmlspecialchars('No "Content-Type" request header specified')));
$url->setOption('query', ['_format' => static::$format]);
@ -734,11 +861,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 400 when unparseable request body.
$response = $this->request('PATCH', $url, $request_options);
// @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813853.
// $this->assertResourceErrorResponse(400, 'Syntax error', $response);
$this->assertSame(400, $response->getStatusCode());
$this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
$this->assertSame($this->serializer->encode(['error' => 'Syntax error'], static::$format), (string) $response->getBody());
$this->assertResourceErrorResponse(400, 'Syntax error', $response);
@ -758,8 +881,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 403 when unauthorized.
$response = $this->request('PATCH', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('PATCH'), $response);
$this->setUpAuthorization('PATCH');
@ -768,12 +890,8 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 422 when invalid entity: multiple values sent for single-value field.
$response = $this->request('PATCH', $url, $request_options);
$label_field = $this->entity->getEntityType()->hasKey('label') ? $this->entity->getEntityType()->getKey('label') : static::$labelFieldName;
$label_field_capitalized = ucfirst($label_field);
// @todo Uncomment, remove next 3 in https://www.drupal.org/node/2813755.
// $this->assertErrorResponse(422, "Unprocessable Entity: validation failed.\ntitle: <em class=\"placeholder\">Title</em>: this field cannot hold more than 1 values.\n", $response);
// $this->assertSame(422, $response->getStatusCode());
// $this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
$this->assertSame($this->serializer->encode(['message' => "Unprocessable Entity: validation failed.\n$label_field: <em class=\"placeholder\">$label_field_capitalized</em>: this field cannot hold more than 1 values.\n"], static::$format), (string) $response->getBody());
$label_field_capitalized = $this->entity->getFieldDefinition($label_field)->getLabel();
$this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n$label_field: $label_field_capitalized: this field cannot hold more than 1 values.\n", $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;
@ -838,15 +956,13 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
$this->refreshTestStateAfterRestConfigChange();
$request_options[RequestOptions::BODY] = $parseable_valid_request_body_2;
// @todo Remove this in https://www.drupal.org/node/2815845.
drupal_flush_all_caches();
// DX: 403 when unauthorized.
$response = $this->request('PATCH', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('PATCH'), $response);
$this->grantPermissionsToTestedRole(['restful patch entity:' . static::$entityTypeId]);
@ -880,24 +996,32 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
$request_options = [];
// DX: 405 when resource not provisioned, but HTML if canonical route.
// DX: 405 when resource not provisioned, but HTML if canonical route. Plain
// text or HTML response because missing ?_format query string.
$response = $this->request('DELETE', $url, $request_options);
if ($has_canonical_url) {
$this->assertSame(405, $response->getStatusCode());
$this->assertSame(['GET, POST, HEAD'], $response->getHeader('Allow'));
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
}
else {
$this->assertResourceErrorResponse(404, 'No route found for "DELETE ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response);
$this->assertSame(404, $response->getStatusCode());
$this->assertSame(['text/html; charset=UTF-8'], $response->getHeader('Content-Type'));
}
$url->setOption('query', ['_format' => static::$format]);
// DX: 405 when resource not provisioned.
// DX: 404 when resource not provisioned, 405 if canonical route.
$response = $this->request('DELETE', $url, $request_options);
$this->assertResourceErrorResponse(405, 'No route found for "DELETE ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '": Method Not Allowed (Allow: GET, POST, HEAD)', $response);
if ($has_canonical_url) {
$this->assertSame(['GET, POST, HEAD'], $response->getHeader('Allow'));
$this->assertResourceErrorResponse(405, 'No route found for "DELETE ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '": Method Not Allowed (Allow: GET, POST, HEAD)', $response);
}
else {
$this->assertResourceErrorResponse(404, 'No route found for "DELETE ' . str_replace($this->baseUrl, '', $this->getUrl()->setAbsolute()->toString()) . '"', $response);
}
$this->provisionEntityResource();
@ -915,8 +1039,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 403 when unauthorized.
$response = $this->request('DELETE', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('DELETE'), $response);
$this->setUpAuthorization('DELETE');
@ -930,23 +1053,24 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// 204 for well-formed request.
$response = $this->request('DELETE', $url, $request_options);
$this->assertSame(204, $response->getStatusCode());
// @todo Uncomment the following line when https://www.drupal.org/node/2821711 is fixed.
// DELETE responses should not include a Content-Type header. But Apache
// sets it to 'text/html' by default. We also cannot detect the presence of
// Apache either here in the CLI. For now having this documented here is all
// we can do.
// $this->assertSame(FALSE, $response->hasHeader('Content-Type'));
$this->assertSame('', (string) $response->getBody());
$this->assertFalse($response->hasHeader('X-Drupal-Cache'));
$this->config('rest.settings')->set('bc_entity_resource_permissions', TRUE)->save(TRUE);
// @todo Remove this in https://www.drupal.org/node/2815845.
drupal_flush_all_caches();
$this->refreshTestStateAfterRestConfigChange();
$this->entity = $this->createEntity();
$url = $this->getUrl()->setOption('query', $url->getOption('query'));
// DX: 403 when unauthorized.
$response = $this->request('DELETE', $url, $request_options);
// @todo Update the message in https://www.drupal.org/node/2808233.
$this->assertResourceErrorResponse(403, '', $response);
$this->assertResourceErrorResponse(403, $this->getExpectedUnauthorizedAccessMessage('DELETE'), $response);
$this->grantPermissionsToTestedRole(['restful delete entity:' . static::$entityTypeId]);
@ -982,11 +1106,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 400 when incorrect entity type bundle is specified.
// @todo Change to 422 in https://www.drupal.org/node/2827084.
$response = $this->request($method, $url, $request_options);
// @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813853 lands.
// $this->assertResourceErrorResponse(400, '"bad_bundle_name" is not a valid bundle type for denormalization.', $response);
$this->assertSame(400, $response->getStatusCode());
$this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
$this->assertSame($this->serializer->encode(['error' => '"bad_bundle_name" is not a valid bundle type for denormalization.'], static::$format), (string) $response->getBody());
$this->assertResourceErrorResponse(400, '"bad_bundle_name" is not a valid bundle type for denormalization.', $response);
}
@ -997,11 +1117,7 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
// DX: 400 when no entity type bundle is specified.
// @todo Change to 422 in https://www.drupal.org/node/2827084.
$response = $this->request($method, $url, $request_options);
// @todo use this commented line instead of the 3 lines thereafter once https://www.drupal.org/node/2813853 lands.
// $this->assertResourceErrorResponse(400, 'A string must be provided as a bundle value.', $response);
$this->assertSame(400, $response->getStatusCode());
$this->assertSame([static::$mimeType], $response->getHeader('Content-Type'));
$this->assertSame($this->serializer->encode(['error' => 'A string must be provided as a bundle value.'], static::$format), (string) $response->getBody());
$this->assertResourceErrorResponse(400, sprintf('Could not determine entity type bundle: "%s" field is missing.', $bundle_field_name), $response);
}
}
@ -1090,4 +1206,23 @@ abstract class EntityResourceTestBase extends ResourceTestBase {
}
}
/**
* Asserts that a resource is unavailable: 404, 406 if it has canonical route.
*
* @param \Drupal\Core\Url $url
* URL to request.
* @param array $request_options
* Request options to apply.
*/
protected function assertResourceNotAvailable(Url $url, array $request_options) {
$has_canonical_url = $this->entity->hasLinkTemplate('canonical');
$response = $this->request('GET', $url, $request_options);
if (!$has_canonical_url) {
$this->assertSame(404, $response->getStatusCode());
}
else {
$this->assert406Response($response);
}
}
}

View file

@ -21,9 +21,4 @@ class EntityTestJsonAnonTest extends EntityTestResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
}

View file

@ -3,7 +3,6 @@
namespace Drupal\Tests\rest\Functional\EntityResource\EntityTest;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\JsonBasicAuthWorkaroundFor2805281Trait;
/**
* @group rest
@ -27,19 +26,9 @@ class EntityTestJsonBasicAuthTest extends EntityTestResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
// @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage.
use JsonBasicAuthWorkaroundFor2805281Trait {
JsonBasicAuthWorkaroundFor2805281Trait::assertResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait;
}
}

View file

@ -21,11 +21,6 @@ class EntityTestJsonCookieTest extends EntityTestResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/

View file

@ -73,7 +73,7 @@ abstract class EntityTestResourceTestBase extends EntityResourceTestBase {
],
'id' => [
[
'value' => '1',
'value' => 1,
],
],
'langcode' => [
@ -93,12 +93,12 @@ abstract class EntityTestResourceTestBase extends EntityResourceTestBase {
],
'created' => [
[
'value' => $this->entity->get('created')->value,
'value' => (int) $this->entity->get('created')->value,
]
],
'user_id' => [
[
'target_id' => $author->id(),
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => $author->toUrl()->toString(),
@ -124,4 +124,22 @@ abstract class EntityTestResourceTestBase extends EntityResourceTestBase {
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
switch ($method) {
case 'GET':
return "The 'view test entity' permission is required.";
case 'POST':
return "The following permissions are required: 'administer entity_test content' OR 'administer entity_test_with_bundle content' OR 'create entity_test entity_test_with_bundle entities'.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\EntityTestLabel;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class EntityTestLabelJsonAnonTest extends EntityTestLabelResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\EntityTestLabel;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class EntityTestLabelJsonBasicAuthTest extends EntityTestLabelResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\EntityTestLabel;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class EntityTestLabelJsonCookieTest extends EntityTestLabelResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,156 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\EntityTestLabel;
use Drupal\entity_test\Entity\EntityTestLabel;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
use Drupal\user\Entity\User;
abstract class EntityTestLabelResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['entity_test'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'entity_test_label';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [];
/**
* @var \Drupal\entity_test\Entity\EntityTestLabel
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
switch ($method) {
case 'GET':
$this->grantPermissionsToTestedRole(['view test entity']);
break;
case 'POST':
$this->grantPermissionsToTestedRole([
'administer entity_test content',
'administer entity_test_with_bundle content',
'create entity_test entity_test_with_bundle entities',
]);
break;
case 'PATCH':
case 'DELETE':
$this->grantPermissionsToTestedRole(['administer entity_test content']);
break;
}
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$entity_test_label = EntityTestLabel::create([
'name' => 'label_llama',
]);
$entity_test_label->setOwnerId(0);
$entity_test_label->save();
return $entity_test_label;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$author = User::load(0);
$normalization = [
'uuid' => [
[
'value' => $this->entity->uuid(),
],
],
'id' => [
[
'value' => (int) $this->entity->id(),
],
],
'langcode' => [
[
'value' => 'en',
],
],
'type' => [
[
'value' => 'entity_test_label',
],
],
'name' => [
[
'value' => 'label_llama',
],
],
'created' => [
[
'value' => (int) $this->entity->get('created')->value,
],
],
'user_id' => [
[
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => $author->toUrl()->toString(),
],
],
];
return $normalization;
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return [
'type' => 'entity_test_label',
'name' => [
[
'value' => 'label_llama',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return ['user.permissions'];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
switch ($method) {
case 'GET':
return "The 'view test entity' permission is required.";
case 'POST':
return "The following permissions are required: 'administer entity_test content' OR 'administer entity_test_with_bundle content' OR 'create entity_test_label entity_test_with_bundle entities'.";
case 'PATCH':
case 'DELETE':
return "The 'administer entity_test content' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\FilterFormat;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class FilterFormatJsonAnonTest extends FilterFormatResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\FilterFormat;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class FilterFormatJsonBasicAuthTest extends FilterFormatResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\FilterFormat;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class FilterFormatJsonCookieTest extends FilterFormatResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,88 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\FilterFormat;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
abstract class FilterFormatResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'filter_format';
/**
* @var \Drupal\filter\FilterFormatInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer filters']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$pablo_format = FilterFormat::create([
'name' => 'Pablo Piccasso',
'format' => 'pablo',
'langcode' => 'es',
'filters' => [
'filter_html' => [
'status' => TRUE,
'settings' => [
'allowed_html' => '<p> <a> <b> <lo>',
],
],
],
]);
$pablo_format->save();
return $pablo_format;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'dependencies' => [],
'filters' => [
'filter_html' => [
'id' => 'filter_html',
'provider' => 'filter',
'status' => TRUE,
'weight' => -10,
'settings' => [
'allowed_html' => '<p> <a> <b> <lo>',
'filter_html_help' => TRUE,
'filter_html_nofollow' => FALSE,
],
],
],
'format' => 'pablo',
'langcode' => 'es',
'name' => 'Pablo Piccasso',
'status' => TRUE,
'uuid' => $this->entity->uuid(),
'weight' => 0,
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\ImageStyle;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class ImageStyleJsonAnonTest extends ImageStyleResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\ImageStyle;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ImageStyleJsonBasicAuthTest extends ImageStyleResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\ImageStyle;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class ImageStyleJsonCookieTest extends ImageStyleResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,113 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\ImageStyle;
use Drupal\image\Entity\ImageStyle;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
* ResourceTestBase for ImageStyle entity.
*/
abstract class ImageStyleResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['image'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'image_style';
/**
* The ImageStyle entity.
*
* @var \Drupal\image\ImageStyleInterface
*/
protected $entity;
/**
* The effect UUID.
*
* @var string
*/
protected $effectUuid;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer image styles']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "Camelids" image style.
$camelids = ImageStyle::create([
'name' => 'camelids',
'label' => 'Camelids',
]);
// Add an image effect.
$effect = [
'id' => 'image_scale_and_crop',
'data' => [
'width' => 120,
'height' => 121,
],
'weight' => 0,
];
$this->effectUuid = $camelids->addImageEffect($effect);
$camelids->save();
return $camelids;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'dependencies' => [],
'effects' => [
$this->effectUuid => [
'uuid' => $this->effectUuid,
'id' => 'image_scale_and_crop',
'weight' => 0,
'data' => [
'width' => 120,
'height' => 121,
],
],
],
'label' => 'Camelids',
'langcode' => 'en',
'name' => 'camelids',
'status' => TRUE,
'uuid' => $this->entity->uuid(),
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
return "The 'administer image styles' permission is required.";
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\Item;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class ItemJsonAnonTest extends ItemResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\Item;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class ItemJsonBasicAuthTest extends ItemResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\Item;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class ItemJsonCookieTest extends ItemResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,177 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\Item;
use Drupal\aggregator\Entity\Feed;
use Drupal\aggregator\Entity\Item;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
* ResourceTestBase for Item entity.
*/
abstract class ItemResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['aggregator'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'aggregator_item';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [];
/**
* The Item entity.
*
* @var \Drupal\aggregator\ItemInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
switch ($method) {
case 'GET':
$this->grantPermissionsToTestedRole(['access news feeds']);
break;
case 'POST':
case 'PATCH':
case 'DELETE':
$this->grantPermissionsToTestedRole(['administer news feeds']);
break;
}
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "Camelids" feed.
$feed = Feed::create([
'title' => 'Camelids',
'url' => 'https://groups.drupal.org/not_used/167169',
'refresh' => 900,
'checked' => 1389919932,
'description' => 'Drupal Core Group feed',
]);
$feed->save();
// Create a "Llama" item.
$item = Item::create();
$item->setTitle('Llama')
->setFeedId($feed->id())
->setLink('https://www.drupal.org/')
->setPostedTime(123456789)
->save();
return $item;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$feed = Feed::load($this->entity->getFeedId());
return [
'iid' => [
[
'value' => 1,
],
],
'langcode' => [
[
'value' => 'en',
],
],
'fid' => [
[
'target_id' => 1,
'target_type' => 'aggregator_feed',
'target_uuid' => $feed->uuid(),
'url' => base_path() . 'aggregator/sources/1',
],
],
'title' => [
[
'value' => 'Llama',
],
],
'link' => [
[
'value' => 'https://www.drupal.org/',
],
],
'author' => [],
'description' => [],
'timestamp' => [
[
'value' => 123456789,
],
],
'guid' => [],
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return [
'fid' => [
[
'target_id' => 1,
],
],
'title' => [
[
'value' => 'Llama',
],
],
'link' => [
[
'value' => 'https://www.drupal.org/',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
// @see ::createEntity()
return ['user.permissions'];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
switch ($method) {
case 'GET':
return "The 'access news feeds' permission is required.";
case 'POST':
case 'PATCH':
case 'DELETE':
return "The 'administer news feeds' permission is required.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\MenuLinkContent;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class MenuLinkContentJsonAnonTest extends MenuLinkContentResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\MenuLinkContent;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class MenuLinkContentJsonBasicAuthTest extends MenuLinkContentResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\MenuLinkContent;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class MenuLinkContentJsonCookieTest extends MenuLinkContentResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,193 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\MenuLinkContent;
use Drupal\menu_link_content\Entity\MenuLinkContent;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
* ResourceTestBase for MenuLinkContent entity.
*/
abstract class MenuLinkContentResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['menu_link_content'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'menu_link_content';
/**
* {@inheritdoc}
*/
protected static $patchProtectedFieldNames = [
'changed',
];
/**
* The MenuLinkContent entity.
*
* @var \Drupal\menu_link_content\MenuLinkContentInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
switch ($method) {
case 'GET':
case 'POST':
case 'PATCH':
case 'DELETE':
$this->grantPermissionsToTestedRole(['administer menu']);
break;
}
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$menu_link = MenuLinkContent::create([
'id' => 'llama',
'title' => 'Llama Gabilondo',
'description' => 'Llama Gabilondo',
'link' => 'https://nl.wikipedia.org/wiki/Llama',
'weight' => 0,
'menu_name' => 'main',
]);
$menu_link->save();
return $menu_link;
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
return [
'title' => [
[
'value' => 'Dramallama',
],
],
'link' => [
[
'uri' => 'http://www.urbandictionary.com/define.php?term=drama%20llama',
],
],
'bundle' => [
[
'value' => 'menu_link_content',
],
],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'uuid' => [
[
'value' => $this->entity->uuid(),
],
],
'id' => [
[
'value' => 1,
],
],
'title' => [
[
'value' => 'Llama Gabilondo',
],
],
'link' => [
[
'uri' => 'https://nl.wikipedia.org/wiki/Llama',
'title' => NULL,
'options' => [],
],
],
'weight' => [
[
'value' => 0,
],
],
'menu_name' => [
[
'value' => 'main',
],
],
'langcode' => [
[
'value' => 'en',
],
],
'bundle' => [
[
'value' => 'menu_link_content',
],
],
'description' => [
[
'value' => 'Llama Gabilondo',
],
],
'external' => [
[
'value' => FALSE,
],
],
'rediscover' => [
[
'value' => FALSE,
],
],
'expanded' => [
[
'value' => FALSE,
],
],
'enabled' => [
[
'value' => TRUE,
],
],
'changed' => [
[
'value' => $this->entity->getChangedTime(),
],
],
'default_langcode' => [
[
'value' => TRUE,
],
],
'parent' => [],
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
switch ($method) {
case 'DELETE':
return "You are not authorized to delete this menu_link_content entity.";
default:
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}
}

View file

@ -21,9 +21,4 @@ class NodeJsonAnonTest extends NodeResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
}

View file

@ -3,7 +3,6 @@
namespace Drupal\Tests\rest\Functional\EntityResource\Node;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Drupal\Tests\rest\Functional\JsonBasicAuthWorkaroundFor2805281Trait;
/**
* @group rest
@ -27,19 +26,9 @@ class NodeJsonBasicAuthTest extends NodeResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
// @todo Fix in https://www.drupal.org/node/2805281: remove this trait usage.
use JsonBasicAuthWorkaroundFor2805281Trait {
JsonBasicAuthWorkaroundFor2805281Trait::assertResponseWhenMissingAuthentication insteadof BasicAuthResourceTestTrait;
}
}

View file

@ -21,11 +21,6 @@ class NodeJsonCookieTest extends NodeResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/

View file

@ -116,32 +116,32 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase {
],
'status' => [
[
'value' => 1,
'value' => TRUE,
],
],
'created' => [
[
'value' => '123456789',
'value' => 123456789,
],
],
'changed' => [
[
'value' => '123456789',
'value' => $this->entity->getChangedTime(),
],
],
'promote' => [
[
'value' => 1,
'value' => TRUE,
],
],
'sticky' => [
[
'value' => '0',
'value' => FALSE,
],
],
'revision_timestamp' => [
[
'value' => '123456789',
'value' => 123456789,
],
],
'revision_translation_affected' => [
@ -156,7 +156,7 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase {
],
'uid' => [
[
'target_id' => $author->id(),
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => base_path() . 'user/' . $author->id(),
@ -164,14 +164,13 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase {
],
'revision_uid' => [
[
'target_id' => $author->id(),
'target_id' => (int) $author->id(),
'target_type' => 'user',
'target_uuid' => $author->uuid(),
'url' => base_path() . 'user/' . $author->id(),
],
],
'revision_log' => [
],
'revision_log' => [],
];
}
@ -193,4 +192,18 @@ abstract class NodeResourceTestBase extends EntityResourceTestBase {
];
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
if ($method === 'GET' || $method == 'PATCH' || $method == 'DELETE') {
return "The 'access content' permission is required.";
}
return parent::getExpectedUnauthorizedAccessMessage($method);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\NodeType;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class NodeTypeJsonAnonTest extends NodeTypeResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\NodeType;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class NodeTypeJsonBasicAuthTest extends NodeTypeResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

View file

@ -0,0 +1,29 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\NodeType;
use Drupal\Tests\rest\Functional\CookieResourceTestTrait;
/**
* @group rest
*/
class NodeTypeJsonCookieTest extends NodeTypeResourceTestBase {
use CookieResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'cookie';
}

View file

@ -0,0 +1,90 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\NodeType;
use Drupal\node\Entity\NodeType;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
/**
* ResourceTestBase for NodeType entity.
*/
abstract class NodeTypeResourceTestBase extends EntityResourceTestBase {
/**
* {@inheritdoc}
*/
public static $modules = ['node'];
/**
* {@inheritdoc}
*/
protected static $entityTypeId = 'node_type';
/**
* The NodeType entity.
*
* @var \Drupal\node\NodeTypeInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
$this->grantPermissionsToTestedRole(['administer content types', 'access content']);
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
// Create a "Camelids" node type.
$camelids = NodeType::create([
'name' => 'Camelids',
'type' => 'camelids',
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
]);
$camelids->save();
return $camelids;
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
return [
'dependencies' => [],
'description' => 'Camelids are large, strictly herbivorous animals with slender necks and long legs.',
'display_submitted' => TRUE,
'help' => NULL,
'langcode' => 'en',
'name' => 'Camelids',
'new_revision' => TRUE,
'preview_mode' => 1,
'status' => TRUE,
'type' => 'camelids',
'uuid' => $this->entity->uuid(),
];
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
// @todo Update in https://www.drupal.org/node/2300677.
}
/**
* {@inheritdoc}
*/
protected function getExpectedUnauthorizedAccessMessage($method) {
if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
return parent::getExpectedUnauthorizedAccessMessage($method);
}
return "The 'access content' permission is required.";
}
}

View file

@ -21,9 +21,4 @@ class RoleJsonAnonTest extends RoleResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
}

View file

@ -3,7 +3,6 @@
namespace Drupal\Tests\rest\Functional\EntityResource\Role;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
use Psr\Http\Message\ResponseInterface;
/**
* @group rest
@ -27,22 +26,9 @@ class RoleJsonBasicAuthTest extends RoleResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
/**
* {@inheritdoc}
*/
protected function assertResponseWhenMissingAuthentication(ResponseInterface $response) {
$this->assertSame(401, $response->getStatusCode());
$this->assertSame('{"message":"A fatal error occurred: No authentication credentials provided."}', (string) $response->getBody());
}
}

View file

@ -21,11 +21,6 @@ class RoleJsonCookieTest extends RoleResourceTestBase {
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $expectedErrorMimeType = 'application/json';
/**
* {@inheritdoc}
*/

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\SearchPage;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class SearchPageJsonAnonTest extends SearchPageResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
}

View file

@ -0,0 +1,34 @@
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\SearchPage;
use Drupal\Tests\rest\Functional\BasicAuthResourceTestTrait;
/**
* @group rest
*/
class SearchPageJsonBasicAuthTest extends SearchPageResourceTestBase {
use BasicAuthResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['basic_auth'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected static $auth = 'basic_auth';
}

Some files were not shown because too many files have changed in this diff Show more