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