Issue #2019123 by klausi, ygerasimov, Crell: Use the same canonical URI paths as for HTML routes.
parent
743957c421
commit
5f61e2663a
|
@ -352,8 +352,13 @@ services:
|
|||
password:
|
||||
class: Drupal\Core\Password\PhpassHashedPassword
|
||||
arguments: [16]
|
||||
mime_type_matcher:
|
||||
class: Drupal\Core\Routing\MimeTypeMatcher
|
||||
accept_header_matcher:
|
||||
class: Drupal\Core\Routing\AcceptHeaderMatcher
|
||||
arguments: ['@content_negotiation']
|
||||
tags:
|
||||
- { name: route_filter }
|
||||
content_type_header_matcher:
|
||||
class: Drupal\Core\Routing\ContentTypeHeaderMatcher
|
||||
tags:
|
||||
- { name: route_filter }
|
||||
paramconverter_manager:
|
||||
|
@ -427,6 +432,10 @@ services:
|
|||
class: Drupal\Core\EventSubscriber\SpecialAttributesRouteSubscriber
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
route_http_method_subscriber:
|
||||
class: Drupal\Core\EventSubscriber\RouteMethodSubscriber
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
controller.page:
|
||||
class: Drupal\Core\Controller\HtmlPageController
|
||||
arguments: ['@controller_resolver', '@string_translation', '@title_resolver']
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Core\EventSubscriber\RouteMethodSubscriber.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Routing\RouteBuildEvent;
|
||||
use Drupal\Core\Routing\RoutingEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* Provides a default value for the HTTP method restriction on routes.
|
||||
*
|
||||
* Most routes will only deal with GET and POST requests, so we restrict them to
|
||||
* those two if nothing else is specified. This is necessary to give other
|
||||
* routes a chance during the route matching process when they are listening
|
||||
* for example to DELETE requests on the same path. A typical use case are REST
|
||||
* web service routes that use the full spectrum of HTTP methods.
|
||||
*/
|
||||
class RouteMethodSubscriber implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* Sets a default value of GET|POST for the _method route property.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteBuildEvent $event
|
||||
* The event containing the build routes.
|
||||
*/
|
||||
public function onRouteBuilding(RouteBuildEvent $event) {
|
||||
foreach ($event->getRouteCollection() as $route) {
|
||||
$methods = $route->getMethods();
|
||||
if (empty($methods)) {
|
||||
$route->setMethods(array('GET', 'POST'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
static function getSubscribedEvents() {
|
||||
// Set a higher priority to ensure that routes get the default HTTP methods
|
||||
// as early as possible.
|
||||
$events[RoutingEvents::ALTER][] = array('onRouteBuilding', 5000);
|
||||
return $events;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains Drupal\Core\Routing\AcceptHeaderMatcher.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Routing;
|
||||
|
||||
use Drupal\Component\Utility\String;
|
||||
use Drupal\Core\ContentNegotiation;
|
||||
use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Filters routes based on the media type specified in the HTTP Accept headers.
|
||||
*/
|
||||
class AcceptHeaderMatcher implements RouteFilterInterface {
|
||||
|
||||
/**
|
||||
* The content negotiation library.
|
||||
*
|
||||
* @var \Drupal\Core\ContentNegotiation
|
||||
*/
|
||||
protected $contentNegotiation;
|
||||
|
||||
/**
|
||||
* Constructs a new AcceptHeaderMatcher.
|
||||
*
|
||||
* @param \Drupal\Core\ContentNegotiation $cotent_negotiation
|
||||
* The content negotiation library.
|
||||
*/
|
||||
public function __construct(ContentNegotiation $content_negotiation) {
|
||||
$this->contentNegotiation = $content_negotiation;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function filter(RouteCollection $collection, Request $request) {
|
||||
// Generates a list of Symfony formats matching the acceptable MIME types.
|
||||
// @todo replace by proper content negotiation library.
|
||||
$acceptable_mime_types = $request->getAcceptableContentTypes();
|
||||
$acceptable_formats = array_filter(array_map(array($request, 'getFormat'), $acceptable_mime_types));
|
||||
$primary_format = $this->contentNegotiation->getContentType($request);
|
||||
|
||||
foreach ($collection as $name => $route) {
|
||||
// _format could be a |-delimited list of supported formats.
|
||||
$supported_formats = array_filter(explode('|', $route->getRequirement('_format')));
|
||||
|
||||
if (empty($supported_formats)) {
|
||||
// No format restriction on the route, so it always matches. Move it to
|
||||
// the end of the collection by re-adding it.
|
||||
$collection->add($name, $route);
|
||||
}
|
||||
elseif (in_array($primary_format, $supported_formats)) {
|
||||
// Perfect match, which will get a higher priority by leaving the route
|
||||
// on top of the list.
|
||||
}
|
||||
// The route partially matches if it doesn't care about format, if it
|
||||
// explicitly allows any format, or if one of its allowed formats is
|
||||
// in the request's list of acceptable formats.
|
||||
elseif (in_array('*/*', $acceptable_mime_types) || array_intersect($acceptable_formats, $supported_formats)) {
|
||||
// Move it to the end of the list.
|
||||
$collection->add($name, $route);
|
||||
}
|
||||
else {
|
||||
// Remove the route if it does not match at all.
|
||||
$collection->remove($name);
|
||||
}
|
||||
}
|
||||
|
||||
if (count($collection)) {
|
||||
return $collection;
|
||||
}
|
||||
|
||||
// We do not throw a
|
||||
// \Symfony\Component\Routing\Exception\ResourceNotFoundException here
|
||||
// because we don't want to return a 404 status code, but rather a 406.
|
||||
throw new NotAcceptableHttpException(String::format('No route found for the specified formats @formats.', array('@formats' => implode(' ', $acceptable_mime_types))));
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains Drupal\Core\Routing\ContentTypeHeaderMatcher.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Routing;
|
||||
|
||||
use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
|
||||
/**
|
||||
* Filters routes based on the HTTP Content-type header.
|
||||
*/
|
||||
class ContentTypeHeaderMatcher implements RouteFilterInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function filter(RouteCollection $collection, Request $request) {
|
||||
// The Content-type header does not make sense on GET requests, because GET
|
||||
// requests do not carry any content. Nothing to filter in this case.
|
||||
if ($request->isMethod('GET')) {
|
||||
return $collection;
|
||||
}
|
||||
|
||||
$format = $request->getContentType();
|
||||
|
||||
foreach ($collection as $name => $route) {
|
||||
$supported_formats = array_filter(explode('|', $route->getRequirement('_content_type_format')));
|
||||
if (empty($supported_formats)) {
|
||||
// No restriction on the route, so we move the route to the end of the
|
||||
// collection by re-adding it. That way generic routes sink down in the
|
||||
// list and exact matching routes stay on top.
|
||||
$collection->add($name, $route);
|
||||
}
|
||||
elseif (!in_array($format, $supported_formats)) {
|
||||
$collection->remove($name);
|
||||
}
|
||||
}
|
||||
if (count($collection)) {
|
||||
return $collection;
|
||||
}
|
||||
// We do not throw a
|
||||
// \Symfony\Component\Routing\Exception\ResourceNotFoundException here
|
||||
// because we don't want to return a 404 status code, but rather a 415.
|
||||
throw new UnsupportedMediaTypeHttpException('No route found that matches the Content-Type header.');
|
||||
}
|
||||
|
||||
}
|
|
@ -1,50 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains Drupal\Core\Routing\MimeTypeMatcher.
|
||||
*/
|
||||
|
||||
namespace Drupal\Core\Routing;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
|
||||
use Symfony\Component\Routing\RouteCollection;
|
||||
use Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface;
|
||||
|
||||
/**
|
||||
* This class filters routes based on the media type in HTTP Accept headers.
|
||||
*/
|
||||
class MimeTypeMatcher implements RouteFilterInterface {
|
||||
|
||||
|
||||
/**
|
||||
* Implements \Symfony\Cmf\Component\Routing\NestedMatcher\RouteFilterInterface::filter()
|
||||
*/
|
||||
public function filter(RouteCollection $collection, Request $request) {
|
||||
// Generates a list of Symfony formats matching the acceptable MIME types.
|
||||
// @todo replace by proper content negotiation library.
|
||||
$acceptable_mime_types = $request->getAcceptableContentTypes();
|
||||
$acceptable_formats = array_map(array($request, 'getFormat'), $acceptable_mime_types);
|
||||
|
||||
$filtered_collection = new RouteCollection();
|
||||
|
||||
foreach ($collection as $name => $route) {
|
||||
// _format could be a |-delimited list of supported formats.
|
||||
$supported_formats = array_filter(explode('|', $route->getRequirement('_format')));
|
||||
// The route partially matches if it doesn't care about format, if it
|
||||
// explicitly allows any format, or if one of its allowed formats is
|
||||
// in the request's list of acceptable formats.
|
||||
if (empty($supported_formats) || in_array('*/*', $acceptable_mime_types) || array_intersect($acceptable_formats, $supported_formats)) {
|
||||
$filtered_collection->add($name, $route);
|
||||
}
|
||||
}
|
||||
|
||||
if (!count($filtered_collection)) {
|
||||
throw new NotAcceptableHttpException();
|
||||
}
|
||||
|
||||
return $filtered_collection;
|
||||
}
|
||||
|
||||
}
|
|
@ -9,7 +9,9 @@ namespace Drupal\rest\Plugin\Derivative;
|
|||
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDerivativeInterface;
|
||||
use Drupal\Core\Routing\RouteProviderInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Routing\Exception\RouteNotFoundException;
|
||||
|
||||
/**
|
||||
* Provides a resource plugin definition for every entity type.
|
||||
|
@ -30,14 +32,24 @@ class EntityDerivative implements ContainerDerivativeInterface {
|
|||
*/
|
||||
protected $entityManager;
|
||||
|
||||
/**
|
||||
* The route provider.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\RouteProviderInterface
|
||||
*/
|
||||
protected $routeProvider;
|
||||
|
||||
/**
|
||||
* Constructs an EntityDerivative object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
|
||||
* The entity manager.
|
||||
* @param \Drupal\Core\Routing\RouteProviderInterface $route_provider
|
||||
* The route provider.
|
||||
*/
|
||||
public function __construct(EntityManagerInterface $entity_manager) {
|
||||
public function __construct(EntityManagerInterface $entity_manager, RouteProviderInterface $route_provider) {
|
||||
$this->entityManager = $entity_manager;
|
||||
$this->routeProvider = $route_provider;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -45,7 +57,8 @@ class EntityDerivative implements ContainerDerivativeInterface {
|
|||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$container->get('entity.manager')
|
||||
$container->get('entity.manager'),
|
||||
$container->get('router.route_provider')
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -74,6 +87,36 @@ class EntityDerivative implements ContainerDerivativeInterface {
|
|||
'serialization_class' => $entity_type->getClass(),
|
||||
'label' => $entity_type->getLabel(),
|
||||
);
|
||||
|
||||
$default_uris = array(
|
||||
'canonical' => "/entity/$entity_type_id/" . '{' . $entity_type_id . '}',
|
||||
'http://drupal.org/link-relations/create' => "/entity/$entity_type_id",
|
||||
);
|
||||
|
||||
foreach ($default_uris as $link_relation => $default_uri) {
|
||||
// Check if there are link templates defined for the entity type and
|
||||
// use the path from the route instead of the default.
|
||||
if ($route_name = $entity_type->getLinkTemplate($link_relation)) {
|
||||
// @todo remove the try/catch as part of
|
||||
// http://drupal.org/node/2158571
|
||||
try {
|
||||
$route = $this->routeProvider->getRouteByName($route_name);
|
||||
$this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = $route->getPath();
|
||||
}
|
||||
catch (RouteNotFoundException $e) {
|
||||
// If the route does not exist it means we are in a brittle state
|
||||
// of module enabling/disabling, so we simply exclude this entity
|
||||
// type.
|
||||
unset($this->derivatives[$entity_type_id]);
|
||||
// Continue with the next entity type;
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$this->derivatives[$entity_type_id]['uri_paths'][$link_relation] = $default_uri;
|
||||
}
|
||||
}
|
||||
|
||||
$this->derivatives[$entity_type_id] += $base_plugin_definition;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -78,13 +78,17 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
|
|||
*/
|
||||
public function routes() {
|
||||
$collection = new RouteCollection();
|
||||
$path_prefix = strtr($this->pluginId, ':', '/');
|
||||
|
||||
$definition = $this->getPluginDefinition();
|
||||
$canonical_path = isset($definition['uri_paths']['canonical']) ? $definition['uri_paths']['canonical'] : '/' . strtr($this->pluginId, ':', '/') . '/{id}';
|
||||
$create_path = isset($definition['uri_paths']['http://drupal.org/link-relations/create']) ? $definition['uri_paths']['http://drupal.org/link-relations/create'] : '/' . strtr($this->pluginId, ':', '/');
|
||||
|
||||
$route_name = strtr($this->pluginId, ':', '.');
|
||||
|
||||
$methods = $this->availableMethods();
|
||||
foreach ($methods as $method) {
|
||||
$lower_method = strtolower($method);
|
||||
$route = new Route("/$path_prefix/{id}", array(
|
||||
$route = new Route($canonical_path, array(
|
||||
'_controller' => 'Drupal\rest\RequestHandler::handle',
|
||||
// Pass the resource plugin ID along as default property.
|
||||
'_plugin' => $this->pluginId,
|
||||
|
@ -98,9 +102,17 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
|
|||
|
||||
switch ($method) {
|
||||
case 'POST':
|
||||
// POST routes do not require an ID in the URL path.
|
||||
$route->setPattern("/$path_prefix");
|
||||
$route->addDefaults(array('id' => NULL));
|
||||
$route->setPattern($create_path);
|
||||
// Restrict the incoming HTTP Content-type header to the known
|
||||
// serialization formats.
|
||||
$route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats)));
|
||||
$collection->add("$route_name.$method", $route);
|
||||
break;
|
||||
|
||||
case 'PATCH':
|
||||
// Restrict the incoming HTTP Content-type header to the known
|
||||
// serialization formats.
|
||||
$route->addRequirements(array('_content_type_format' => implode('|', $this->serializerFormats)));
|
||||
$collection->add("$route_name.$method", $route);
|
||||
break;
|
||||
|
||||
|
@ -110,7 +122,6 @@ abstract class ResourceBase extends PluginBase implements ContainerFactoryPlugin
|
|||
// HTTP Accept headers.
|
||||
foreach ($this->serializerFormats as $format_name) {
|
||||
// Expose one route per available format.
|
||||
//$format_route = new Route($route->getPath(), $route->getDefaults(), $route->getRequirements());
|
||||
$format_route = clone $route;
|
||||
$format_route->addRequirements(array('_format' => $format_name));
|
||||
$collection->add("$route_name.$method.$format_name", $format_route);
|
||||
|
|
|
@ -17,7 +17,10 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|||
*
|
||||
* @RestResource(
|
||||
* id = "dblog",
|
||||
* label = @Translation("Watchdog database log")
|
||||
* label = @Translation("Watchdog database log"),
|
||||
* uri_paths = {
|
||||
* "canonical" = "/dblog/{id}"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class DBLogResource extends ResourceBase {
|
||||
|
|
|
@ -14,7 +14,6 @@ use Drupal\rest\ResourceResponse;
|
|||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Represents entities as resources.
|
||||
|
@ -23,7 +22,11 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|||
* id = "entity",
|
||||
* label = @Translation("Entity"),
|
||||
* serialization_class = "Drupal\Core\Entity\Entity",
|
||||
* derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative"
|
||||
* derivative = "Drupal\rest\Plugin\Derivative\EntityDerivative",
|
||||
* uri_paths = {
|
||||
* "canonical" = "/entity/{entity_type}/{entity}",
|
||||
* "http://drupal.org/link-relations/create" = "/entity/{entity_type}"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class EntityResource extends ResourceBase {
|
||||
|
@ -31,36 +34,29 @@ class EntityResource extends ResourceBase {
|
|||
/**
|
||||
* Responds to entity GET requests.
|
||||
*
|
||||
* @param mixed $id
|
||||
* The entity ID.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity object.
|
||||
*
|
||||
* @return \Drupal\rest\ResourceResponse
|
||||
* The response containing the loaded entity.
|
||||
* The response containing the entity with its accessible fields.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
public function get($id) {
|
||||
$definition = $this->getPluginDefinition();
|
||||
$entity = entity_load($definition['entity_type'], $id);
|
||||
if ($entity) {
|
||||
if (!$entity->access('view')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
foreach ($entity as $field_name => $field) {
|
||||
if (!$field->access('view')) {
|
||||
unset($entity->{$field_name});
|
||||
}
|
||||
}
|
||||
return new ResourceResponse($entity);
|
||||
public function get(EntityInterface $entity) {
|
||||
if (!$entity->access('view')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
|
||||
foreach ($entity as $field_name => $field) {
|
||||
if (!$field->access('view')) {
|
||||
unset($entity->{$field_name});
|
||||
}
|
||||
}
|
||||
return new ResourceResponse($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Responds to entity POST requests and saves the new entity.
|
||||
*
|
||||
* @param mixed $id
|
||||
* Ignored. A new entity is created with a new ID.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity.
|
||||
*
|
||||
|
@ -69,7 +65,7 @@ class EntityResource extends ResourceBase {
|
|||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
public function post($id, EntityInterface $entity = NULL) {
|
||||
public function post(EntityInterface $entity = NULL) {
|
||||
if ($entity == NULL) {
|
||||
throw new BadRequestHttpException(t('No entity content received.'));
|
||||
}
|
||||
|
@ -112,8 +108,8 @@ class EntityResource extends ResourceBase {
|
|||
/**
|
||||
* Responds to entity PATCH requests.
|
||||
*
|
||||
* @param mixed $id
|
||||
* The entity ID.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $original_entity
|
||||
* The original entity object.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity.
|
||||
*
|
||||
|
@ -122,24 +118,14 @@ class EntityResource extends ResourceBase {
|
|||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
public function patch($id, EntityInterface $entity = NULL) {
|
||||
public function patch(EntityInterface $original_entity, EntityInterface $entity = NULL) {
|
||||
if ($entity == NULL) {
|
||||
throw new BadRequestHttpException(t('No entity content received.'));
|
||||
}
|
||||
|
||||
if (empty($id)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
$definition = $this->getPluginDefinition();
|
||||
if ($entity->getEntityTypeId() != $definition['entity_type']) {
|
||||
throw new BadRequestHttpException(t('Invalid entity type'));
|
||||
}
|
||||
$original_entity = entity_load($definition['entity_type'], $id);
|
||||
// We don't support creating entities with PATCH, so we throw an error if
|
||||
// there is no existing entity.
|
||||
if ($original_entity == FALSE) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
if (!$original_entity->access('update')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
|
@ -174,33 +160,28 @@ class EntityResource extends ResourceBase {
|
|||
/**
|
||||
* Responds to entity DELETE requests.
|
||||
*
|
||||
* @param mixed $id
|
||||
* The entity ID.
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity object.
|
||||
*
|
||||
* @return \Drupal\rest\ResourceResponse
|
||||
* The HTTP response object.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\HttpException
|
||||
*/
|
||||
public function delete($id) {
|
||||
$definition = $this->getPluginDefinition();
|
||||
$entity = entity_load($definition['entity_type'], $id);
|
||||
if ($entity) {
|
||||
if (!$entity->access('delete')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
try {
|
||||
$entity->delete();
|
||||
watchdog('rest', 'Deleted entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
|
||||
|
||||
// Delete responses have an empty body.
|
||||
return new ResourceResponse(NULL, 204);
|
||||
}
|
||||
catch (EntityStorageException $e) {
|
||||
throw new HttpException(500, t('Internal Server Error'), $e);
|
||||
}
|
||||
public function delete(EntityInterface $entity) {
|
||||
if (!$entity->access('delete')) {
|
||||
throw new AccessDeniedHttpException();
|
||||
}
|
||||
try {
|
||||
$entity->delete();
|
||||
watchdog('rest', 'Deleted entity %type with ID %id.', array('%type' => $entity->getEntityTypeId(), '%id' => $entity->id()));
|
||||
|
||||
// Delete responses have an empty body.
|
||||
return new ResourceResponse(NULL, 204);
|
||||
}
|
||||
catch (EntityStorageException $e) {
|
||||
throw new HttpException(500, t('Internal Server Error'), $e);
|
||||
}
|
||||
throw new NotFoundHttpException(t('Entity with ID @id not found', array('@id' => $id)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -25,13 +25,12 @@ class RequestHandler extends ContainerAware {
|
|||
*
|
||||
* @param Symfony\Component\HttpFoundation\Request $request
|
||||
* The HTTP request object.
|
||||
* @param mixed $id
|
||||
* The resource ID.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* The response object.
|
||||
*/
|
||||
public function handle(Request $request, $id = NULL) {
|
||||
public function handle(Request $request) {
|
||||
|
||||
$plugin = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getDefault('_plugin');
|
||||
$method = strtolower($request->getMethod());
|
||||
|
||||
|
@ -69,13 +68,24 @@ class RequestHandler extends ContainerAware {
|
|||
}
|
||||
}
|
||||
|
||||
// Determine the request parameters that should be passed to the resource
|
||||
// plugin.
|
||||
$route_parameters = $request->attributes->get('_route_params');
|
||||
$parameters = array();
|
||||
// Filter out all internal parameters starting with "_".
|
||||
foreach ($route_parameters as $key => $parameter) {
|
||||
if ($key{0} !== '_') {
|
||||
$parameters[] = $parameter;
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke the operation on the resource plugin.
|
||||
// All REST routes are restricted to exactly one format, so instead of
|
||||
// parsing it out of the Accept headers again, we can simply retrieve the
|
||||
// format requirement. If there is no format associated, just pick JSON.
|
||||
$format = $request->attributes->get(RouteObjectInterface::ROUTE_OBJECT)->getRequirement('_format') ?: 'json';
|
||||
try {
|
||||
$response = $resource->{$method}($id, $unserialized, $request);
|
||||
$response = call_user_func_array(array($resource, $method), array_merge($parameters, array($unserialized, $request)));
|
||||
}
|
||||
catch (HttpException $e) {
|
||||
$error['error'] = $e->getMessage();
|
||||
|
|
|
@ -46,7 +46,7 @@ class AuthTest extends RESTTestBase {
|
|||
$entity->save();
|
||||
|
||||
// Try to read the resource as an anonymous user, which should not work.
|
||||
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse('401', 'HTTP response code is 401 when the request is not authenticated and the user is anonymous.');
|
||||
$this->assertText('A fatal error occurred: No authentication credentials provided.');
|
||||
|
||||
|
@ -63,7 +63,7 @@ class AuthTest extends RESTTestBase {
|
|||
|
||||
// Try to read the resource with session cookie authentication, which is
|
||||
// not enabled and should not work.
|
||||
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse('401', 'HTTP response code is 401 when the request is authenticated but not authorized.');
|
||||
|
||||
// Ensure that cURL settings/headers aren't carried over to next request.
|
||||
|
@ -71,7 +71,7 @@ class AuthTest extends RESTTestBase {
|
|||
|
||||
// Now read it with the Basic authentication which is enabled and should
|
||||
// work.
|
||||
$response = $this->basicAuthGet('entity/' . $entity_type . '/' . $entity->id(), $account->getUsername(), $account->pass_raw);
|
||||
$this->basicAuthGet($entity->getSystemPath(), $account->getUsername(), $account->pass_raw);
|
||||
$this->assertResponse('200', 'HTTP response code is 200 for successfully authorized requests.');
|
||||
$this->curlClose();
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
namespace Drupal\rest\Tests;
|
||||
|
||||
use Drupal\Component\Utility\Json;
|
||||
use Drupal\rest\Tests\RESTTestBase;
|
||||
|
||||
/**
|
||||
|
@ -51,7 +50,7 @@ class DeleteTest extends RESTTestBase {
|
|||
$entity = $this->entityCreate($entity_type);
|
||||
$entity->save();
|
||||
// Delete it over the REST API.
|
||||
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'DELETE');
|
||||
$response = $this->httpRequest($entity->getSystemPath(), 'DELETE');
|
||||
// Clear the static cache with entity_load(), otherwise we won't see the
|
||||
// update.
|
||||
$entity = entity_load($entity_type, $entity->id(), TRUE);
|
||||
|
@ -60,17 +59,16 @@ class DeleteTest extends RESTTestBase {
|
|||
$this->assertEqual($response, '', 'Response body is empty.');
|
||||
|
||||
// Try to delete an entity that does not exist.
|
||||
$response = $this->httpRequest('entity/' . $entity_type . '/9999', 'DELETE');
|
||||
$response = $this->httpRequest($entity_type . '/9999', 'DELETE');
|
||||
$this->assertResponse(404);
|
||||
$decoded = Json::decode($response);
|
||||
$this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.');
|
||||
$this->assertText('The requested page "/' . $entity_type . '/9999" could not be found.');
|
||||
|
||||
// Try to delete an entity without proper permissions.
|
||||
$this->drupalLogout();
|
||||
// Re-save entity to the database.
|
||||
$entity = $this->entityCreate($entity_type);
|
||||
$entity->save();
|
||||
$this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'DELETE');
|
||||
$this->httpRequest($entity->getSystemPath(), 'DELETE');
|
||||
$this->assertResponse(403);
|
||||
$this->assertNotIdentical(FALSE, entity_load($entity_type, $entity->id(), TRUE), 'The ' . $entity_type . ' entity is still in the database.');
|
||||
}
|
||||
|
|
|
@ -55,8 +55,16 @@ class NodeTest extends RESTTestBase {
|
|||
|
||||
$node = $this->entityCreate('node');
|
||||
$node->save();
|
||||
$this->httpRequest('entity/node/' . $node->id(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->httpRequest('node/' . $node->id(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(200);
|
||||
$this->assertHeader('Content-type', $this->defaultMimeType);
|
||||
|
||||
// Also check that JSON works and the routing system selects the correct
|
||||
// REST route.
|
||||
$this->enableService('entity:node', 'GET', 'json');
|
||||
$this->httpRequest('node/' . $node->id(), 'GET', NULL, 'application/json');
|
||||
$this->assertResponse(200);
|
||||
$this->assertHeader('Content-type', 'application/json');
|
||||
|
||||
// Check that a simple PATCH update to the node title works as expected.
|
||||
$this->enableNodeConfiguration('PATCH', 'update');
|
||||
|
@ -76,7 +84,7 @@ class NodeTest extends RESTTestBase {
|
|||
),
|
||||
);
|
||||
$serialized = $this->container->get('serializer')->serialize($data, $this->defaultFormat);
|
||||
$this->httpRequest('entity/node/' . $node->id(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->httpRequest('node/' . $node->id(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(204);
|
||||
|
||||
// Reload the node from the DB and check if the title was correctly updated.
|
||||
|
|
|
@ -276,16 +276,22 @@ abstract class RESTTestBase extends WebTestBase {
|
|||
}
|
||||
|
||||
/**
|
||||
* Overrides WebTestBase::drupalLogin().
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* This method is overridden to deal with a cURL quirk: the usage of
|
||||
* CURLOPT_CUSTOMREQUEST cannot be unset on the cURL handle, so we need to
|
||||
* override it every time it is omitted.
|
||||
*/
|
||||
protected function drupalLogin(AccountInterface $user) {
|
||||
if (isset($this->curlHandle)) {
|
||||
// cURL quirk: when setting CURLOPT_CUSTOMREQUEST to anything other than
|
||||
// POST in httpRequest() it has to be restored to POST here. Otherwise the
|
||||
// POST request to login a user will not work.
|
||||
curl_setopt($this->curlHandle, CURLOPT_CUSTOMREQUEST, 'POST');
|
||||
protected function curlExec($curl_options, $redirect = FALSE) {
|
||||
if (!isset($curl_options[CURLOPT_CUSTOMREQUEST])) {
|
||||
if (!empty($curl_options[CURLOPT_HTTPGET])) {
|
||||
$curl_options[CURLOPT_CUSTOMREQUEST] = 'GET';
|
||||
}
|
||||
if (!empty($curl_options[CURLOPT_POST])) {
|
||||
$curl_options[CURLOPT_CUSTOMREQUEST] = 'POST';
|
||||
}
|
||||
}
|
||||
parent::drupalLogin($user);
|
||||
return parent::curlExec($curl_options, $redirect);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -338,4 +344,5 @@ abstract class RESTTestBase extends WebTestBase {
|
|||
$id = end($url_parts);
|
||||
return entity_load($this->testEntityType, $id);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ class ReadTest extends RESTTestBase {
|
|||
$entity = $this->entityCreate($entity_type);
|
||||
$entity->save();
|
||||
// Read it over the REST API.
|
||||
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
|
||||
$response = $this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse('200', 'HTTP response code is correct.');
|
||||
$this->assertHeader('content-type', $this->defaultMimeType);
|
||||
$data = Json::decode($response);
|
||||
|
@ -60,14 +60,14 @@ class ReadTest extends RESTTestBase {
|
|||
$this->assertEqual($data['uuid'][0]['value'], $entity->uuid(), 'Entity UUID is correct');
|
||||
|
||||
// Try to read the entity with an unsupported mime format.
|
||||
$this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, 'application/wrongformat');
|
||||
$this->assertResponse(406);
|
||||
$response = $this->httpRequest($entity->getSystemPath(), 'GET', NULL, 'application/wrongformat');
|
||||
$this->assertResponse(200);
|
||||
$this->assertHeader('Content-type', 'text/html; charset=UTF-8');
|
||||
|
||||
// Try to read an entity that does not exist.
|
||||
$response = $this->httpRequest('entity/' . $entity_type . '/9999', 'GET', NULL, $this->defaultMimeType);
|
||||
$response = $this->httpRequest($entity_type . '/9999', 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(404);
|
||||
$decoded = Json::decode($response);
|
||||
$this->assertEqual($decoded['error'], 'Entity with ID 9999 not found', 'Response message is correct.');
|
||||
$this->assertText('A fatal error occurred: The "' . $entity_type . '" parameter was not converted for the path', 'Response message is correct.');
|
||||
|
||||
// Make sure that field level access works and that the according field is
|
||||
// not available in the response. Only applies to entity_test.
|
||||
|
@ -75,7 +75,7 @@ class ReadTest extends RESTTestBase {
|
|||
if ($entity_type == 'entity_test') {
|
||||
$entity->field_test_text->value = 'no access value';
|
||||
$entity->save();
|
||||
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
|
||||
$response = $this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(200);
|
||||
$this->assertHeader('content-type', $this->defaultMimeType);
|
||||
$data = Json::decode($response);
|
||||
|
@ -84,14 +84,14 @@ class ReadTest extends RESTTestBase {
|
|||
|
||||
// Try to read an entity without proper permissions.
|
||||
$this->drupalLogout();
|
||||
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'GET', NULL, $this->defaultMimeType);
|
||||
$response = $this->httpRequest($entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(403);
|
||||
$this->assertNull(Json::decode($response), 'No valid JSON found.');
|
||||
}
|
||||
// Try to read a resource which is not REST API enabled.
|
||||
$account = $this->drupalCreateUser();
|
||||
$this->drupalLogin($account);
|
||||
$response = $this->httpRequest('entity/user/' . $account->id(), 'GET', NULL, $this->defaultMimeType);
|
||||
$response = $this->httpRequest($account->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(404);
|
||||
$this->assertNull(Json::decode($response), 'No valid JSON found.');
|
||||
}
|
||||
|
@ -114,7 +114,7 @@ class ReadTest extends RESTTestBase {
|
|||
$entity->save();
|
||||
|
||||
// Read it over the REST API.
|
||||
$response = $this->httpRequest('entity/node/' . $entity->id(), 'GET', NULL, 'application/json');
|
||||
$response = $this->httpRequest($entity->getSystemPath(), 'GET', NULL, 'application/json');
|
||||
$this->assertResponse('200', 'HTTP response code is correct.');
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ class ResourceTest extends RESTTestBase {
|
|||
$this->rebuildCache();
|
||||
|
||||
// Verify that accessing the resource returns 401.
|
||||
$response = $this->httpRequest('entity/entity_test/' . $this->entity->id(), 'GET', NULL, $this->defaultMimeType);
|
||||
$response = $this->httpRequest($this->entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse('404', 'HTTP response code is 404 when the resource does not define formats.');
|
||||
$this->curlClose();
|
||||
}
|
||||
|
@ -89,7 +89,7 @@ class ResourceTest extends RESTTestBase {
|
|||
$this->rebuildCache();
|
||||
|
||||
// Verify that accessing the resource returns 401.
|
||||
$response = $this->httpRequest('entity/entity_test/' . $this->entity->id(), 'GET', NULL, $this->defaultMimeType);
|
||||
$response = $this->httpRequest($this->entity->getSystemPath(), 'GET', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse('404', 'HTTP response code is 404 when the resource does not define authentication.');
|
||||
$this->curlClose();
|
||||
}
|
||||
|
|
|
@ -62,7 +62,7 @@ class UpdateTest extends RESTTestBase {
|
|||
$serialized = $serializer->serialize($patch_entity, $this->defaultFormat);
|
||||
|
||||
// Update the entity over the REST API.
|
||||
$this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(204);
|
||||
|
||||
// Re-load updated entity from the database.
|
||||
|
@ -74,7 +74,7 @@ class UpdateTest extends RESTTestBase {
|
|||
$normalized = $serializer->normalize($patch_entity, $this->defaultFormat);
|
||||
unset($normalized['field_test_text']);
|
||||
$serialized = $serializer->encode($normalized, $this->defaultFormat);
|
||||
$this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(204);
|
||||
|
||||
$entity = entity_load($entity_type, $entity->id(), TRUE);
|
||||
|
@ -85,7 +85,7 @@ class UpdateTest extends RESTTestBase {
|
|||
$serialized = $serializer->encode($normalized, $this->defaultFormat);
|
||||
|
||||
// Update the entity over the REST API.
|
||||
$this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(204);
|
||||
|
||||
// Re-load updated entity from the database.
|
||||
|
@ -99,7 +99,7 @@ class UpdateTest extends RESTTestBase {
|
|||
$entity->save();
|
||||
|
||||
// Try to empty a field that is access protected.
|
||||
$this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Re-load the entity from the database.
|
||||
|
@ -109,7 +109,7 @@ class UpdateTest extends RESTTestBase {
|
|||
// Try to update an access protected field.
|
||||
$patch_entity->get('field_test_text')->value = 'no access value';
|
||||
$serialized = $serializer->serialize($patch_entity, $this->defaultFormat);
|
||||
$this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Re-load the entity from the database.
|
||||
|
@ -122,7 +122,7 @@ class UpdateTest extends RESTTestBase {
|
|||
'format' => 'full_html',
|
||||
));
|
||||
$serialized = $serializer->serialize($patch_entity, $this->defaultFormat);
|
||||
$this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(422);
|
||||
|
||||
// Re-load the entity from the database.
|
||||
|
@ -134,11 +134,11 @@ class UpdateTest extends RESTTestBase {
|
|||
$entity->save();
|
||||
|
||||
// Try to send no data at all, which does not make sense on PATCH requests.
|
||||
$this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', NULL, $this->defaultMimeType);
|
||||
$this->httpRequest($entity->getSystemPath(), 'PATCH', NULL, $this->defaultMimeType);
|
||||
$this->assertResponse(400);
|
||||
|
||||
// Try to update a non-existing entity with ID 9999.
|
||||
$this->httpRequest('entity/' . $entity_type . '/9999', 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->httpRequest($entity_type . '/9999', 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(404);
|
||||
$loaded_entity = entity_load($entity_type, 9999, TRUE);
|
||||
$this->assertFalse($loaded_entity, 'Entity 9999 was not created.');
|
||||
|
@ -147,21 +147,21 @@ class UpdateTest extends RESTTestBase {
|
|||
// Send a UUID that is too long.
|
||||
$entity->set('uuid', $this->randomName(129));
|
||||
$invalid_serialized = $serializer->serialize($entity, $this->defaultFormat);
|
||||
$response = $this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $invalid_serialized, $this->defaultMimeType);
|
||||
$response = $this->httpRequest($entity->getSystemPath(), 'PATCH', $invalid_serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(422);
|
||||
$error = Json::decode($response);
|
||||
$this->assertEqual($error['error'], "Unprocessable Entity: validation failed.\nuuid.0.value: <em class=\"placeholder\">UUID</em>: may not be longer than 128 characters.\n");
|
||||
|
||||
// Try to update an entity without proper permissions.
|
||||
$this->drupalLogout();
|
||||
$this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(403);
|
||||
|
||||
// Try to update a resource which is not REST API enabled.
|
||||
$this->enableService(FALSE);
|
||||
$this->drupalLogin($account);
|
||||
$this->httpRequest('entity/' . $entity_type . '/' . $entity->id(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(404);
|
||||
$this->httpRequest($entity->getSystemPath(), 'PATCH', $serialized, $this->defaultMimeType);
|
||||
$this->assertResponse(405);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -180,6 +180,9 @@ class PathBasedBreadcrumbBuilder extends BreadcrumbBuilderBase {
|
|||
// @todo Use the RequestHelper once https://drupal.org/node/2090293 is
|
||||
// fixed.
|
||||
$request = Request::create($this->request->getBaseUrl() . '/' . $path);
|
||||
// Performance optimization: set a short accept header to reduce overhead in
|
||||
// AcceptHeaderMatcher when matching the request.
|
||||
$request->headers->set('Accept', 'text/html');
|
||||
// Find the system path by resolving aliases, language prefix, etc.
|
||||
$processed = $this->pathProcessor->processInbound($path, $request);
|
||||
if (empty($processed) || !empty($exclude[$processed])) {
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains Drupal\Tests\Core\Routing\AcceptHeaderMatcherTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\Tests\Core\Routing;
|
||||
|
||||
use Drupal\Core\ContentNegotiation;
|
||||
use Drupal\Core\Routing\AcceptHeaderMatcher;
|
||||
use Drupal\Tests\Core\Routing\RoutingFixtures;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Basic tests for the AcceptHeaderMatcher class.
|
||||
*
|
||||
* @coversClassDefault \Drupal\Core\Routing\AcceptHeaderMatcher
|
||||
*/
|
||||
class AcceptHeaderMatcherTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* A collection of shared fixture data for tests.
|
||||
*
|
||||
* @var RoutingFixtures
|
||||
*/
|
||||
protected $fixtures;
|
||||
|
||||
/**
|
||||
* The matcher object that is going to be tested.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\AcceptHeaderMatcher
|
||||
*/
|
||||
protected $matcher;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Partial matcher MIME types tests',
|
||||
'description' => 'Confirm that the mime types partial matcher is functioning properly.',
|
||||
'group' => 'Routing',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->fixtures = new RoutingFixtures();
|
||||
$this->matcher = new AcceptHeaderMatcher(new ContentNegotiation());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides data for the Accept header filtering test.
|
||||
*
|
||||
* @see Drupal\Tests\Core\Routing\AcceptHeaderMatcherTest::testAcceptFiltering()
|
||||
*/
|
||||
public function acceptFilterProvider() {
|
||||
return array(
|
||||
// Check that JSON routes get filtered and prioritized correctly.
|
||||
array('application/json, text/xml;q=0.9', 'route_c', 'route_e'),
|
||||
// Tests a JSON request with alternative JSON MIME type Accept header.
|
||||
array('application/x-json, text/xml;q=0.9', 'route_c', 'route_e'),
|
||||
// Tests a standard HTML request.
|
||||
array('text/html, text/xml;q=0.9', 'route_e', 'route_c'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that requests using Accept headers get filtered correctly.
|
||||
*
|
||||
* @param string $accept_header
|
||||
* The HTTP Accept header value of the request.
|
||||
* @param string $included_route
|
||||
* The route name that should survive the filter and be ranked first.
|
||||
* @param string $excluded_route
|
||||
* The route name that should be filtered out during matching.
|
||||
*
|
||||
* @dataProvider acceptFilterProvider
|
||||
*/
|
||||
public function testAcceptFiltering($accept_header, $included_route, $excluded_route) {
|
||||
$collection = $this->fixtures->sampleRouteCollection();
|
||||
|
||||
$request = Request::create('path/two', 'GET');
|
||||
$request->headers->set('Accept', $accept_header);
|
||||
$routes = $this->matcher->filter($collection, $request);
|
||||
$this->assertEquals(count($routes), 4, 'The correct number of routes was found.');
|
||||
$this->assertNotNull($routes->get($included_route), "Route $included_route was found when matching $accept_header.");
|
||||
$this->assertNull($routes->get($excluded_route), "Route $excluded_route was not found when matching $accept_header.");
|
||||
foreach ($routes as $name => $route) {
|
||||
$this->assertEquals($name, $included_route, "Route $included_route is the first one in the collection when matching $accept_header.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that the AcceptHeaderMatcher throws an exception for no-route.
|
||||
*
|
||||
* @expectedException \Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException
|
||||
* @expectedExceptionMessage No route found for the specified formats application/json text/xml.
|
||||
*/
|
||||
public function testNoRouteFound() {
|
||||
// Remove the sample routes that would match any method.
|
||||
$routes = $this->fixtures->sampleRouteCollection();
|
||||
$routes->remove('route_a');
|
||||
$routes->remove('route_b');
|
||||
$routes->remove('route_c');
|
||||
$routes->remove('route_d');
|
||||
|
||||
$request = Request::create('path/two', 'GET');
|
||||
$request->headers->set('Accept', 'application/json, text/xml;q=0.9');
|
||||
$this->matcher->filter($routes, $request);
|
||||
$this->fail('No exception was thrown.');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains Drupal\Tests\Core\Routing\ContentTypeHeaderMatcherTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\Tests\Core\Routing;
|
||||
|
||||
use Drupal\Core\Routing\ContentTypeHeaderMatcher;
|
||||
use Drupal\Tests\Core\Routing\RoutingFixtures;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
/**
|
||||
* Basic tests for the ContentTypeHeaderMatcher class.
|
||||
*
|
||||
* @coversClassDefault \Drupal\Core\Routing\ContentTypeHeaderMatcher
|
||||
*/
|
||||
class ContentTypeHeaderMatcherTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* A collection of shared fixture data for tests.
|
||||
*
|
||||
* @var RoutingFixtures
|
||||
*/
|
||||
protected $fixtures;
|
||||
|
||||
/**
|
||||
* The matcher object that is going to be tested.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\ContentTypeHeaderMatcher
|
||||
*/
|
||||
protected $matcher;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Content Type header matcher test',
|
||||
'description' => 'Confirm that the content types partial matcher is functioning properly.',
|
||||
'group' => 'Routing',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->fixtures = new RoutingFixtures();
|
||||
$this->matcher = new ContentTypeHeaderMatcher();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that routes are not filtered on GET requests.
|
||||
*/
|
||||
public function testGetRequestFilter() {
|
||||
$collection = $this->fixtures->sampleRouteCollection();
|
||||
$collection->addCollection($this->fixtures->contentRouteCollection());
|
||||
|
||||
$request = Request::create('path/two', 'GET');
|
||||
$routes = $this->matcher->filter($collection, $request);
|
||||
$this->assertEquals(count($routes), 7, 'The correct number of routes was found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that XML-restricted routes get filtered out on JSON requests.
|
||||
*/
|
||||
public function testJsonRequest() {
|
||||
$collection = $this->fixtures->sampleRouteCollection();
|
||||
$collection->addCollection($this->fixtures->contentRouteCollection());
|
||||
|
||||
$request = Request::create('path/two', 'POST');
|
||||
$request->headers->set('Content-type', 'application/json');
|
||||
$routes = $this->matcher->filter($collection, $request);
|
||||
$this->assertEquals(count($routes), 6, 'The correct number of routes was found.');
|
||||
$this->assertNotNull($routes->get('route_f'), 'The json route was found.');
|
||||
$this->assertNull($routes->get('route_g'), 'The xml route was not found.');
|
||||
foreach ($routes as $name => $route) {
|
||||
$this->assertEquals($name, 'route_f', 'The json route is the first one in the collection.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests route filtering on POST form submission requests.
|
||||
*/
|
||||
public function testPostForm() {
|
||||
$collection = $this->fixtures->sampleRouteCollection();
|
||||
$collection->addCollection($this->fixtures->contentRouteCollection());
|
||||
|
||||
// Test that all XML and JSON restricted routes get filtered out on a POST
|
||||
// form submission.
|
||||
$request = Request::create('path/two', 'POST');
|
||||
$request->headers->set('Content-type', 'application/www-form-urlencoded');
|
||||
$routes = $this->matcher->filter($collection, $request);
|
||||
$this->assertEquals(count($routes), 5, 'The correct number of routes was found.');
|
||||
$this->assertNull($routes->get('route_f'), 'The json route was found.');
|
||||
$this->assertNull($routes->get('route_g'), 'The xml route was not found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that the matcher throws an exception for no-route.
|
||||
*
|
||||
* @expectedException \Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException
|
||||
* @expectedExceptionMessage No route found that matches the Content-Type header.
|
||||
*/
|
||||
public function testNoRouteFound() {
|
||||
$matcher = new ContentTypeHeaderMatcher();
|
||||
|
||||
$routes = $this->fixtures->contentRouteCollection();
|
||||
$request = Request::create('path/two', 'POST');
|
||||
$request->headers->set('Content-type', 'application/hal+json');
|
||||
$matcher->filter($routes, $request);
|
||||
$this->fail('No exception was thrown.');
|
||||
}
|
||||
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains Drupal\Tests\Core\Routing.
|
||||
*/
|
||||
|
||||
namespace Drupal\Tests\Core\Routing;
|
||||
|
||||
use Drupal\Core\Routing\MimeTypeMatcher;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
|
||||
|
||||
/**
|
||||
* Basic tests for the MimeTypeMatcher class.
|
||||
*
|
||||
* @group Drupal
|
||||
* @group Routing
|
||||
*/
|
||||
class MimeTypeMatcherTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* A collection of shared fixture data for tests.
|
||||
*
|
||||
* @var RoutingFixtures
|
||||
*/
|
||||
protected $fixtures;
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Partial matcher MIME types tests',
|
||||
'description' => 'Confirm that the mime types partial matcher is functioning properly.',
|
||||
'group' => 'Routing',
|
||||
);
|
||||
}
|
||||
|
||||
public function setUp() {
|
||||
$this->fixtures = new RoutingFixtures();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that the MimeType matcher matches properly.
|
||||
*
|
||||
* @param string $accept_header
|
||||
* The 'Accept` header to test.
|
||||
* @param integer $routes_count
|
||||
* The number of expected routes.
|
||||
* @param string $null_route
|
||||
* The route that is expected to be null.
|
||||
* @param string $not_null_route
|
||||
* The route that is expected to not be null.
|
||||
*
|
||||
* @dataProvider providerTestFilterRoutes
|
||||
*/
|
||||
public function testFilterRoutes($accept_header, $routes_count, $null_route, $not_null_route) {
|
||||
|
||||
$matcher = new MimeTypeMatcher();
|
||||
$collection = $this->fixtures->sampleRouteCollection();
|
||||
|
||||
// Tests basic JSON request.
|
||||
$request = Request::create('path/two', 'GET');
|
||||
$request->headers->set('Accept', $accept_header);
|
||||
$routes = $matcher->filter($collection, $request);
|
||||
$this->assertEquals($routes_count, count($routes), 'An incorrect number of routes was found.');
|
||||
$this->assertNull($routes->get($null_route), 'A route was found where it should be null.');
|
||||
$this->assertNotNull($routes->get($not_null_route), 'The expected route was not found.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test routes for testFilterRoutes.
|
||||
*
|
||||
* @return array
|
||||
* An array of arrays, each containing the parameters necessary for the
|
||||
* testFilterRoutes method.
|
||||
*/
|
||||
public function providerTestFilterRoutes() {
|
||||
return array(
|
||||
// Tests basic JSON request.
|
||||
array('application/json, text/xml;q=0.9', 4, 'route_e', 'route_c'),
|
||||
|
||||
// Tests JSON request with alternative JSON MIME type Accept header.
|
||||
array('application/x-json, text/xml;q=0.9', 4, 'route_e', 'route_c'),
|
||||
|
||||
// Tests basic HTML request.
|
||||
array('text/html, text/xml;q=0.9', 4, 'route_c', 'route_e'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms that the MimeTypeMatcher matcher throws an exception for no-route.
|
||||
*
|
||||
* @expectedException Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException
|
||||
*/
|
||||
public function testNoRouteFound() {
|
||||
$matcher = new MimeTypeMatcher();
|
||||
|
||||
// Remove the sample routes that would match any method.
|
||||
$routes = $this->fixtures->sampleRouteCollection();
|
||||
$routes->remove('route_a');
|
||||
$routes->remove('route_b');
|
||||
$routes->remove('route_c');
|
||||
$routes->remove('route_d');
|
||||
|
||||
// This should throw NotAcceptableHttpException.
|
||||
$request = Request::create('path/two', 'GET');
|
||||
$request->headers->set('Accept', 'application/json, text/xml;q=0.9');
|
||||
$routes = $matcher->filter($routes, $request);
|
||||
}
|
||||
|
||||
}
|
|
@ -143,6 +143,26 @@ class RoutingFixtures {
|
|||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Content-type restricted set of routes for testing.
|
||||
*
|
||||
* @return \Symfony\Component\Routing\RouteCollection
|
||||
*/
|
||||
public function contentRouteCollection() {
|
||||
$collection = new RouteCollection();
|
||||
|
||||
$route = new Route('path/three');
|
||||
$route->setRequirement('_method', 'POST');
|
||||
$route->setRequirement('_content_type_format', 'json');
|
||||
$collection->add('route_f', $route);
|
||||
|
||||
$route = new Route('path/three');
|
||||
$route->setRequirement('_method', 'PATCH');
|
||||
$route->setRequirement('_content_type_format', 'xml');
|
||||
$collection->add('route_g', $route);
|
||||
return $collection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the table definition for the routing fixtures.
|
||||
*
|
||||
|
|
Loading…
Reference in New Issue