Mercurial > hg > isophonics-drupal-site
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 } |