Update Composer, update everything

This commit is contained in:
Oliver Davies 2018-11-23 12:29:20 +00:00
parent ea3e94409f
commit dda5c284b6
19527 changed files with 1135420 additions and 351004 deletions

View file

@ -2,7 +2,7 @@
namespace Drupal\rest\Annotation;
use \Drupal\Component\Annotation\Plugin;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a REST resource annotation object.

View file

@ -13,6 +13,13 @@ use Drupal\rest\RestResourceConfigInterface;
* @ConfigEntityType(
* id = "rest_resource_config",
* label = @Translation("REST resource configuration"),
* label_collection = @Translation("REST resource configurations"),
* label_singular = @Translation("REST resource configuration"),
* label_plural = @Translation("REST resource configurations"),
* label_count = @PluralTranslation(
* singular = "@count REST resource configuration",
* plural = "@count REST resource configurations",
* ),
* config_prefix = "resource",
* admin_permission = "administer rest resources",
* label_callback = "getLabelFromPlugin",
@ -203,7 +210,7 @@ class RestResourceConfig extends ConfigEntityBase implements RestResourceConfigI
*/
public function getPluginCollections() {
return [
'resource' => new DefaultSingleLazyPluginCollection($this->getResourcePluginManager(), $this->plugin_id, [])
'resource' => new DefaultSingleLazyPluginCollection($this->getResourcePluginManager(), $this->plugin_id, []),
];
}

View file

@ -0,0 +1,74 @@
<?php
namespace Drupal\rest\EventSubscriber;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Generates a 'create' route for an entity type if it has a REST POST route.
*/
class EntityResourcePostRouteSubscriber implements EventSubscriberInterface {
/**
* The REST resource config storage.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $resourceConfigStorage;
/**
* Constructs a new EntityResourcePostRouteSubscriber instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->resourceConfigStorage = $entity_type_manager->getStorage('rest_resource_config');
}
/**
* Provides routes on route rebuild time.
*
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The route build event.
*/
public function onDynamicRouteEvent(RouteBuildEvent $event) {
$route_collection = $event->getRouteCollection();
$resource_configs = $this->resourceConfigStorage->loadMultiple();
// Iterate over all REST resource config entities.
foreach ($resource_configs as $resource_config) {
// We only care about REST resource config entities for the
// \Drupal\rest\Plugin\rest\resource\EntityResource plugin.
$plugin_id = $resource_config->toArray()['plugin_id'];
if (substr($plugin_id, 0, 6) !== 'entity') {
continue;
}
$entity_type_id = substr($plugin_id, 7);
$rest_post_route_name = "rest.entity.$entity_type_id.POST";
if ($rest_post_route = $route_collection->get($rest_post_route_name)) {
// Create a route for the 'create' link relation type for this entity
// type that uses the same route definition as the REST 'POST' route
// which use that entity type.
// @see \Drupal\Core\Entity\Entity::toUrl()
$entity_create_route_name = "entity.$entity_type_id.create";
$route_collection->add($entity_create_route_name, $rest_post_route);
}
}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Priority -10, to run after \Drupal\rest\Routing\ResourceRoutes, which has
// priority 0.
$events[RoutingEvents::DYNAMIC][] = ['onDynamicRouteEvent', -10];
return $events;
}
}

View file

@ -2,12 +2,14 @@
namespace Drupal\rest\EventSubscriber;
use Drupal\Core\Cache\CacheableMetadata;
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 Drupal\serialization\Normalizer\CacheableNormalizerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
@ -79,7 +81,7 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
* 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
* forget to specify a response 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.
@ -94,43 +96,53 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
*/
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;
$acceptable_response_formats = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : [];
$acceptable_request_formats = $route->hasRequirement('_content_type_format') ? explode('|', $route->getRequirement('_content_type_format')) : [];
$acceptable_formats = $request->isMethodCacheable() ? $acceptable_response_formats : $acceptable_request_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)) {
// If an acceptable response format is requested, then use that. Otherwise,
// including and particularly when the client forgot to specify a response
// format, then use heuristics to select the format that is most likely
// expected.
if (in_array($requested_format, $acceptable_response_formats, TRUE)) {
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)) {
if (!empty($request->getContent()) && in_array($content_type_format, $acceptable_request_formats, TRUE)) {
return $content_type_format;
}
// Otherwise, use the first acceptable format.
elseif (!empty($acceptable_formats)) {
if (!empty($acceptable_formats)) {
return $acceptable_formats[0];
}
// Sometimes, there are no acceptable formats, e.g. DELETE routes.
else {
return NULL;
}
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.
* During serialization, encoders and normalizers are able to explicitly
* bubble cacheability metadata via the 'cacheability' key-value pair in the
* received context. This bubbled cacheability metadata will be applied to the
* the response.
*
* In versions of Drupal prior to 8.5, implicit bubbling of cacheability
* metadata was allowed because there was no explicit cacheability metadata
* bubbling API. To maintain backwards compatibility, we continue to support
* this, but support for this will be dropped in Drupal 9.0.0. This is
* especially useful when interacting with APIs that implicitly invoke
* rendering (for example: generating URLs): this allows those to "leak", and
* we collect their bubbled cacheability metadata automatically in a render
* context.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The request object.
@ -150,14 +162,25 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
// If there is data to send, serialize and set it as the response body.
if ($data !== NULL) {
$serialization_context = [
'request' => $request,
CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY => new CacheableMetadata(),
];
// @deprecated In Drupal 8.5.0, will be removed before Drupal 9.0.0. Use
// explicit cacheability metadata bubbling instead. (The wrapping call to
// executeInRenderContext() will be removed before Drupal 9.0.0.)
$context = new RenderContext();
$output = $this->renderer
->executeInRenderContext($context, function () use ($serializer, $data, $format) {
return $serializer->serialize($data, $format);
->executeInRenderContext($context, function () use ($serializer, $data, $format, $serialization_context) {
return $serializer->serialize($data, $format, $serialization_context);
});
if ($response instanceof CacheableResponseInterface && !$context->isEmpty()) {
$response->addCacheableDependency($context->pop());
if ($response instanceof CacheableResponseInterface) {
if (!$context->isEmpty()) {
@trigger_error('Implicit cacheability metadata bubbling (onto the global render context) in normalizers is deprecated since Drupal 8.5.0 and will be removed in Drupal 9.0.0. Use the "cacheability" serialization context instead, for explicit cacheability metadata bubbling. See https://www.drupal.org/node/2918937', E_USER_DEPRECATED);
$response->addCacheableDependency($context->pop());
}
$response->addCacheableDependency($serialization_context[CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY]);
}
$response->setContent($output);
@ -196,8 +219,9 @@ class ResourceResponseSubscriber implements EventSubscriberInterface {
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
// Run shortly before \Drupal\Core\EventSubscriber\FinishResponseSubscriber.
$events[KernelEvents::RESPONSE][] = ['onResponse', 5];
// Run before \Drupal\dynamic_page_cache\EventSubscriber\DynamicPageCacheSubscriber
// (priority 100), so that Dynamic Page Cache can cache flattened responses.
$events[KernelEvents::RESPONSE][] = ['onResponse', 128];
return $events;
}

View file

@ -22,7 +22,7 @@ class RestConfigSubscriber implements EventSubscriberInterface {
/**
* Constructs the RestConfigSubscriber.
*
* @param \Drupal\Core\Routing\RouteBuilderInterface $route_builder
* @param \Drupal\Core\Routing\RouteBuilderInterface $router_builder
* The router builder service.
*/
public function __construct(RouteBuilderInterface $router_builder) {
@ -37,6 +37,7 @@ class RestConfigSubscriber implements EventSubscriberInterface {
*/
public function onSave(ConfigCrudEvent $event) {
$saved_config = $event->getConfig();
// @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
if ($saved_config->getName() === 'rest.settings' && $event->isChanged('bc_entity_resource_permissions')) {
$this->routerBuilder->setRebuildNeeded();
}

View file

@ -7,5 +7,7 @@ use Drupal\hal\LinkManager\ConfigurableLinkManagerInterface as MovedConfigurable
/**
* @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.
*
* @see https://www.drupal.org/node/2830467
*/
interface ConfigurableLinkManagerInterface extends MovedConfigurableLinkManagerInterface {}

View file

@ -7,5 +7,7 @@ use Drupal\hal\LinkManager\LinkManager as MovedLinkManager;
/**
* @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.
*
* @see https://www.drupal.org/node/2830467
*/
class LinkManager extends MovedLinkManager implements LinkManagerInterface {}

View file

@ -7,5 +7,7 @@ use Drupal\hal\LinkManager\LinkManagerBase as MovedLinkManagerBase;
/**
* @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.
*
* @see https://www.drupal.org/node/2830467
*/
abstract class LinkManagerBase extends MovedLinkManagerBase {}

View file

@ -7,5 +7,7 @@ use Drupal\hal\LinkManager\LinkManagerInterface as MovedLinkManagerInterface;
/**
* @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.
*
* @see https://www.drupal.org/node/2830467
*/
interface LinkManagerInterface extends MovedLinkManagerInterface {}

View file

@ -7,5 +7,7 @@ use Drupal\hal\LinkManager\RelationLinkManager as MovedLinkRelationManager;
/**
* @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.
*
* @see https://www.drupal.org/node/2830467
*/
class RelationLinkManager extends MovedLinkRelationManager implements RelationLinkManagerInterface {}

View file

@ -7,5 +7,7 @@ use Drupal\hal\LinkManager\RelationLinkManagerInterface as MovedRelationLinkMana
/**
* @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.
*
* @see https://www.drupal.org/node/2830467
*/
interface RelationLinkManagerInterface extends MovedRelationLinkManagerInterface {}

View file

@ -7,5 +7,7 @@ use Drupal\hal\LinkManager\TypeLinkManager as MovedTypeLinkManager;
/**
* @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.
*
* @see https://www.drupal.org/node/2830467
*/
class TypeLinkManager extends MovedTypeLinkManager implements TypeLinkManagerInterface {}

View file

@ -7,5 +7,7 @@ use Drupal\hal\LinkManager\TypeLinkManagerInterface as MovedTypeLinkManagerInter
/**
* @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.
*
* @see https://www.drupal.org/node/2830467
*/
interface TypeLinkManagerInterface extends MovedTypeLinkManagerInterface {}

View file

@ -0,0 +1,55 @@
<?php
namespace Drupal\rest\PathProcessor;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Path processor to maintain BC for entity REST resource URLs from Drupal 8.0.
*/
class PathProcessorEntityResourceBC implements InboundPathProcessorInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Creates a new PathProcessorEntityResourceBC instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public function processInbound($path, Request $request) {
if ($request->getMethod() === 'POST' && strpos($path, '/entity/') === 0) {
$parts = explode('/', $path);
$entity_type_id = array_pop($parts);
// Until Drupal 8.3, no entity types specified a link template for the
// 'create' link relation type. As of Drupal 8.3, all core content entity
// types provide this link relation type. This inbound path processor
// provides automatic backwards compatibility: it allows both the old
// default from \Drupal\rest\Plugin\rest\resource\EntityResource, i.e.
// "/entity/{entity_type}" and the link template specified in a particular
// entity type. The former is rewritten to the latter
// specific one if it exists.
$entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
if ($entity_type->hasLinkTemplate('create')) {
return $entity_type->getLinkTemplate('create');
}
}
return $path;
}
}

View file

@ -65,6 +65,10 @@ 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) {
if ($entity_type->isInternal()) {
continue;
}
$this->derivatives[$entity_type_id] = [
'id' => 'entity:' . $entity_type_id,
'entity_type' => $entity_type_id,
@ -74,7 +78,7 @@ class EntityDeriver implements ContainerDeriverInterface {
$default_uris = [
'canonical' => "/entity/$entity_type_id/" . '{' . $entity_type_id . '}',
'https://www.drupal.org/link-relations/create' => "/entity/$entity_type_id",
'create' => "/entity/$entity_type_id",
];
foreach ($default_uris as $link_relation => $default_uri) {

View file

@ -4,6 +4,7 @@ namespace Drupal\rest\Plugin;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Routing\BcRoute;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Routing\Route;
@ -100,35 +101,38 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
$definition = $this->getPluginDefinition();
$canonical_path = isset($definition['uri_paths']['canonical']) ? $definition['uri_paths']['canonical'] : '/' . strtr($this->pluginId, ':', '/') . '/{id}';
$create_path = isset($definition['uri_paths']['https://www.drupal.org/link-relations/create']) ? $definition['uri_paths']['https://www.drupal.org/link-relations/create'] : '/' . strtr($this->pluginId, ':', '/');
$create_path = isset($definition['uri_paths']['create']) ? $definition['uri_paths']['create'] : '/' . strtr($this->pluginId, ':', '/');
// BC: the REST module originally created the POST URL for a resource by
// reading the 'https://www.drupal.org/link-relations/create' URI path from
// the plugin annotation. For consistency with entity type definitions, that
// then changed to reading the 'create' URI path. For any REST Resource
// plugins that were using the old mechanism, we continue to support that.
if (!isset($definition['uri_paths']['create']) && isset($definition['uri_paths']['https://www.drupal.org/link-relations/create'])) {
$create_path = $definition['uri_paths']['https://www.drupal.org/link-relations/create'];
}
$route_name = strtr($this->pluginId, ':', '.');
$methods = $this->availableMethods();
foreach ($methods as $method) {
$route = $this->getBaseRoute($canonical_path, $method);
$path = $method === 'POST'
? $create_path
: $canonical_path;
$route = $this->getBaseRoute($path, $method);
switch ($method) {
case 'POST':
$route->setPath($create_path);
$collection->add("$route_name.$method", $route);
break;
// Note that '_format' and '_content_type_format' route requirements are
// added in ResourceRoutes::getRoutesForResourceConfig().
$collection->add("$route_name.$method", $route);
case 'GET':
case 'HEAD':
// Restrict GET and HEAD requests to the media type specified in the
// HTTP Accept headers.
foreach ($this->serializerFormats as $format_name) {
// Expose one route per available format.
$format_route = clone $route;
$format_route->addRequirements(['_format' => $format_name]);
$collection->add("$route_name.$method.$format_name", $format_route);
}
break;
default:
$collection->add("$route_name.$method", $route);
break;
// BC: the REST module originally created per-format GET routes, instead
// of a single route. To minimize the surface of this BC layer, this uses
// route definitions that are as empty as possible, plus an outbound route
// processor.
// @see \Drupal\rest\RouteProcessor\RestResourceGetRouteProcessorBC
if ($method === 'GET' || $method === 'HEAD') {
foreach ($this->serializerFormats as $format_name) {
$collection->add("$route_name.$method.$format_name", (new BcRoute())->setRequirement('_format', $format_name));
}
}
}

View file

@ -40,8 +40,10 @@ class ResourcePluginManager extends DefaultPluginManager {
* @deprecated in Drupal 8.2.0.
* Use Drupal\rest\Plugin\Type\ResourcePluginManager::createInstance()
* instead.
*
* @see https://www.drupal.org/node/2874934
*/
public function getInstance(array $options){
public function getInstance(array $options) {
if (isset($options['id'])) {
return $this->createInstance($options['id']);
}

View file

@ -4,6 +4,7 @@ namespace Drupal\rest\Plugin\rest\resource;
use Drupal\Component\Plugin\DependentPluginInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Access\AccessResultReasonInterface;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\Core\Entity\EntityTypeManagerInterface;
@ -12,7 +13,7 @@ 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\Core\Http\Exception\CacheableAccessDeniedHttpException;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Psr\Log\LoggerInterface;
@ -35,7 +36,7 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
* deriver = "Drupal\rest\Plugin\Deriver\EntityDeriver",
* uri_paths = {
* "canonical" = "/entity/{entity_type}/{entity}",
* "https://www.drupal.org/link-relations/create" = "/entity/{entity_type}"
* "create" = "/entity/{entity_type}"
* }
* )
*/
@ -122,7 +123,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($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
throw new CacheableAccessDeniedHttpException($entity_access, $entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'view'));
}
$response = new ResourceResponse($entity, 200);
@ -201,41 +202,6 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
}
}
/**
* 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.
*
@ -262,42 +228,30 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
throw new AccessDeniedHttpException($entity_access->getReason() ?: $this->generateFallbackAccessDeniedMessage($entity, 'update'));
}
// Overwrite the received properties.
$entity_keys = $entity->getEntityType()->getKeys();
// Overwrite the received fields.
// @todo Remove $changed_fields in https://www.drupal.org/project/drupal/issues/2862574.
$changed_fields = [];
foreach ($entity->_restSubmittedFields as $field_name) {
$field = $entity->get($field_name);
// Entity key fields need special treatment: together they uniquely
// identify the entity. Therefore it does not make sense to modify any of
// 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 ($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
// re-initialized. As it must not be empty, skip it if it is.
elseif (isset($entity_keys['langcode']) && $field_name === $entity_keys['langcode'] && $field->isEmpty()) {
continue;
}
// It is not possible to set the language to NULL as it is automatically
// re-initialized. As it must not be empty, skip it if it is.
// @todo Remove in https://www.drupal.org/project/drupal/issues/2933408.
if ($entity->getEntityType()->hasKey('langcode') && $field_name === $entity->getEntityType()->getKey('langcode') && $field->isEmpty()) {
continue;
}
if (!$original_entity->get($field_name)->access('edit')) {
throw new AccessDeniedHttpException("Access denied on updating field '$field_name'.");
if ($this->checkPatchFieldAccess($original_entity->get($field_name), $field)) {
$changed_fields[] = $field_name;
$original_entity->set($field_name, $field->getValue());
}
$original_entity->set($field_name, $field->getValue());
}
// If no fields are changed, we can send a response immediately!
if (empty($changed_fields)) {
return new ModifiedResourceResponse($original_entity, 200);
}
// Validate the received data before saving.
$this->validate($original_entity);
$this->validate($original_entity, $changed_fields);
try {
$original_entity->save();
$this->logger->notice('Updated entity %type with ID %id.', ['%type' => $original_entity->getEntityTypeId(), '%id' => $original_entity->id()]);
@ -310,6 +264,57 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
}
}
/**
* Checks whether the given field should be PATCHed.
*
* @param \Drupal\Core\Field\FieldItemListInterface $original_field
* The original (stored) value for the field.
* @param \Drupal\Core\Field\FieldItemListInterface $received_field
* The received value for the field.
*
* @return bool
* Whether the field should be PATCHed or not.
*
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
* Thrown when the user sending the request is not allowed to update the
* field. Only thrown when the user could not abuse this information to
* determine the stored value.
*
* @internal
*/
protected function checkPatchFieldAccess(FieldItemListInterface $original_field, FieldItemListInterface $received_field) {
// The user might not have access to edit the field, but still needs to
// submit the current field value as part of the PATCH request. For
// example, the entity keys required by denormalizers. Therefore, if the
// received value equals the stored value, return FALSE without throwing an
// exception. But only for fields that the user has access to view, because
// the user has no legitimate way of knowing the current value of fields
// that they are not allowed to view, and we must not make the presence or
// absence of a 403 response a way to find that out.
if ($original_field->access('view') && $original_field->equals($received_field)) {
return FALSE;
}
// If the user is allowed to edit the field, it is always safe to set the
// received value. We may be setting an unchanged value, but that is ok.
$field_edit_access = $original_field->access('edit', NULL, TRUE);
if ($field_edit_access->isAllowed()) {
return TRUE;
}
// It's helpful and safe to let the user know when they are not allowed to
// update a field.
$field_name = $received_field->getName();
$error_message = "Access denied on updating field '$field_name'.";
if ($field_edit_access instanceof AccessResultReasonInterface) {
$reason = $field_edit_access->getReason();
if ($reason) {
$error_message .= ' ' . $reason;
}
}
throw new AccessDeniedHttpException($error_message);
}
/**
* Responds to entity DELETE requests.
*
@ -431,8 +436,11 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
* @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)) {
foreach ($entity->uriRelationships() as $relation_name) {
if ($this->linkRelationTypeManager->hasDefinition($relation_name)) {
/** @var \Drupal\Core\Http\LinkRelationTypeInterface $link_relation_type */
$link_relation_type = $this->linkRelationTypeManager->createInstance($relation_name);
$generator_url = $entity->toUrl($relation_name)
->setAbsolute(TRUE)
->toString(TRUE);
@ -440,10 +448,10 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
$response->addCacheableDependency($generator_url);
}
$uri = $generator_url->getGeneratedUrl();
$relationship = $relation_name;
if (!empty($definition['uri'])) {
$relationship = $definition['uri'];
}
$relationship = $link_relation_type->isRegistered()
? $link_relation_type->getRegisteredName()
: $link_relation_type->getExtensionUri();
$link_header = '<' . $uri . '>; rel="' . $relationship . '"';
$response->headers->set('Link', $link_header, FALSE);

View file

@ -14,16 +14,23 @@ use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
trait EntityResourceValidationTrait {
/**
* Verifies that the whole entity does not violate any validation constraints.
* Verifies that an entity does not violate any validation constraints.
*
* The validation errors will be filtered to not include fields to which the
* current user does not have access and if $fields_to_validate is provided
* will only include fields in that array.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity to validate.
* @param string[] $fields_to_validate
* (optional) An array of field names. If specified, filters the violations
* list to include only this set of fields.
*
* @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.
protected function validate(EntityInterface $entity, array $fields_to_validate = []) {
// @todo Update this check in https://www.drupal.org/node/2300677.
if (!$entity instanceof FieldableEntityInterface) {
return;
}
@ -33,6 +40,11 @@ trait EntityResourceValidationTrait {
// changes.
$violations->filterByFieldAccess();
if ($fields_to_validate) {
// Filter violations by explicitly provided array of field names.
$violations->filterByFields(array_diff(array_keys($entity->getFieldDefinitions()), $fields_to_validate));
}
if ($violations->count() > 0) {
$message = "Unprocessable Entity: validation failed.\n";
foreach ($violations as $violation) {

View file

@ -70,7 +70,7 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
*
* @var string
*/
protected $mimeType;
protected $mimeType = 'application/json';
/**
* The renderer.
@ -87,12 +87,35 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
protected $authenticationCollector;
/**
* The authentication providers, keyed by ID.
* The authentication providers, like 'cookie' and 'basic_auth'.
*
* @var string[]
*/
protected $authenticationProviderIds;
/**
* The authentication providers' modules, keyed by provider ID.
*
* Authentication providers like 'cookie' and 'basic_auth' are the array
* keys. The array values are the module names, e.g.:
* @code
* ['cookie' => 'user', 'basic_auth' => 'basic_auth']
* @endcode
*
* @deprecated as of 8.4.x, will be removed in before Drupal 9.0.0, see
* https://www.drupal.org/node/2825204.
*
* @var string[]
*/
protected $authenticationProviders;
/**
* The serialization format providers, keyed by format.
*
* @var string[]
*/
protected $formatProviders;
/**
* Constructs a RestExport object.
*
@ -110,12 +133,22 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
* The renderer.
* @param string[] $authentication_providers
* The authentication providers, keyed by ID.
* @param string[] $serializer_format_providers
* The serialization format providers, keyed by format.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer, array $authentication_providers) {
public function __construct(array $configuration, $plugin_id, $plugin_definition, RouteProviderInterface $route_provider, StateInterface $state, RendererInterface $renderer, array $authentication_providers, array $serializer_format_providers) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $route_provider, $state);
$this->renderer = $renderer;
// $authentication_providers as defined in
// \Drupal\Core\DependencyInjection\Compiler\AuthenticationProviderPass
// and as such it is an array, with authentication providers (cookie,
// basic_auth) as keys and modules providing those as values (user,
// basic_auth).
$this->authenticationProviderIds = array_keys($authentication_providers);
// For BC reasons we keep around authenticationProviders as before.
$this->authenticationProviders = $authentication_providers;
$this->formatProviders = $serializer_format_providers;
}
/**
@ -129,31 +162,33 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
$container->get('router.route_provider'),
$container->get('state'),
$container->get('renderer'),
$container->getParameter('authentication_providers')
$container->getParameter('authentication_providers'),
$container->getParameter('serializer.format_providers')
);
}
/**
* {@inheritdoc}
*/
public function initDisplay(ViewExecutable $view, array &$display, array &$options = NULL) {
parent::initDisplay($view, $display, $options);
$request_content_type = $this->view->getRequest()->getRequestFormat();
// Only use the requested content type if it's not 'html'. If it is then
// default to 'json' to aid debugging.
// @todo Remove the need for this when we have better content negotiation.
if ($request_content_type != 'html') {
$this->setContentType($request_content_type);
}
// If the requested content type is 'html' and the default 'json' is not
// selected as a format option in the view display, fallback to the first
// format in the array.
elseif (!empty($options['style']['options']['formats']) && !isset($options['style']['options']['formats'][$this->getContentType()])) {
$this->setContentType(reset($options['style']['options']['formats']));
// If the default 'json' format is not selected as a format option in the
// view display, fallback to the first format available for the default.
if (!empty($options['style']['options']['formats']) && !isset($options['style']['options']['formats'][$this->getContentType()])) {
$default_format = reset($options['style']['options']['formats']);
$this->setContentType($default_format);
}
$this->setMimeType($this->view->getRequest()->getMimeType($this->contentType));
// Only use the requested content type if it's not 'html'. This allows
// still falling back to the default for things like views preview.
$request_content_type = $this->view->getRequest()->getRequestFormat();
if ($request_content_type !== 'html') {
$this->setContentType($request_content_type);
}
$this->setMimeType($this->view->getRequest()->getMimeType($this->getContentType()));
}
/**
@ -227,7 +262,7 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
* An array to use as value for "#options" in the form element.
*/
public function getAuthOptions() {
return array_combine($this->authenticationProviders, $this->authenticationProviders);
return array_combine($this->authenticationProviderIds, $this->authenticationProviderIds);
}
/**
@ -327,17 +362,21 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
if ($route = $collection->get("view.$view_id.$display_id")) {
$style_plugin = $this->getPlugin('style');
// REST exports should only respond to get methods.
// REST exports should only respond to GET methods.
$route->setMethods(['GET']);
// Format as a string using pipes as a delimiter.
if ($formats = $style_plugin->getFormats()) {
// Allow a REST Export View to be returned with an HTML-only accept
// format. That allows browsers or other non-compliant systems to access
// the view, as it is unlikely to have a conflicting HTML representation
// anyway.
$route->setRequirement('_format', implode('|', $formats + ['html']));
$formats = $style_plugin->getFormats();
// If there are no configured formats, add all formats that serialization
// is known to support.
if (!$formats) {
$formats = $this->getFormatOptions();
}
// Format as a string using pipes as a delimiter.
$route->setRequirement('_format', implode('|', $formats));
// Add authentication to the route if it was set. If no authentication was
// set, the default authentication will be used, which is cookie based by
// default.
@ -407,7 +446,7 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
*/
public function render() {
$build = [];
$build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function() {
$build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function () {
return $this->view->style_plugin->render();
});
@ -421,21 +460,21 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
$build['#suffix'] = '</pre>';
unset($build['#markup']);
}
elseif ($this->view->getRequest()->getFormat($this->view->element['#content_type']) !== 'html') {
// This display plugin is primarily for returning non-HTML formats.
// However, we still invoke the renderer to collect cacheability metadata.
// Because the renderer is designed for HTML rendering, it filters
// #markup for XSS unless it is already known to be safe, but that filter
// only works for HTML. Therefore, we mark the contents as safe to bypass
// the filter. So long as we are returning this in a non-HTML response
// (checked above), this is safe, because an XSS attack only works when
// executed by an HTML agent.
else {
// This display plugin is for returning non-HTML formats. However, we
// still invoke the renderer to collect cacheability metadata. Because the
// renderer is designed for HTML rendering, it filters #markup for XSS
// unless it is already known to be safe, but that filter only works for
// HTML. Therefore, we mark the contents as safe to bypass the filter. So
// long as we are returning this in a non-HTML response,
// this is safe, because an XSS attack only works when executed by an HTML
// agent.
// @todo Decide how to support non-HTML in the render API in
// https://www.drupal.org/node/2501313.
$build['#markup'] = ViewsRenderPipelineMarkup::create($build['#markup']);
}
parent::applyDisplayCachablityMetadata($build);
parent::applyDisplayCacheabilityMetadata($build);
return $build;
}
@ -457,12 +496,27 @@ class RestExport extends PathPluginBase implements ResponseDisplayPluginInterfac
$dependencies = parent::calculateDependencies();
$dependencies += ['module' => []];
$modules = array_map(function ($authentication_provider) {
return $this->authenticationProviders[$authentication_provider];
}, $this->getOption('auth'));
$dependencies['module'] = array_merge($dependencies['module'], $modules);
$dependencies['module'] = array_merge($dependencies['module'], array_filter(array_map(function ($provider) {
// During the update path the provider options might be wrong. This can
// happen when any update function, like block_update_8300() triggers a
// view to be saved.
return isset($this->authenticationProviderIds[$provider])
? $this->authenticationProviderIds[$provider]
: NULL;
}, $this->getOption('auth'))));
return $dependencies;
}
/**
* Returns an array of format options.
*
* @return string[]
* An array of format options. Both key and value are the same.
*/
protected function getFormatOptions() {
$formats = array_keys($this->formatProviders);
return array_combine($formats, $formats);
}
}

View file

@ -184,7 +184,7 @@ class DataFieldRow extends RowPluginBase {
* A regular one dimensional array of values.
*/
protected static function extractFromOptionsArray($key, $options) {
return array_map(function($item) use ($key) {
return array_map(function ($item) use ($key) {
return isset($item[$key]) ? $item[$key] : NULL;
}, $options);
}

View file

@ -2,110 +2,330 @@
namespace Drupal\rest;
use Drupal\Component\Utility\ArgumentsResolver;
use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
use Drupal\rest\Plugin\ResourceInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Exception\InvalidArgumentException;
use Symfony\Component\Serializer\SerializerInterface;
/**
* Acts as intermediate request forwarder for resource plugins.
*
* @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
*/
class RequestHandler implements ContainerAwareInterface, ContainerInjectionInterface {
use ContainerAwareTrait;
class RequestHandler implements ContainerInjectionInterface {
/**
* The resource configuration storage.
* The config factory.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $resourceStorage;
protected $configFactory;
/**
* The serializer.
*
* @var \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface
*/
protected $serializer;
/**
* Creates a new RequestHandler instance.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $entity_storage
* The resource configuration storage.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface $serializer
* The serializer.
*/
public function __construct(EntityStorageInterface $entity_storage) {
$this->resourceStorage = $entity_storage;
public function __construct(ConfigFactoryInterface $config_factory, SerializerInterface $serializer) {
$this->configFactory = $config_factory;
$this->serializer = $serializer;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static($container->get('entity_type.manager')->getStorage('rest_resource_config'));
return new static(
$container->get('config.factory'),
$container->get('serializer')
);
}
/**
* Handles a web API request.
* Handles a REST API request.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
* @param \Drupal\rest\RestResourceConfigInterface $_rest_resource_config
* The REST resource config entity.
*
* @return \Symfony\Component\HttpFoundation\Response
* The response object.
* @return \Drupal\rest\ResourceResponseInterface|\Symfony\Component\HttpFoundation\Response
* The REST resource response.
*/
public function handle(RouteMatchInterface $route_match, Request $request) {
$method = strtolower($request->getMethod());
public function handle(RouteMatchInterface $route_match, Request $request, RestResourceConfigInterface $_rest_resource_config) {
$resource = $_rest_resource_config->getResourcePlugin();
$unserialized = $this->deserialize($route_match, $request, $resource);
$response = $this->delegateToRestResourcePlugin($route_match, $request, $unserialized, $resource);
return $this->prepareResponse($response, $_rest_resource_config);
}
/**
* Handles a REST API request without deserializing the request body.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
* @param \Drupal\rest\RestResourceConfigInterface $_rest_resource_config
* The REST resource config entity.
*
* @return \Symfony\Component\HttpFoundation\Response|\Drupal\rest\ResourceResponseInterface
* The REST resource response.
*/
public function handleRaw(RouteMatchInterface $route_match, Request $request, RestResourceConfigInterface $_rest_resource_config) {
$resource = $_rest_resource_config->getResourcePlugin();
$response = $this->delegateToRestResourcePlugin($route_match, $request, NULL, $resource);
return $this->prepareResponse($response, $_rest_resource_config);
}
/**
* Prepares the REST resource response.
*
* @param \Drupal\rest\ResourceResponseInterface $response
* The REST resource response.
* @param \Drupal\rest\RestResourceConfigInterface $resource_config
* The REST resource config entity.
*
* @return \Drupal\rest\ResourceResponseInterface
* The prepared REST resource response.
*/
protected function prepareResponse($response, RestResourceConfigInterface $resource_config) {
if ($response instanceof CacheableResponseInterface) {
$response->addCacheableDependency($resource_config);
// Add global rest settings config's cache tag, for BC flags.
// @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
// @see \Drupal\rest\EventSubscriber\RestConfigSubscriber
// @todo Remove in https://www.drupal.org/node/2893804
$response->addCacheableDependency($this->configFactory->get('rest.settings'));
}
return $response;
}
/**
* Gets the normalized HTTP request method of the matched route.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
*
* @return string
* The normalized HTTP request method.
*/
protected static function getNormalizedRequestMethod(RouteMatchInterface $route_match) {
// Symfony is built to transparently map HEAD requests to a GET request. In
// the case of the REST module's RequestHandler though, we essentially have
// our own light-weight routing system on top of the Drupal/symfony routing
// system. So, we have to do the same as what the UrlMatcher does: map HEAD
// requests to the logic for GET. This also guarantees response headers for
// HEAD requests are identical to those for GET requests, because we just
// return a GET response. Response::prepare() will transform it to a HEAD
// response at the very last moment.
// system. So, we have to respect the decision that the routing system made:
// we look not at the request method, but at the route's method. All REST
// routes are guaranteed to have _method set.
// Response::prepare() will transform it to a HEAD response at the very last
// moment.
// @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
// @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
// @see \Symfony\Component\HttpFoundation\Response::prepare()
if ($method === 'head') {
$method = 'get';
}
$resource_config_id = $route_match->getRouteObject()->getDefault('_rest_resource_config');
/** @var \Drupal\rest\RestResourceConfigInterface $resource_config */
$resource_config = $this->resourceStorage->load($resource_config_id);
$resource = $resource_config->getResourcePlugin();
$method = strtolower($route_match->getRouteObject()->getMethods()[0]);
assert(count($route_match->getRouteObject()->getMethods()) === 1);
return $method;
}
/**
* Deserializes request body, if any.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
* @param \Drupal\rest\Plugin\ResourceInterface $resource
* The REST resource plugin.
*
* @return array|null
* An object normalization, ikf there is a valid request body. NULL if there
* is no request body.
*
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
* Thrown if the request body cannot be decoded.
* @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
* Thrown if the request body cannot be denormalized.
*/
protected function deserialize(RouteMatchInterface $route_match, Request $request, ResourceInterface $resource) {
// Deserialize incoming data if available.
/** @var \Symfony\Component\Serializer\SerializerInterface $serializer */
$serializer = $this->container->get('serializer');
$received = $request->getContent();
$unserialized = NULL;
if (!empty($received)) {
$method = static::getNormalizedRequestMethod($route_match);
$format = $request->getContentType();
$definition = $resource->getPluginDefinition();
// First decode the request data. We can then determine if the
// serialized data was malformed.
try {
if (!empty($definition['serialization_class'])) {
$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, ['request_method' => $method]);
}
$unserialized = $this->serializer->decode($received, $format, ['request_method' => $method]);
}
catch (UnexpectedValueException $e) {
// If an exception was thrown at this stage, there was a problem
// decoding the data. Throw a 400 http exception.
throw new BadRequestHttpException($e->getMessage());
}
// Then attempt to denormalize if there is a serialization class.
if (!empty($definition['serialization_class'])) {
try {
$unserialized = $this->serializer->denormalize($unserialized, $definition['serialization_class'], $format, ['request_method' => $method]);
}
// These two serialization exception types mean there was a problem
// with the structure of the decoded data and it's not valid.
catch (UnexpectedValueException $e) {
throw new UnprocessableEntityHttpException($e->getMessage());
}
catch (InvalidArgumentException $e) {
throw new UnprocessableEntityHttpException($e->getMessage());
}
}
}
return $unserialized;
}
/**
* Delegates an incoming request to the appropriate REST resource plugin.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param \Symfony\Component\HttpFoundation\Request $request
* The HTTP request object.
* @param mixed|null $unserialized
* The unserialized request body, if any.
* @param \Drupal\rest\Plugin\ResourceInterface $resource
* The REST resource plugin.
*
* @return \Symfony\Component\HttpFoundation\Response|\Drupal\rest\ResourceResponseInterface
* The REST resource response.
*/
protected function delegateToRestResourcePlugin(RouteMatchInterface $route_match, Request $request, $unserialized, ResourceInterface $resource) {
$method = static::getNormalizedRequestMethod($route_match);
// Determine the request parameters that should be passed to the resource
// plugin.
$argument_resolver = $this->createArgumentResolver($route_match, $unserialized, $request);
try {
$arguments = $argument_resolver->getArguments([$resource, $method]);
}
catch (\RuntimeException $exception) {
@trigger_error('Passing in arguments the legacy way is deprecated in Drupal 8.4.0 and will be removed before Drupal 9.0.0. Provide the right parameter names in the method, similar to controllers. See https://www.drupal.org/node/2894819', E_USER_DEPRECATED);
$arguments = $this->getLegacyParameters($route_match, $unserialized, $request);
}
// Invoke the operation on the resource plugin.
return call_user_func_array([$resource, $method], $arguments);
}
/**
* Creates an argument resolver, containing all REST parameters.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match.
* @param mixed $unserialized
* The unserialized data.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @return \Drupal\Component\Utility\ArgumentsResolver
* An instance of the argument resolver containing information like the
* 'entity' we process and the 'unserialized' content from the request body.
*/
protected function createArgumentResolver(RouteMatchInterface $route_match, $unserialized, Request $request) {
$route = $route_match->getRouteObject();
// Defaults for the parameters defined on the route object need to be added
// to the raw arguments.
$raw_route_arguments = $route_match->getRawParameters()->all() + $route->getDefaults();
$route_arguments = $route_match->getParameters()->all();
$upcasted_route_arguments = $route_arguments;
// For request methods that have request bodies, ResourceInterface plugin
// methods historically receive the unserialized request body as the N+1th
// method argument, where N is the number of route parameters specified on
// the accompanying route. To be able to use the argument resolver, which is
// not based on position but on name and typehint, specify commonly used
// names here. Similarly, those methods receive the original stored object
// as the first method argument.
$route_arguments_entity = NULL;
// Try to find a parameter which is an entity.
foreach ($route_arguments as $value) {
if ($value instanceof EntityInterface) {
$route_arguments_entity = $value;
break;
}
}
if (in_array($request->getMethod(), ['PATCH', 'POST'], TRUE)) {
$upcasted_route_arguments['entity'] = $unserialized;
$upcasted_route_arguments['data'] = $unserialized;
$upcasted_route_arguments['unserialized'] = $unserialized;
$upcasted_route_arguments['original_entity'] = $route_arguments_entity;
}
else {
$upcasted_route_arguments['entity'] = $route_arguments_entity;
}
// Parameters which are not defined on the route object, but still are
// essential for access checking are passed as wildcards to the argument
// resolver.
$wildcard_arguments = [$route, $route_match];
$wildcard_arguments[] = $request;
if (isset($unserialized)) {
$wildcard_arguments[] = $unserialized;
}
return new ArgumentsResolver($raw_route_arguments, $upcasted_route_arguments, $wildcard_arguments);
}
/**
* Provides the parameter usable without an argument resolver.
*
* This creates an list of parameters in a statically defined order.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The route match
* @param mixed $unserialized
* The unserialized data.
* @param \Symfony\Component\HttpFoundation\Request $request
* The request.
*
* @deprecated in Drupal 8.4.0, will be removed before Drupal 9.0.0. Use the
* argument resolver method instead, see ::createArgumentResolver().
*
* @see https://www.drupal.org/node/2894819
*
* @return array
* An array of parameters.
*/
protected function getLegacyParameters(RouteMatchInterface $route_match, $unserialized, Request $request) {
$route_parameters = $route_match->getParameters();
$parameters = [];
// Filter out all internal parameters starting with "_".
@ -115,15 +335,7 @@ class RequestHandler implements ContainerAwareInterface, ContainerInjectionInter
}
}
// Invoke the operation on the resource plugin.
$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);
}
return $response;
return array_merge($parameters, [$unserialized, $request]);
}
}

View file

@ -2,7 +2,6 @@
namespace Drupal\rest;
trait ResourceResponseTrait {
/**

View file

@ -7,7 +7,7 @@ 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\ChildDefinition;
use Symfony\Component\DependencyInjection\Reference;
/**
@ -27,19 +27,22 @@ class RestServiceProvider implements ServiceProviderInterface {
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'));
// @see https://www.drupal.org/node/2830467
$service_definition = new ChildDefinition(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'));
// @see https://www.drupal.org/node/2830467
$service_definition = new ChildDefinition(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'));
// @see https://www.drupal.org/node/2830467
$service_definition = new ChildDefinition(new Reference('hal.link_manager.relation'));
$service_definition->setClass(RelationLinkManager::class);
$container->setDefinition('rest.link_manager.relation', $service_definition);
}

View file

@ -0,0 +1,80 @@
<?php
namespace Drupal\rest\RouteProcessor;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Symfony\Component\Routing\Route;
/**
* Processes the BC REST routes, to ensure old route names continue to work.
*/
class RestResourceGetRouteProcessorBC implements OutboundRouteProcessorInterface {
/**
* The available serialization formats.
*
* @var string[]
*/
protected $serializerFormats = [];
/**
* The route provider.
*
* @var \Drupal\Core\Routing\RouteProviderInterface
*/
protected $routeProvider;
/**
* Constructs a RestResourceGetRouteProcessorBC object.
*
* @param string[] $serializer_formats
* The available serialization formats.
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
* The route provider.
*/
public function __construct(array $serializer_formats, RouteProviderInterface $route_provider) {
$this->serializerFormats = $serializer_formats;
$this->routeProvider = $route_provider;
}
/**
* {@inheritdoc}
*/
public function processOutbound($route_name, Route $route, array &$parameters, BubbleableMetadata $bubbleable_metadata = NULL) {
$route_name_parts = explode('.', $route_name);
// BC: the REST module originally created per-format GET routes, instead
// of a single route. To minimize the surface of this BC layer, this uses
// route definitions that are as empty as possible, plus an outbound route
// processor.
// @see \Drupal\rest\Plugin\ResourceBase::routes()
if ($route_name_parts[0] === 'rest' && $route_name_parts[count($route_name_parts) - 2] === 'GET' && in_array($route_name_parts[count($route_name_parts) - 1], $this->serializerFormats, TRUE)) {
array_pop($route_name_parts);
$redirected_route_name = implode('.', $route_name_parts);
@trigger_error(sprintf("The '%s' route is deprecated since version 8.5.x and will be removed in 9.0.0. Use the '%s' route instead.", $route_name, $redirected_route_name), E_USER_DEPRECATED);
static::overwriteRoute($route, $this->routeProvider->getRouteByName($redirected_route_name));
}
}
/**
* Overwrites one route's metadata with the other's.
*
* @param \Symfony\Component\Routing\Route $target_route
* The route whose metadata to overwrite.
* @param \Symfony\Component\Routing\Route $source_route
* The route whose metadata to read from.
*
* @see \Symfony\Component\Routing\Route
*/
protected static function overwriteRoute(Route $target_route, Route $source_route) {
$target_route->setPath($source_route->getPath());
$target_route->setDefaults($source_route->getDefaults());
$target_route->setRequirements($source_route->getRequirements());
$target_route->setOptions($source_route->getOptions());
$target_route->setHost($source_route->getHost());
$target_route->setSchemes($source_route->getSchemes());
$target_route->setMethods($source_route->getMethods());
}
}

View file

@ -3,16 +3,18 @@
namespace Drupal\rest\Routing;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RoutingEvents;
use Drupal\rest\Plugin\Type\ResourcePluginManager;
use Drupal\rest\RestResourceConfigInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for REST-style routes.
*/
class ResourceRoutes extends RouteSubscriberBase {
class ResourceRoutes implements EventSubscriberInterface {
/**
* The plugin manager for REST plugins.
@ -54,18 +56,18 @@ class ResourceRoutes extends RouteSubscriberBase {
/**
* Alters existing routes for a specific collection.
*
* @param \Symfony\Component\Routing\RouteCollection $collection
* The route collection for adding routes.
* @param \Drupal\Core\Routing\RouteBuildEvent $event
* The route build event.
* @return array
*/
protected function alterRoutes(RouteCollection $collection) {
public function onDynamicRouteEvent(RouteBuildEvent $event) {
// Iterate over all enabled REST resource config entities.
/** @var \Drupal\rest\RestResourceConfigInterface[] $resource_configs */
$resource_configs = $this->resourceConfigStorage->loadMultiple();
foreach ($resource_configs as $resource_config) {
if ($resource_config->status()) {
$resource_routes = $this->getRoutesForResourceConfig($resource_config);
$collection->addCollection($resource_routes);
$event->getRouteCollection()->addCollection($resource_routes);
}
}
}
@ -90,8 +92,11 @@ class ResourceRoutes extends RouteSubscriberBase {
/** @var \Symfony\Component\Routing\Route $route */
// @todo: Are multiple methods possible here?
$methods = $route->getMethods();
// Only expose routes where the method is enabled in the configuration.
if ($methods && ($method = $methods[0]) && $supported_formats = $rest_resource_config->getFormats($method)) {
// Only expose routes
// - that have an explicit method and allow >=1 format for that method
// - that exist for BC
// @see \Drupal\rest\RouteProcessor\RestResourceGetRouteProcessorBC
if (($methods && ($method = $methods[0]) && $supported_formats = $rest_resource_config->getFormats($method)) || $route->hasOption('bc_route')) {
$route->setRequirement('_csrf_request_header_token', 'TRUE');
// Check that authentication providers are defined.
@ -106,24 +111,34 @@ class ResourceRoutes extends RouteSubscriberBase {
continue;
}
// If the route has a format requirement, then verify that the
// resource has it.
$format_requirement = $route->getRequirement('_format');
if ($format_requirement && !in_array($format_requirement, $rest_resource_config->getFormats($method))) {
continue;
// Remove BC routes for unsupported formats.
if ($route->getOption('bc_route') === TRUE) {
$format_requirement = $route->getRequirement('_format');
if ($format_requirement && !in_array($format_requirement, $rest_resource_config->getFormats($method))) {
continue;
}
}
// The configuration has been validated, so we update the route to:
// - set the allowed response body content types/formats for methods
// that may send response bodies (unless hardcoded by the plugin)
// - set the allowed request body content types/formats for methods that
// allow request bodies to be sent
// allow request bodies to be sent (unless hardcoded by the plugin)
// - set the allowed authentication providers
if (in_array($method, ['POST', 'PATCH', 'PUT'], TRUE)) {
// Restrict the incoming HTTP Content-type header to the allowed
// formats.
if (in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'PATCH'], TRUE) && !$route->hasRequirement('_format')) {
$route->addRequirements(['_format' => implode('|', $rest_resource_config->getFormats($method))]);
}
if (in_array($method, ['POST', 'PATCH', 'PUT'], TRUE) && !$route->hasRequirement('_content_type_format')) {
$route->addRequirements(['_content_type_format' => implode('|', $rest_resource_config->getFormats($method))]);
}
$route->setOption('_auth', $rest_resource_config->getAuthenticationProviders($method));
$route->setDefault('_rest_resource_config', $rest_resource_config->id());
$parameters = $route->getOption('parameters') ?: [];
$route->setOption('parameters', $parameters + [
'_rest_resource_config' => [
'type' => 'entity:' . $rest_resource_config->getEntityTypeId(),
],
]);
$collection->add("rest.$name", $route);
}
@ -131,4 +146,12 @@ class ResourceRoutes extends RouteSubscriberBase {
return $collection;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents() {
$events[RoutingEvents::DYNAMIC] = 'onDynamicRouteEvent';
return $events;
}
}

View file

@ -306,10 +306,12 @@ abstract class RESTTestBase extends WebTestBase {
return [
'name' => $this->randomMachineName(),
'user_id' => 1,
'field_test_text' => [0 => [
'value' => $this->randomString(),
'format' => 'plain_text',
]],
'field_test_text' => [
0 => [
'value' => $this->randomString(),
'format' => 'plain_text',
],
],
];
case 'config_test':
return [
@ -381,7 +383,7 @@ abstract class RESTTestBase extends WebTestBase {
$resource_config = $this->resourceConfigStorage->create([
'id' => $resource_config_id,
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
'configuration' => []
'configuration' => [],
]);
}
$configuration = $resource_config->get('configuration');
@ -560,7 +562,7 @@ abstract class RESTTestBase extends WebTestBase {
* The first value to check.
* @param $message
* (optional) A message to display with the assertion. Do not translate
* messages: use \Drupal\Component\Utility\SafeMarkup::format() to embed
* messages: use \Drupal\Component\Render\FormattableMarkup to embed
* variables in the message text, not t(). If left blank, a default message
* will be displayed.
* @param $group

View file

@ -1,130 +0,0 @@
<?php
namespace Drupal\rest\Tests;
use Drupal\Core\Session\AccountInterface;
use Drupal\rest\RestResourceConfigInterface;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests the structure of a REST resource.
*
* @group rest
*/
class ResourceTest extends RESTTestBase {
/**
* Modules to install.
*
* @var array
*/
public static $modules = ['hal', 'rest', 'entity_test', 'rest_test'];
/**
* The entity.
*
* @var \Drupal\Core\Entity\EntityInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Create an entity programmatic.
$this->entity = $this->entityCreate('entity_test');
$this->entity->save();
Role::load(AccountInterface::ANONYMOUS_ROLE)
->grantPermission('view test entity')
->save();
}
/**
* Tests that a resource without formats cannot be enabled.
*/
public function testFormats() {
$this->resourceConfigStorage->create([
'id' => 'entity.entity_test',
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
'configuration' => [
'GET' => [
'supported_auth' => [
'basic_auth',
],
],
],
])->save();
// Verify that accessing the resource returns 406.
$response = $this->httpRequest($this->entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
// \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical,
// non-REST route a match, but a lower quality one: no format restrictions
// means there's always a match and hence when there is no matching REST
// route, the non-REST route is used, but can't render into
// application/hal+json, so it returns a 406.
$this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.');
$this->curlClose();
}
/**
* Tests that a resource without authentication cannot be enabled.
*/
public function testAuthentication() {
$this->resourceConfigStorage->create([
'id' => 'entity.entity_test',
'granularity' => RestResourceConfigInterface::METHOD_GRANULARITY,
'configuration' => [
'GET' => [
'supported_formats' => [
'hal_json',
],
],
],
])->save();
// Verify that accessing the resource returns 401.
$response = $this->httpRequest($this->entity->urlInfo()->setRouteParameter('_format', $this->defaultFormat), 'GET');
// \Drupal\Core\Routing\RequestFormatRouteFilter considers the canonical,
// non-REST route a match, but a lower quality one: no format restrictions
// means there's always a match and hence when there is no matching REST
// route, the non-REST route is used, but can't render into
// application/hal+json, so it returns a 406.
$this->assertResponse('406', 'HTTP response code is 406 when the resource does not define formats, because it falls back to the canonical, non-REST route.');
$this->curlClose();
}
/**
* Tests that serialization_class is optional.
*/
public function testSerializationClassIsOptional() {
$this->enableService('serialization_test', 'POST', 'json');
Role::load(RoleInterface::ANONYMOUS_ID)
->grantPermission('restful post serialization_test')
->save();
$serialized = $this->container->get('serializer')->serialize(['foo', 'bar'], 'json');
$this->httpRequest('serialization_test', 'POST', $serialized, 'application/json');
$this->assertResponse(200);
$this->assertResponseBody('["foo","bar"]');
}
/**
* Tests that resource URI paths are formatted properly.
*/
public function testUriPaths() {
$this->enableService('entity:entity_test');
/** @var \Drupal\rest\Plugin\Type\ResourcePluginManager $manager */
$manager = \Drupal::service('plugin.manager.rest');
foreach ($manager->getDefinitions() as $resource => $definition) {
foreach ($definition['uri_paths'] as $key => $uri_path) {
$this->assertFalse(strpos($uri_path, '//'), 'The resource URI path does not have duplicate slashes.');
}
}
}
}

View file

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

View file

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

View file

@ -1,65 +0,0 @@
<?php
namespace Drupal\rest\Tests\Update;
use Drupal\rest\RestResourceConfigInterface;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Tests that rest.settings is converted to rest_resource_config entities.
*
* @see https://www.drupal.org/node/2308745
* @see rest_update_8201()
* @see rest_post_update_create_rest_resource_config_entities()
*
* @group rest
*/
class RestConfigurationEntitiesUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['rest', 'serialization'];
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
__DIR__ . '/../../../../rest/tests/fixtures/update/drupal-8.rest-rest_update_8201.php',
];
}
/**
* Tests rest_update_8201().
*/
public function testResourcesConvertedToConfigEntities() {
/** @var \Drupal\Core\Entity\EntityStorageInterface $resource_config_storage */
$resource_config_storage = $this->container->get('entity_type.manager')->getStorage('rest_resource_config');
// Make sure we have the expected values before the update.
$rest_settings = $this->config('rest.settings');
$this->assertTrue(array_key_exists('resources', $rest_settings->getRawData()));
$this->assertTrue(array_key_exists('entity:node', $rest_settings->getRawData()['resources']));
$resource_config_entities = $resource_config_storage->loadMultiple();
$this->assertIdentical([], array_keys($resource_config_entities));
$this->runUpdates();
// Make sure we have the expected values after the update.
$rest_settings = $this->config('rest.settings');
$this->assertFalse(array_key_exists('resources', $rest_settings->getRawData()));
$resource_config_entities = $resource_config_storage->loadMultiple();
$this->assertIdentical(['entity.node'], array_keys($resource_config_entities));
$node_resource_config_entity = $resource_config_entities['entity.node'];
$this->assertIdentical(RestResourceConfigInterface::RESOURCE_GRANULARITY, $node_resource_config_entity->get('granularity'));
$this->assertIdentical([
'methods' => ['GET'],
'formats' => ['json'],
'authentication' => ['basic_auth'],
], $node_resource_config_entity->get('configuration'));
$this->assertIdentical(['module' => ['basic_auth', 'node', 'serialization']], $node_resource_config_entity->getDependencies());
}
}

View file

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

View file

@ -1,87 +0,0 @@
<?php
namespace Drupal\rest\Tests\Views;
use Drupal\node\Entity\Node;
use Drupal\views\Tests\ViewTestBase;
use Drupal\views\Tests\ViewTestData;
use Drupal\views\Views;
/**
* Tests the display of an excluded field that is used as a token.
*
* @group rest
* @see \Drupal\rest\Plugin\views\display\RestExport
* @see \Drupal\rest\Plugin\views\row\DataFieldRow
*/
class ExcludedFieldTokenTest extends ViewTestBase {
/**
* @var \Drupal\views\ViewExecutable
*/
protected $view;
/**
* The views that are used by this test.
*
* @var array
*/
public static $testViews = ['test_excluded_field_token_display'];
/**
* The modules that need to be installed for this test.
*
* @var array
*/
public static $modules = [
'entity_test',
'rest_test_views',
'node',
'field',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
ViewTestData::createTestViews(get_class($this), ['rest_test_views']);
// Create some test content.
for ($i = 1; $i <= 10; $i++) {
Node::create([
'type' => 'article',
'title' => 'Article test ' . $i,
])->save();
}
$this->enableViewsTestModule();
$this->view = Views::getView('test_excluded_field_token_display');
$this->view->setDisplay('rest_export_1');
}
/**
* Tests the display of an excluded title field when used as a token.
*/
public function testExcludedTitleTokenDisplay() {
$actual_json = $this->drupalGetWithFormat($this->view->getPath(), 'json');
$this->assertResponse(200);
$expected = [
['nothing' => 'Article test 10'],
['nothing' => 'Article test 9'],
['nothing' => 'Article test 8'],
['nothing' => 'Article test 7'],
['nothing' => 'Article test 6'],
['nothing' => 'Article test 5'],
['nothing' => 'Article test 4'],
['nothing' => 'Article test 3'],
['nothing' => 'Article test 2'],
['nothing' => 'Article test 1'],
];
$this->assertIdentical($actual_json, json_encode($expected));
}
}

View file

@ -1,838 +0,0 @@
<?php
namespace Drupal\rest\Tests\Views;
use Drupal\Core\Cache\Cache;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\language\Entity\ConfigurableLanguage;
use Drupal\system\Tests\Cache\AssertPageCacheContextsAndTagsTrait;
use Drupal\views\Entity\View;
use Drupal\views\Plugin\views\display\DisplayPluginBase;
use Drupal\views\Views;
use Drupal\views\Tests\Plugin\PluginTestBase;
use Drupal\views\Tests\ViewTestData;
use Symfony\Component\HttpFoundation\Request;
/**
* Tests the serializer style plugin.
*
* @group rest
* @see \Drupal\rest\Plugin\views\display\RestExport
* @see \Drupal\rest\Plugin\views\style\Serializer
* @see \Drupal\rest\Plugin\views\row\DataEntityRow
* @see \Drupal\rest\Plugin\views\row\DataFieldRow
*/
class StyleSerializerTest extends PluginTestBase {
use AssertPageCacheContextsAndTagsTrait;
/**
* {@inheritdoc}
*/
protected $dumpHeaders = TRUE;
/**
* Modules to install.
*
* @var array
*/
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 = ['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.
*/
protected $adminUser;
protected function setUp() {
parent::setUp();
ViewTestData::createTestViews(get_class($this), ['rest_test_views']);
$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(['name' => 'test_' . $i, 'user_id' => $this->adminUser->id()])->save();
}
$this->enableViewsTestModule();
}
/**
* Checks that the auth options restricts access to a REST views display.
*/
public function testRestViewsAuthentication() {
// Assume the view is hidden behind a permission.
$this->drupalGetWithFormat('test/serialize/auth_with_perm', 'json');
$this->assertResponse(401);
// Not even logging in would make it possible to see the view, because then
// we are denied based on authentication method (cookie).
$this->drupalLogin($this->adminUser);
$this->drupalGetWithFormat('test/serialize/auth_with_perm', 'json');
$this->assertResponse(403);
$this->drupalLogout();
// But if we use the basic auth authentication strategy, we should be able
// to see the page.
$url = $this->buildUrl('test/serialize/auth_with_perm');
$response = \Drupal::httpClient()->get($url, [
'auth' => [$this->adminUser->getUsername(), $this->adminUser->pass_raw],
]);
// Ensure that any changes to variables in the other thread are picked up.
$this->refreshVariables();
$headers = $response->getHeaders();
$this->verbose('GET request to: ' . $url .
'<hr />Code: ' . curl_getinfo($this->curlHandle, CURLINFO_HTTP_CODE) .
'<hr />Response headers: ' . nl2br(print_r($headers, TRUE)) .
'<hr />Response body: ' . (string) $response->getBody());
$this->assertResponse(200);
}
/**
* Checks the behavior of the Serializer callback paths and row plugins.
*/
public function testSerializerResponses() {
// Test the serialize callback.
$view = Views::getView('test_serializer_display_field');
$view->initDisplay();
$this->executeView($view);
$actual_json = $this->drupalGetWithFormat('test/serialize/field', 'json');
$this->assertResponse(200);
$this->assertCacheTags($view->getCacheTags());
$this->assertCacheContexts(['languages:language_interface', 'theme', 'request_format']);
// @todo Due to https://www.drupal.org/node/2352009 we can't yet test the
// propagation of cache max-age.
// Test the http Content-type.
$headers = $this->drupalGetHeaders();
$this->assertEqual($headers['content-type'], 'application/json', 'The header Content-type is correct.');
$expected = [];
foreach ($view->result as $row) {
$expected_row = [];
foreach ($view->field as $id => $field) {
$expected_row[$id] = $field->render($row);
}
$expected[] = $expected_row;
}
$this->assertIdentical($actual_json, json_encode($expected), 'The expected JSON output was found.');
// Test that the rendered output and the preview output are the same.
$view->destroy();
$view->setDisplay('rest_export_1');
// Mock the request content type by setting it on the display handler.
$view->display_handler->setContentType('json');
$output = $view->preview();
$this->assertIdentical($actual_json, (string) drupal_render_root($output), 'The expected JSON preview output was found.');
// Test a 403 callback.
$this->drupalGet('test/serialize/denied');
$this->assertResponse(403);
// Test the entity rows.
$view = Views::getView('test_serializer_display_entity');
$view->initDisplay();
$this->executeView($view);
// Get the serializer service.
$serializer = $this->container->get('serializer');
$entities = [];
foreach ($view->result as $row) {
$entities[] = $row->_entity;
}
$expected = $serializer->serialize($entities, 'json');
$actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'json');
$this->assertResponse(200);
$this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.');
$expected_cache_tags = $view->getCacheTags();
$expected_cache_tags[] = 'entity_test_list';
/** @var \Drupal\Core\Entity\EntityInterface $entity */
foreach ($entities as $entity) {
$expected_cache_tags = Cache::mergeTags($expected_cache_tags, $entity->getCacheTags());
}
$this->assertCacheTags($expected_cache_tags);
$this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']);
$expected = $serializer->serialize($entities, 'hal_json');
$actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'hal_json');
$this->assertIdentical($actual_json, $expected, 'The expected HAL output was found.');
$this->assertCacheTags($expected_cache_tags);
// Change the default format to xml.
$view->setDisplay('rest_export_1');
$view->getDisplay()->setOption('style', [
'type' => 'serializer',
'options' => [
'uses_fields' => FALSE,
'formats' => [
'xml' => 'xml',
],
],
]);
$view->save();
$expected = $serializer->serialize($entities, 'xml');
$actual_xml = $this->drupalGet('test/serialize/entity');
$this->assertIdentical($actual_xml, $expected, 'The expected XML output was found.');
$this->assertCacheContexts(['languages:language_interface', 'theme', 'entity_test_view_grants', 'request_format']);
// Allow multiple formats.
$view->setDisplay('rest_export_1');
$view->getDisplay()->setOption('style', [
'type' => 'serializer',
'options' => [
'uses_fields' => FALSE,
'formats' => [
'xml' => 'xml',
'json' => 'json',
],
],
]);
$view->save();
$expected = $serializer->serialize($entities, 'json');
$actual_json = $this->drupalGetWithFormat('test/serialize/entity', 'json');
$this->assertIdentical($actual_json, $expected, 'The expected JSON output was found.');
$expected = $serializer->serialize($entities, 'xml');
$actual_xml = $this->drupalGetWithFormat('test/serialize/entity', 'xml');
$this->assertIdentical($actual_xml, $expected, 'The expected XML output was found.');
}
/**
* Verifies site maintenance mode functionality.
*/
public function testSiteMaintenance() {
$view = Views::getView('test_serializer_display_field');
$view->initDisplay();
$this->executeView($view);
// Set the site to maintenance mode.
$this->container->get('state')->set('system.maintenance_mode', TRUE);
$this->drupalGetWithFormat('test/serialize/entity', 'json');
// Verify that the endpoint is unavailable for anonymous users.
$this->assertResponse(503);
}
/**
* Sets up a request on the request stack with a specified format.
*
* @param string $format
* The new request format.
*/
protected function addRequestWithFormat($format) {
$request = \Drupal::request();
$request = clone $request;
$request->setRequestFormat($format);
\Drupal::requestStack()->push($request);
}
/**
* Tests REST export with views render caching enabled.
*/
public function testRestRenderCaching() {
$this->drupalLogin($this->adminUser);
/** @var \Drupal\Core\Render\RenderCacheInterface $render_cache */
$render_cache = \Drupal::service('render_cache');
// Enable render caching for the views.
/** @var \Drupal\views\ViewEntityInterface $storage */
$storage = View::load('test_serializer_display_entity');
$options = &$storage->getDisplay('default');
$options['display_options']['cache'] = [
'type' => 'tag',
];
$storage->save();
$original = DisplayPluginBase::buildBasicRenderable('test_serializer_display_entity', 'rest_export_1');
// Ensure that there is no corresponding render cache item yet.
$original['#cache'] += ['contexts' => []];
$original['#cache']['contexts'] = Cache::mergeContexts($original['#cache']['contexts'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
$cache_tags = [
'config:views.view.test_serializer_display_entity',
'entity_test:1',
'entity_test:10',
'entity_test:2',
'entity_test:3',
'entity_test:4',
'entity_test:5',
'entity_test:6',
'entity_test:7',
'entity_test:8',
'entity_test:9',
'entity_test_list'
];
$cache_contexts = [
'entity_test_view_grants',
'languages:language_interface',
'theme',
'request_format',
];
$this->assertFalse($render_cache->get($original));
// Request the page, once in XML and once in JSON to ensure that the caching
// varies by it.
$result1 = $this->drupalGetJSON('test/serialize/entity');
$this->addRequestWithFormat('json');
$this->assertHeader('content-type', 'application/json');
$this->assertCacheContexts($cache_contexts);
$this->assertCacheTags($cache_tags);
$this->assertTrue($render_cache->get($original));
$result_xml = $this->drupalGetWithFormat('test/serialize/entity', 'xml');
$this->addRequestWithFormat('xml');
$this->assertHeader('content-type', 'text/xml; charset=UTF-8');
$this->assertCacheContexts($cache_contexts);
$this->assertCacheTags($cache_tags);
$this->assertTrue($render_cache->get($original));
// Ensure that the XML output is different from the JSON one.
$this->assertNotEqual($result1, $result_xml);
// Ensure that the cached page works.
$result2 = $this->drupalGetJSON('test/serialize/entity');
$this->addRequestWithFormat('json');
$this->assertHeader('content-type', 'application/json');
$this->assertEqual($result2, $result1);
$this->assertCacheContexts($cache_contexts);
$this->assertCacheTags($cache_tags);
$this->assertTrue($render_cache->get($original));
// Create a new entity and ensure that the cache tags are taken over.
EntityTest::create(['name' => 'test_11', 'user_id' => $this->adminUser->id()])->save();
$result3 = $this->drupalGetJSON('test/serialize/entity');
$this->addRequestWithFormat('json');
$this->assertHeader('content-type', 'application/json');
$this->assertNotEqual($result3, $result2);
// Add the new entity cache tag and remove the first one, because we just
// show 10 items in total.
$cache_tags[] = 'entity_test:11';
unset($cache_tags[array_search('entity_test:1', $cache_tags)]);
$this->assertCacheContexts($cache_contexts);
$this->assertCacheTags($cache_tags);
$this->assertTrue($render_cache->get($original));
}
/**
* Tests the response format configuration.
*/
public function testResponseFormatConfiguration() {
$this->drupalLogin($this->adminUser);
$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, ['style_options[formats][xml]' => 'xml'], t('Apply'));
$this->drupalPostForm(NULL, [], t('Save'));
// Should return a 406.
$this->drupalGetWithFormat('test/serialize/field', 'json');
$this->assertHeader('content-type', 'application/json');
$this->assertResponse(406, 'A 406 response was returned when JSON was requested.');
// Should return a 200.
$this->drupalGetWithFormat('test/serialize/field', 'xml');
$this->assertHeader('content-type', 'text/xml; charset=UTF-8');
$this->assertResponse(200, 'A 200 response was returned when XML was requested.');
// Add 'json' as an accepted format, so we have multiple.
$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.
$this->drupalGet('test/serialize/field');
$this->assertHeader('content-type', 'application/json');
$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', [], ['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.');
// Should return a 200.
$this->drupalGetWithFormat('test/serialize/field', 'json');
$this->assertHeader('content-type', 'application/json');
$this->assertResponse(200, 'A 200 response was returned when JSON was requested.');
$headers = $this->drupalGetHeaders();
$this->assertEqual($headers['content-type'], 'application/json', 'The header Content-type is correct.');
// Should return a 200.
$this->drupalGetWithFormat('test/serialize/field', 'xml');
$this->assertHeader('content-type', 'text/xml; charset=UTF-8');
$this->assertResponse(200, 'A 200 response was returned when XML was requested');
$headers = $this->drupalGetHeaders();
$this->assertTrue(strpos($headers['content-type'], 'text/xml') !== FALSE, 'The header Content-type is correct.');
// Should return a 406.
$this->drupalGetWithFormat('test/serialize/field', 'html');
// We want to show the first format by default, see
// \Drupal\rest\Plugin\views\style\Serializer::render.
$this->assertHeader('content-type', 'application/json');
$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, ['style_options[formats][json]' => '0', 'style_options[formats][xml]' => '0'], t('Apply'));
// Should return a 200.
$this->drupalGetWithFormat('test/serialize/field', 'json');
$this->assertHeader('content-type', 'application/json');
$this->assertResponse(200, 'A 200 response was returned when JSON was requested.');
// Should return a 200.
$this->drupalGetWithFormat('test/serialize/field', 'xml');
$this->assertHeader('content-type', 'text/xml; charset=UTF-8');
$this->assertResponse(200, 'A 200 response was returned when XML was requested');
// Should return a 200.
$this->drupalGetWithFormat('test/serialize/field', 'html');
// We want to show the first format by default, see
// \Drupal\rest\Plugin\views\style\Serializer::render.
$this->assertHeader('content-type', 'application/json');
$this->assertResponse(200, 'A 200 response was returned when HTML was requested.');
}
/**
* Test the field ID alias functionality of the DataFieldRow plugin.
*/
public function testUIFieldAlias() {
$this->drupalLogin($this->adminUser);
// Test the UI settings for adding field ID aliases.
$this->drupalGet('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1');
$row_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options';
$this->assertLinkByHref($row_options);
// 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, ['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 = [];
foreach ($view->result as $row) {
$expected_row = [];
foreach ($view->field as $id => $field) {
$expected_row[$id] = $field->render($row);
}
$expected[] = $expected_row;
}
$this->assertIdentical($this->drupalGetJSON('test/serialize/field'), $this->castSafeStrings($expected));
// Test a random aliases for fields, they should be replaced.
$alias_map = [
'name' => $this->randomMachineName(),
// Use # to produce an invalid character for the validation.
'nothing' => '#' . $this->randomMachineName(),
'created' => 'created',
];
$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 = ['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, [], t('Save'));
$view = Views::getView('test_serializer_display_field');
$view->setDisplay('rest_export_1');
$this->executeView($view);
$expected = [];
foreach ($view->result as $row) {
$expected_row = [];
foreach ($view->field as $id => $field) {
$expected_row[$alias_map[$id]] = $field->render($row);
}
$expected[] = $expected_row;
}
$this->assertIdentical($this->drupalGetJSON('test/serialize/field'), $this->castSafeStrings($expected));
}
/**
* Tests the raw output options for row field rendering.
*/
public function testFieldRawOutput() {
$this->drupalLogin($this->adminUser);
// Test the UI settings for adding field ID aliases.
$this->drupalGet('admin/structure/views/view/test_serializer_display_field/edit/rest_export_1');
$row_options = 'admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options';
$this->assertLinkByHref($row_options);
// 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 = [
'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, [], t('Save'));
$view = Views::getView('test_serializer_display_field');
$view->setDisplay('rest_export_1');
$this->executeView($view);
$storage = $this->container->get('entity_type.manager')->getStorage('entity_test');
// Update the name for each to include a script tag.
foreach ($storage->loadMultiple() as $entity_test) {
$name = $entity_test->name->value;
$entity_test->set('name', "<script>$name</script>");
$entity_test->save();
}
// Just test the raw 'created' value against each row.
foreach ($this->drupalGetJSON('test/serialize/field') as $index => $values) {
$this->assertIdentical($values['created'], $view->result[$index]->views_test_data_created, 'Expected raw created value found.');
$this->assertIdentical($values['name'], $view->result[$index]->views_test_data_name, 'Expected raw name value found.');
}
// Test result with an excluded field.
$view->setDisplay('rest_export_1');
$view->displayHandlers->get('rest_export_1')->overrideOption('fields', [
'name' => [
'id' => 'name',
'table' => 'views_test_data',
'field' => 'name',
'relationship' => 'none',
],
'created' => [
'id' => 'created',
'exclude' => TRUE,
'table' => 'views_test_data',
'field' => 'created',
'relationship' => 'none',
],
]);
$view->save();
$this->executeView($view);
foreach ($this->drupalGetJSON('test/serialize/field') as $index => $values) {
$this->assertTrue(!isset($values['created']), 'Excluded value not found.');
}
// Test that the excluded field is not shown in the row options.
$this->drupalGet('admin/structure/views/nojs/display/test_serializer_display_field/rest_export_1/row_options');
$this->assertNoText('created');
}
/**
* Tests the live preview output for json output.
*/
public function testLivePreview() {
// We set up a request so it looks like an request in the live preview.
$request = new Request();
$request->query->add([MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']);
/** @var \Symfony\Component\HttpFoundation\RequestStack $request_stack */
$request_stack = \Drupal::service('request_stack');
$request_stack->push($request);
$view = Views::getView('test_serializer_display_entity');
$view->setDisplay('rest_export_1');
$this->executeView($view);
// Get the serializer service.
$serializer = $this->container->get('serializer');
$entities = [];
foreach ($view->result as $row) {
$entities[] = $row->_entity;
}
$expected = $serializer->serialize($entities, 'json');
$view->live_preview = TRUE;
$build = $view->preview();
$rendered_json = $build['#plain_text'];
$this->assertTrue(!isset($build['#markup']) && $rendered_json == $expected, 'Ensure the previewed json is escaped.');
$view->destroy();
$expected = $serializer->serialize($entities, 'xml');
// Change the request format to xml.
$view->setDisplay('rest_export_1');
$view->getDisplay()->setOption('style', [
'type' => 'serializer',
'options' => [
'uses_fields' => FALSE,
'formats' => [
'xml' => 'xml',
],
],
]);
$this->executeView($view);
$build = $view->preview();
$rendered_xml = $build['#plain_text'];
$this->assertEqual($rendered_xml, $expected, 'Ensure we preview xml when we change the request format.');
}
/**
* Tests the views interface for REST export displays.
*/
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 = [], t('Update preview'));
$this->assertResponse(200);
// Check if we receive the expected result.
$result = $this->xpath('//div[@id="views-live-preview"]/pre');
$this->assertIdentical($this->drupalGet('test/serialize/field'), (string) $result[0], 'The expected JSON preview output was found.');
}
/**
* Tests the field row style using fieldapi fields.
*/
public function testFieldapiField() {
$this->drupalCreateContentType(['type' => 'page']);
$node = $this->drupalCreateNode();
$result = $this->drupalGetJSON('test/serialize/node-field');
$this->assertEqual($result[0]['nid'], $node->id());
$this->assertEqual($result[0]['body'], $node->body->processed);
// Make sure that serialized fields are not exposed to XSS.
$node = $this->drupalCreateNode();
$node->body = [
'value' => '<script type="text/javascript">alert("node-body");</script>' . $this->randomMachineName(32),
'format' => filter_default_format(),
];
$node->save();
$result = $this->drupalGetJSON('test/serialize/node-field');
$this->assertEqual($result[1]['nid'], $node->id());
$this->assertTrue(strpos($this->getRawContent(), "<script") === FALSE, "No script tag is present in the raw page contents.");
$this->drupalLogin($this->adminUser);
// Add an alias and make the output raw.
$row_options = 'admin/structure/views/nojs/display/test_serializer_node_display_field/rest_export_1/row_options';
// 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, ['row_options[field_options][title][raw_output]' => '1'], t('Apply'));
$this->drupalPostForm(NULL, [], t('Save'));
$view = Views::getView('test_serializer_node_display_field');
$view->setDisplay('rest_export_1');
$this->executeView($view);
// Test the raw 'created' value against each row.
foreach ($this->drupalGetJSON('test/serialize/node-field') as $index => $values) {
$this->assertIdentical($values['title'], $view->result[$index]->_entity->title->value, 'Expected raw title value found.');
}
// Test that multiple raw body fields are shown.
// Make the body field unlimited cardinatlity.
$storage_definition = $node->getFieldDefinition('body')->getFieldStorageDefinition();
$storage_definition->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED);
$storage_definition->save();
$this->drupalPostForm($row_options, ['row_options[field_options][body][raw_output]' => '1'], t('Apply'));
$this->drupalPostForm(NULL, [], t('Save'));
$node = $this->drupalCreateNode();
$body = [
'value' => '<script type="text/javascript">alert("node-body");</script>' . $this->randomMachineName(32),
'format' => filter_default_format(),
];
// Add two body items.
$node->body = [$body, $body];
$node->save();
$view = Views::getView('test_serializer_node_display_field');
$view->setDisplay('rest_export_1');
$this->executeView($view);
$result = $this->drupalGetJSON('test/serialize/node-field');
$this->assertEqual(count($result[2]['body']), $node->body->count(), 'Expected count of values');
$this->assertEqual($result[2]['body'], array_map(function($item) { return $item['value']; }, $node->body->getValue()), 'Expected raw body values found.');
}
/**
* Tests the "Grouped rows" functionality.
*/
public function testGroupRows() {
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = $this->container->get('renderer');
$this->drupalCreateContentType(['type' => 'page']);
// Create a text field with cardinality set to unlimited.
$field_name = 'field_group_rows';
$field_storage = FieldStorageConfig::create([
'field_name' => $field_name,
'entity_type' => 'node',
'type' => 'string',
'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED,
]);
$field_storage->save();
// Create an instance of the text field on the content type.
$field = FieldConfig::create([
'field_storage' => $field_storage,
'bundle' => 'page',
]);
$field->save();
$grouped_field_values = ['a', 'b', 'c'];
$edit = [
'title' => $this->randomMachineName(),
$field_name => $grouped_field_values,
];
$this->drupalCreateNode($edit);
$view = Views::getView('test_serializer_node_display_field');
$view->setDisplay('rest_export_1');
// Override the view's fields to include the field_group_rows field, set the
// group_rows setting to true.
$fields = [
$field_name => [
'id' => $field_name,
'table' => 'node__' . $field_name,
'field' => $field_name,
'type' => 'string',
'group_rows' => TRUE,
],
];
$view->displayHandlers->get('default')->overrideOption('fields', $fields);
$build = $view->preview();
// Get the serializer service.
$serializer = $this->container->get('serializer');
// Check if the field_group_rows field is grouped.
$expected = [];
$expected[] = [$field_name => implode(', ', $grouped_field_values)];
$this->assertEqual($serializer->serialize($expected, 'json'), (string) $renderer->renderRoot($build));
// Set the group rows setting to false.
$view = Views::getView('test_serializer_node_display_field');
$view->setDisplay('rest_export_1');
$fields[$field_name]['group_rows'] = FALSE;
$view->displayHandlers->get('default')->overrideOption('fields', $fields);
$build = $view->preview();
// Check if the field_group_rows field is ungrouped and displayed per row.
$expected = [];
foreach ($grouped_field_values as $grouped_field_value) {
$expected[] = [$field_name => $grouped_field_value];
}
$this->assertEqual($serializer->serialize($expected, 'json'), (string) $renderer->renderRoot($build));
}
/**
* Tests the exposed filter works.
*
* There is an exposed filter on the title field which takes a title query
* parameter. This is set to filter nodes by those whose title starts with
* the value provided.
*/
public function testRestViewExposedFilter() {
$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 = [
0 => [
'nid' => $node0->id(),
'body' => $node0->body->processed,
],
1 => [
'nid' => $node1->id(),
'body' => $node1->body->processed,
],
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 = [
0 => [
'nid' => $node1->id(),
'body' => $node1->body->processed,
],
1 => [
'nid' => $node2->id(),
'body' => $node2->body->processed,
],
];
$cache_contexts = [
'languages:language_content',
'languages:language_interface',
'theme',
'request_format',
'user.node_grants:view',
'url',
];
$this->assertEqual($result, $expected, 'Querying a view with a starts with exposed filter on the title returns nodes whose title starts with value provided.');
$this->assertCacheContexts($cache_contexts);
}
/**
* Test multilingual entity rows.
*/
public function testMulEntityRows() {
// Create some languages.
ConfigurableLanguage::createFromLangcode('l1')->save();
ConfigurableLanguage::createFromLangcode('l2')->save();
// Create an entity with no translations.
$storage = \Drupal::entityTypeManager()->getStorage('entity_test_mul');
$storage->create(['langcode' => 'l1', 'name' => 'mul-none'])->save();
// Create some entities with translations.
$entity = $storage->create(['langcode' => 'l1', 'name' => 'mul-l1-orig']);
$entity->save();
$entity->addTranslation('l2', ['name' => 'mul-l1-l2'])->save();
$entity = $storage->create(['langcode' => 'l2', 'name' => 'mul-l2-orig']);
$entity->save();
$entity->addTranslation('l1', ['name' => 'mul-l2-l1'])->save();
// Get the names of the output.
$json = $this->drupalGetWithFormat('test/serialize/translated_entity', 'json');
$decoded = $this->container->get('serializer')->decode($json, 'hal_json');
$names = [];
foreach ($decoded as $item) {
$names[] = $item['name'][0]['value'];
}
sort($names);
// Check that the names are correct.
$expected = ['mul-l1-l2', 'mul-l1-orig', 'mul-l2-l1', 'mul-l2-orig', 'mul-none'];
$this->assertIdentical($names, $expected, 'The translated content was found in the JSON.');
}
}