comparison core/modules/rest/src/RequestHandler.php @ 14:1fec387a4317

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