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