292 lines
12 KiB
PHP
292 lines
12 KiB
PHP
<?php
|
|
|
|
namespace Drupal\rest;
|
|
|
|
use Drupal\Component\Utility\ArgumentsResolver;
|
|
use Drupal\Core\Cache\CacheableResponseInterface;
|
|
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
|
use Drupal\Core\Entity\EntityInterface;
|
|
use Drupal\Core\Routing\RouteMatchInterface;
|
|
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 ContainerInjectionInterface {
|
|
|
|
/**
|
|
* The serializer.
|
|
*
|
|
* @var \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface
|
|
*/
|
|
protected $serializer;
|
|
|
|
/**
|
|
* Creates a new RequestHandler instance.
|
|
*
|
|
* @param \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface $serializer
|
|
* The serializer.
|
|
*/
|
|
public function __construct(SerializerInterface $serializer) {
|
|
$this->serializer = $serializer;
|
|
}
|
|
|
|
/**
|
|
* {@inheritdoc}
|
|
*/
|
|
public static function create(ContainerInterface $container) {
|
|
return new static(
|
|
$container->get('serializer')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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 \Drupal\rest\ResourceResponseInterface|\Symfony\Component\HttpFoundation\Response
|
|
* The REST resource response.
|
|
*/
|
|
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);
|
|
}
|
|
|
|
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 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()
|
|
$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.
|
|
$received = $request->getContent();
|
|
$unserialized = NULL;
|
|
if (!empty($received)) {
|
|
$method = static::getNormalizedRequestMethod($route_match);
|
|
$format = $request->getContentTypeFormat();
|
|
|
|
$definition = $resource->getPluginDefinition();
|
|
|
|
// First decode the request data. We can then determine if the
|
|
// serialized data was malformed.
|
|
try {
|
|
$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);
|
|
$arguments = $argument_resolver->getArguments([$resource, $method]);
|
|
|
|
// 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)) {
|
|
if (is_object($unserialized)) {
|
|
$upcasted_route_arguments['entity'] = $unserialized;
|
|
$upcasted_route_arguments['data'] = $unserialized;
|
|
$upcasted_route_arguments['unserialized'] = $unserialized;
|
|
}
|
|
else {
|
|
$raw_route_arguments['data'] = $unserialized;
|
|
$raw_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);
|
|
}
|
|
|
|
}
|