RequestHandler.php 14 KB

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