Added Entity and Entity Reference Revisions which got dropped somewhere along the...
[yaffs-website] / web / core / modules / rest / src / RequestHandler.php
1 <?php
2
3 namespace Drupal\rest;
4
5 use Drupal\Component\Utility\ArgumentsResolver;
6 use Drupal\Core\Cache\CacheableResponseInterface;
7 use Drupal\Core\Config\ConfigFactoryInterface;
8 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
9 use Drupal\Core\Entity\EntityInterface;
10 use Drupal\Core\Routing\RouteMatchInterface;
11 use Drupal\rest\Plugin\ResourceInterface;
12 use Symfony\Component\DependencyInjection\ContainerInterface;
13 use Symfony\Component\HttpFoundation\Request;
14 use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
15 use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
16 use Symfony\Component\Serializer\Exception\UnexpectedValueException;
17 use Symfony\Component\Serializer\Exception\InvalidArgumentException;
18 use Symfony\Component\Serializer\SerializerInterface;
19
20 /**
21  * Acts as intermediate request forwarder for resource plugins.
22  *
23  * @see \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
24  */
25 class RequestHandler implements ContainerInjectionInterface {
26
27   /**
28    * The config factory.
29    *
30    * @var \Drupal\Core\Config\ConfigFactoryInterface
31    */
32   protected $configFactory;
33
34   /**
35    * The serializer.
36    *
37    * @var \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface
38    */
39   protected $serializer;
40
41   /**
42    * Creates a new RequestHandler instance.
43    *
44    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
45    *   The config factory.
46    * @param \Symfony\Component\Serializer\SerializerInterface|\Symfony\Component\Serializer\Encoder\DecoderInterface $serializer
47    *   The serializer.
48    */
49   public function __construct(ConfigFactoryInterface $config_factory, SerializerInterface $serializer) {
50     $this->configFactory = $config_factory;
51     $this->serializer = $serializer;
52   }
53
54   /**
55    * {@inheritdoc}
56    */
57   public static function create(ContainerInterface $container) {
58     return new static(
59       $container->get('config.factory'),
60       $container->get('serializer')
61     );
62   }
63
64   /**
65    * Handles a REST API request.
66    *
67    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
68    *   The route match.
69    * @param \Symfony\Component\HttpFoundation\Request $request
70    *   The HTTP request object.
71    * @param \Drupal\rest\RestResourceConfigInterface $_rest_resource_config
72    *   The REST resource config entity.
73    *
74    * @return \Drupal\rest\ResourceResponseInterface|\Symfony\Component\HttpFoundation\Response
75    *   The REST resource response.
76    */
77   public function handle(RouteMatchInterface $route_match, Request $request, RestResourceConfigInterface $_rest_resource_config) {
78     $resource = $_rest_resource_config->getResourcePlugin();
79     $unserialized = $this->deserialize($route_match, $request, $resource);
80     $response = $this->delegateToRestResourcePlugin($route_match, $request, $unserialized, $resource);
81     return $this->prepareResponse($response, $_rest_resource_config);
82   }
83
84   /**
85    * Handles a REST API request without deserializing the request body.
86    *
87    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
88    *   The route match.
89    * @param \Symfony\Component\HttpFoundation\Request $request
90    *   The HTTP request object.
91    * @param \Drupal\rest\RestResourceConfigInterface $_rest_resource_config
92    *   The REST resource config entity.
93    *
94    * @return \Symfony\Component\HttpFoundation\Response|\Drupal\rest\ResourceResponseInterface
95    *   The REST resource response.
96    */
97   public function handleRaw(RouteMatchInterface $route_match, Request $request, RestResourceConfigInterface $_rest_resource_config) {
98     $resource = $_rest_resource_config->getResourcePlugin();
99     $response = $this->delegateToRestResourcePlugin($route_match, $request, NULL, $resource);
100     return $this->prepareResponse($response, $_rest_resource_config);
101   }
102
103   /**
104    * Prepares the REST resource response.
105    *
106    * @param \Drupal\rest\ResourceResponseInterface $response
107    *   The REST resource response.
108    * @param \Drupal\rest\RestResourceConfigInterface $resource_config
109    *   The REST resource config entity.
110    *
111    * @return \Drupal\rest\ResourceResponseInterface
112    *   The prepared REST resource response.
113    */
114   protected function prepareResponse($response, RestResourceConfigInterface $resource_config) {
115     if ($response instanceof CacheableResponseInterface) {
116       $response->addCacheableDependency($resource_config);
117       // Add global rest settings config's cache tag, for BC flags.
118       // @see \Drupal\rest\Plugin\rest\resource\EntityResource::permissions()
119       // @see \Drupal\rest\EventSubscriber\RestConfigSubscriber
120       // @todo Remove in https://www.drupal.org/node/2893804
121       $response->addCacheableDependency($this->configFactory->get('rest.settings'));
122     }
123
124     return $response;
125   }
126
127   /**
128    * Gets the normalized HTTP request method of the matched route.
129    *
130    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
131    *   The route match.
132    *
133    * @return string
134    *   The normalized HTTP request method.
135    */
136   protected static function getNormalizedRequestMethod(RouteMatchInterface $route_match) {
137     // Symfony is built to transparently map HEAD requests to a GET request. In
138     // the case of the REST module's RequestHandler though, we essentially have
139     // our own light-weight routing system on top of the Drupal/symfony routing
140     // system. So, we have to respect the decision that the routing system made:
141     // we look not at the request method, but at the route's method. All REST
142     // routes are guaranteed to have _method set.
143     // Response::prepare() will transform it to a HEAD response at the very last
144     // moment.
145     // @see https://www.w3.org/Protocols/rfc2616/rfc2616-sec9.html#sec9.4
146     // @see \Symfony\Component\Routing\Matcher\UrlMatcher::matchCollection()
147     // @see \Symfony\Component\HttpFoundation\Response::prepare()
148     $method = strtolower($route_match->getRouteObject()->getMethods()[0]);
149     assert(count($route_match->getRouteObject()->getMethods()) === 1);
150     return $method;
151   }
152
153   /**
154    * Deserializes request body, if any.
155    *
156    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
157    *   The route match.
158    * @param \Symfony\Component\HttpFoundation\Request $request
159    *   The HTTP request object.
160    * @param \Drupal\rest\Plugin\ResourceInterface $resource
161    *   The REST resource plugin.
162    *
163    * @return array|null
164    *   An object normalization, ikf there is a valid request body. NULL if there
165    *   is no request body.
166    *
167    * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
168    *   Thrown if the request body cannot be decoded.
169    * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
170    *   Thrown if the request body cannot be denormalized.
171    */
172   protected function deserialize(RouteMatchInterface $route_match, Request $request, ResourceInterface $resource) {
173     // Deserialize incoming data if available.
174     $received = $request->getContent();
175     $unserialized = NULL;
176     if (!empty($received)) {
177       $method = static::getNormalizedRequestMethod($route_match);
178       $format = $request->getContentType();
179
180       $definition = $resource->getPluginDefinition();
181
182       // First decode the request data. We can then determine if the
183       // serialized data was malformed.
184       try {
185         $unserialized = $this->serializer->decode($received, $format, ['request_method' => $method]);
186       }
187       catch (UnexpectedValueException $e) {
188         // If an exception was thrown at this stage, there was a problem
189         // decoding the data. Throw a 400 http exception.
190         throw new BadRequestHttpException($e->getMessage());
191       }
192
193       // Then attempt to denormalize if there is a serialization class.
194       if (!empty($definition['serialization_class'])) {
195         try {
196           $unserialized = $this->serializer->denormalize($unserialized, $definition['serialization_class'], $format, ['request_method' => $method]);
197         }
198         // These two serialization exception types mean there was a problem
199         // with the structure of the decoded data and it's not valid.
200         catch (UnexpectedValueException $e) {
201           throw new UnprocessableEntityHttpException($e->getMessage());
202         }
203         catch (InvalidArgumentException $e) {
204           throw new UnprocessableEntityHttpException($e->getMessage());
205         }
206       }
207     }
208
209     return $unserialized;
210   }
211
212   /**
213    * Delegates an incoming request to the appropriate REST resource plugin.
214    *
215    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
216    *   The route match.
217    * @param \Symfony\Component\HttpFoundation\Request $request
218    *   The HTTP request object.
219    * @param mixed|null $unserialized
220    *   The unserialized request body, if any.
221    * @param \Drupal\rest\Plugin\ResourceInterface $resource
222    *   The REST resource plugin.
223    *
224    * @return \Symfony\Component\HttpFoundation\Response|\Drupal\rest\ResourceResponseInterface
225    *   The REST resource response.
226    */
227   protected function delegateToRestResourcePlugin(RouteMatchInterface $route_match, Request $request, $unserialized, ResourceInterface $resource) {
228     $method = static::getNormalizedRequestMethod($route_match);
229
230     // Determine the request parameters that should be passed to the resource
231     // plugin.
232     $argument_resolver = $this->createArgumentResolver($route_match, $unserialized, $request);
233     try {
234       $arguments = $argument_resolver->getArguments([$resource, $method]);
235     }
236     catch (\RuntimeException $exception) {
237       @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);
238       $arguments = $this->getLegacyParameters($route_match, $unserialized, $request);
239     }
240
241     // Invoke the operation on the resource plugin.
242     return call_user_func_array([$resource, $method], $arguments);
243   }
244
245   /**
246    * Creates an argument resolver, containing all REST parameters.
247    *
248    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
249    *   The route match.
250    * @param mixed $unserialized
251    *   The unserialized data.
252    * @param \Symfony\Component\HttpFoundation\Request $request
253    *   The request.
254    *
255    * @return \Drupal\Component\Utility\ArgumentsResolver
256    *   An instance of the argument resolver containing information like the
257    *   'entity' we process and the 'unserialized' content from the request body.
258    */
259   protected function createArgumentResolver(RouteMatchInterface $route_match, $unserialized, Request $request) {
260     $route = $route_match->getRouteObject();
261
262     // Defaults for the parameters defined on the route object need to be added
263     // to the raw arguments.
264     $raw_route_arguments = $route_match->getRawParameters()->all() + $route->getDefaults();
265
266     $route_arguments = $route_match->getParameters()->all();
267     $upcasted_route_arguments = $route_arguments;
268
269     // For request methods that have request bodies, ResourceInterface plugin
270     // methods historically receive the unserialized request body as the N+1th
271     // method argument, where N is the number of route parameters specified on
272     // the accompanying route. To be able to use the argument resolver, which is
273     // not based on position but on name and typehint, specify commonly used
274     // names here. Similarly, those methods receive the original stored object
275     // as the first method argument.
276
277     $route_arguments_entity = NULL;
278     // Try to find a parameter which is an entity.
279     foreach ($route_arguments as $value) {
280       if ($value instanceof EntityInterface) {
281         $route_arguments_entity = $value;
282         break;
283       }
284     }
285
286     if (in_array($request->getMethod(), ['PATCH', 'POST'], TRUE)) {
287       $upcasted_route_arguments['entity'] = $unserialized;
288       $upcasted_route_arguments['data'] = $unserialized;
289       $upcasted_route_arguments['unserialized'] = $unserialized;
290       $upcasted_route_arguments['original_entity'] = $route_arguments_entity;
291     }
292     else {
293       $upcasted_route_arguments['entity'] = $route_arguments_entity;
294     }
295
296     // Parameters which are not defined on the route object, but still are
297     // essential for access checking are passed as wildcards to the argument
298     // resolver.
299     $wildcard_arguments = [$route, $route_match];
300     $wildcard_arguments[] = $request;
301     if (isset($unserialized)) {
302       $wildcard_arguments[] = $unserialized;
303     }
304
305     return new ArgumentsResolver($raw_route_arguments, $upcasted_route_arguments, $wildcard_arguments);
306   }
307
308   /**
309    * Provides the parameter usable without an argument resolver.
310    *
311    * This creates an list of parameters in a statically defined order.
312    *
313    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
314    *   The route match
315    * @param mixed $unserialized
316    *   The unserialized data.
317    * @param \Symfony\Component\HttpFoundation\Request $request
318    *   The request.
319    *
320    * @deprecated in Drupal 8.4.0, will be removed before Drupal 9.0.0. Use the
321    *   argument resolver method instead, see ::createArgumentResolver().
322    *
323    * @see https://www.drupal.org/node/2894819
324    *
325    * @return array
326    *   An array of parameters.
327    */
328   protected function getLegacyParameters(RouteMatchInterface $route_match, $unserialized, Request $request) {
329     $route_parameters = $route_match->getParameters();
330     $parameters = [];
331     // Filter out all internal parameters starting with "_".
332     foreach ($route_parameters as $key => $parameter) {
333       if ($key{0} !== '_') {
334         $parameters[] = $parameter;
335       }
336     }
337
338     return array_merge($parameters, [$unserialized, $request]);
339   }
340
341 }