Upgraded drupal core with security updates
[yaffs-website] / web / core / modules / rest / src / EventSubscriber / ResourceResponseSubscriber.php
1 <?php
2
3 namespace Drupal\rest\EventSubscriber;
4
5 use Drupal\Core\Cache\CacheableResponse;
6 use Drupal\Core\Cache\CacheableResponseInterface;
7 use Drupal\Core\Render\RenderContext;
8 use Drupal\Core\Render\RendererInterface;
9 use Drupal\Core\Routing\RouteMatchInterface;
10 use Drupal\rest\ResourceResponseInterface;
11 use Symfony\Component\HttpFoundation\Request;
12 use Symfony\Component\HttpFoundation\Response;
13 use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
14 use Symfony\Component\HttpKernel\KernelEvents;
15 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16 use Symfony\Component\Serializer\SerializerInterface;
17
18 /**
19  * Response subscriber that serializes and removes ResourceResponses' data.
20  */
21 class ResourceResponseSubscriber implements EventSubscriberInterface {
22
23   /**
24    * The serializer.
25    *
26    * @var \Symfony\Component\Serializer\SerializerInterface
27    */
28   protected $serializer;
29
30   /**
31    * The renderer.
32    *
33    * @var \Drupal\Core\Render\RendererInterface
34    */
35   protected $renderer;
36
37   /**
38    * The current route match.
39    *
40    * @var \Drupal\Core\Routing\RouteMatchInterface
41    */
42   protected $routeMatch;
43
44   /**
45    * Constructs a ResourceResponseSubscriber object.
46    *
47    * @param \Symfony\Component\Serializer\SerializerInterface $serializer
48    *   The serializer.
49    * @param \Drupal\Core\Render\RendererInterface $renderer
50    *   The renderer.
51    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
52    *   The current route match.
53    */
54   public function __construct(SerializerInterface $serializer, RendererInterface $renderer, RouteMatchInterface $route_match) {
55     $this->serializer = $serializer;
56     $this->renderer = $renderer;
57     $this->routeMatch = $route_match;
58   }
59
60   /**
61    * Serializes ResourceResponse responses' data, and removes that data.
62    *
63    * @param \Symfony\Component\HttpKernel\Event\FilterResponseEvent $event
64    *   The event to process.
65    */
66   public function onResponse(FilterResponseEvent $event) {
67     $response = $event->getResponse();
68     if (!$response instanceof ResourceResponseInterface) {
69       return;
70     }
71
72     $request = $event->getRequest();
73     $format = $this->getResponseFormat($this->routeMatch, $request);
74     $this->renderResponseBody($request, $response, $this->serializer, $format);
75     $event->setResponse($this->flattenResponse($response));
76   }
77
78   /**
79    * Determines the format to respond in.
80    *
81    * Respects the requested format if one is specified. However, it is common to
82    * forget to specify a request format in case of a POST or PATCH. Rather than
83    * simply throwing an error, we apply the robustness principle: when POSTing
84    * or PATCHing using a certain format, you probably expect a response in that
85    * same format.
86    *
87    * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
88    *   The current route match.
89    * @param \Symfony\Component\HttpFoundation\Request $request
90    *   The current request.
91    *
92    * @return string
93    *   The response format.
94    */
95   public function getResponseFormat(RouteMatchInterface $route_match, Request $request) {
96     $route = $route_match->getRouteObject();
97     $acceptable_request_formats = $route->hasRequirement('_format') ? explode('|', $route->getRequirement('_format')) : [];
98     $acceptable_content_type_formats = $route->hasRequirement('_content_type_format') ? explode('|', $route->getRequirement('_content_type_format')) : [];
99     $acceptable_formats = $request->isMethodSafe() ? $acceptable_request_formats : $acceptable_content_type_formats;
100
101     $requested_format = $request->getRequestFormat();
102     $content_type_format = $request->getContentType();
103
104     // If an acceptable format is requested, then use that. Otherwise, including
105     // and particularly when the client forgot to specify a format, then use
106     // heuristics to select the format that is most likely expected.
107     if (in_array($requested_format, $acceptable_formats)) {
108       return $requested_format;
109     }
110     // If a request body is present, then use the format corresponding to the
111     // request body's Content-Type for the response, if it's an acceptable
112     // format for the request.
113     elseif (!empty($request->getContent()) && in_array($content_type_format, $acceptable_content_type_formats)) {
114       return $content_type_format;
115     }
116     // Otherwise, use the first acceptable format.
117     elseif (!empty($acceptable_formats)) {
118       return $acceptable_formats[0];
119     }
120     // Sometimes, there are no acceptable formats, e.g. DELETE routes.
121     else {
122       return NULL;
123     }
124   }
125
126   /**
127    * Renders a resource response body.
128    *
129    * Serialization can invoke rendering (e.g., generating URLs), but the
130    * serialization API does not provide a mechanism to collect the
131    * bubbleable metadata associated with that (e.g., language and other
132    * contexts), so instead, allow those to "leak" and collect them here in
133    * a render context.
134    *
135    * @param \Symfony\Component\HttpFoundation\Request $request
136    *   The request object.
137    * @param \Drupal\rest\ResourceResponseInterface $response
138    *   The response from the REST resource.
139    * @param \Symfony\Component\Serializer\SerializerInterface $serializer
140    *   The serializer to use.
141    * @param string|null $format
142    *   The response format, or NULL in case the response does not need a format,
143    *   for example for the response to a DELETE request.
144    *
145    * @todo Add test coverage for language negotiation contexts in
146    *   https://www.drupal.org/node/2135829.
147    */
148   protected function renderResponseBody(Request $request, ResourceResponseInterface $response, SerializerInterface $serializer, $format) {
149     $data = $response->getResponseData();
150
151     // If there is data to send, serialize and set it as the response body.
152     if ($data !== NULL) {
153       $context = new RenderContext();
154       $output = $this->renderer
155         ->executeInRenderContext($context, function () use ($serializer, $data, $format) {
156           return $serializer->serialize($data, $format);
157         });
158
159       if ($response instanceof CacheableResponseInterface && !$context->isEmpty()) {
160         $response->addCacheableDependency($context->pop());
161       }
162
163       $response->setContent($output);
164       $response->headers->set('Content-Type', $request->getMimeType($format));
165     }
166   }
167
168   /**
169    * Flattens a fully rendered resource response.
170    *
171    * Ensures that complex data structures in ResourceResponse::getResponseData()
172    * are not serialized. Not doing this means that caching this response object
173    * requires unserializing the PHP data when reading this response object from
174    * cache, which can be very costly, and is unnecessary.
175    *
176    * @param \Drupal\rest\ResourceResponseInterface $response
177    *   A fully rendered resource response.
178    *
179    * @return \Drupal\Core\Cache\CacheableResponse|\Symfony\Component\HttpFoundation\Response
180    *   The flattened response.
181    */
182   protected function flattenResponse(ResourceResponseInterface $response) {
183     $final_response = ($response instanceof CacheableResponseInterface) ? new CacheableResponse() : new Response();
184     $final_response->setContent($response->getContent());
185     $final_response->setStatusCode($response->getStatusCode());
186     $final_response->setProtocolVersion($response->getProtocolVersion());
187     $final_response->setCharset($response->getCharset());
188     $final_response->headers = clone $response->headers;
189     if ($final_response instanceof CacheableResponseInterface) {
190       $final_response->addCacheableDependency($response->getCacheableMetadata());
191     }
192     return $final_response;
193   }
194
195   /**
196    * {@inheritdoc}
197    */
198   public static function getSubscribedEvents() {
199     // Run shortly before \Drupal\Core\EventSubscriber\FinishResponseSubscriber.
200     $events[KernelEvents::RESPONSE][] = ['onResponse', 5];
201     return $events;
202   }
203
204 }