75ea5fd86a5326062a70c7d972e7ba5c9b0a1434
[yaffs-website] / web / core / lib / Drupal / Core / EventSubscriber / EarlyRenderingControllerWrapperSubscriber.php
1 <?php
2
3 namespace Drupal\Core\EventSubscriber;
4
5 use Drupal\Core\Ajax\AjaxResponse;
6 use Drupal\Core\Cache\CacheableDependencyInterface;
7 use Drupal\Core\Cache\CacheableResponseInterface;
8 use Drupal\Core\Controller\ControllerResolverInterface;
9 use Drupal\Core\Render\AttachmentsInterface;
10 use Drupal\Core\Render\BubbleableMetadata;
11 use Drupal\Core\Render\RenderContext;
12 use Drupal\Core\Render\RendererInterface;
13 use Symfony\Component\EventDispatcher\EventSubscriberInterface;
14 use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
15 use Symfony\Component\HttpKernel\KernelEvents;
16
17 /**
18  * Subscriber that wraps controllers, to handle early rendering.
19  *
20  * When controllers call drupal_render() (RendererInterface::render()) outside
21  * of a render context, we call that "early rendering". Controllers should
22  * return only render arrays, but we cannot prevent controllers from doing early
23  * rendering. The problem with early rendering is that the bubbleable metadata
24  * (cacheability & attachments) are lost.
25  *
26  * This can lead to broken pages (missing assets), stale pages (missing cache
27  * tags causing a page not to be invalidated) or even security problems (missing
28  * cache contexts causing a cached page not to be varied sufficiently).
29  *
30  * This event subscriber wraps all controller executions in a closure that sets
31  * up a render context. Consequently, any early rendering will have their
32  * bubbleable metadata (assets & cacheability) stored on that render context.
33  *
34  * If the render context is empty, then the controller either did not do any
35  * rendering at all, or used the RendererInterface::renderRoot() or
36  * ::renderPlain() methods. In that case, no bubbleable metadata is lost.
37  *
38  * If the render context is not empty, then the controller did use
39  * drupal_render(), and bubbleable metadata was collected. This bubbleable
40  * metadata is then merged onto the render array.
41  *
42  * In other words: this just exists to ease the transition to Drupal 8: it
43  * allows controllers that return render arrays (the majority) and
44  * \Drupal\Core\Ajax\AjaxResponse\AjaxResponse objects (a sizable minority that
45  * often involve a fair amount of rendering) to still do early rendering. But
46  * controllers that return any other kind of response are already expected to
47  * do the right thing, so if early rendering is detected in such a case, an
48  * exception is thrown.
49  *
50  * @see \Drupal\Core\Render\RendererInterface
51  * @see \Drupal\Core\Render\Renderer
52  *
53  * @todo Remove in Drupal 9.0.0, by disallowing early rendering.
54  */
55 class EarlyRenderingControllerWrapperSubscriber implements EventSubscriberInterface {
56
57   /**
58    * The controller resolver.
59    *
60    * @var \Drupal\Core\Controller\ControllerResolverInterface
61    */
62   protected $controllerResolver;
63
64   /**
65    * The renderer.
66    *
67    * @var \Drupal\Core\Render\RendererInterface
68    */
69   protected $renderer;
70
71   /**
72    * Constructs a new EarlyRenderingControllerWrapperSubscriber instance.
73    *
74    * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
75    *   The controller resolver.
76    * @param \Drupal\Core\Render\RendererInterface $renderer
77    *   The renderer.
78    */
79   public function __construct(ControllerResolverInterface $controller_resolver, RendererInterface $renderer) {
80     $this->controllerResolver = $controller_resolver;
81     $this->renderer = $renderer;
82   }
83
84   /**
85    * Ensures bubbleable metadata from early rendering is not lost.
86    *
87    * @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
88    *   The controller event.
89    */
90   public function onController(FilterControllerEvent $event) {
91     $controller = $event->getController();
92
93     // See \Symfony\Component\HttpKernel\HttpKernel::handleRaw().
94     $arguments = $this->controllerResolver->getArguments($event->getRequest(), $controller);
95
96     $event->setController(function () use ($controller, $arguments) {
97       return $this->wrapControllerExecutionInRenderContext($controller, $arguments);
98     });
99   }
100
101   /**
102    * Wraps a controller execution in a render context.
103    *
104    * @param callable $controller
105    *   The controller to execute.
106    * @param array $arguments
107    *   The arguments to pass to the controller.
108    *
109    * @return mixed
110    *   The return value of the controller.
111    *
112    * @throws \LogicException
113    *   When early rendering has occurred in a controller that returned a
114    *   Response or domain object that cares about attachments or cacheability.
115    *
116    * @see \Symfony\Component\HttpKernel\HttpKernel::handleRaw()
117    */
118   protected function wrapControllerExecutionInRenderContext($controller, array $arguments) {
119     $context = new RenderContext();
120
121     $response = $this->renderer->executeInRenderContext($context, function () use ($controller, $arguments) {
122       // Now call the actual controller, just like HttpKernel does.
123       return call_user_func_array($controller, $arguments);
124     });
125
126     // If early rendering happened, i.e. if code in the controller called
127     // drupal_render() outside of a render context, then the bubbleable metadata
128     // for that is stored in the current render context.
129     if (!$context->isEmpty()) {
130       /** @var \Drupal\Core\Render\BubbleableMetadata $early_rendering_bubbleable_metadata */
131       $early_rendering_bubbleable_metadata = $context->pop();
132
133       // If a render array or AjaxResponse is returned by the controller, merge
134       // the "lost" bubbleable metadata.
135       if (is_array($response)) {
136         BubbleableMetadata::createFromRenderArray($response)
137           ->merge($early_rendering_bubbleable_metadata)
138           ->applyTo($response);
139       }
140       elseif ($response instanceof AjaxResponse) {
141         $response->addAttachments($early_rendering_bubbleable_metadata->getAttachments());
142         // @todo Make AjaxResponse cacheable in
143         //   https://www.drupal.org/node/956186. Meanwhile, allow contrib
144         //   subclasses to be.
145         if ($response instanceof CacheableResponseInterface) {
146           $response->addCacheableDependency($early_rendering_bubbleable_metadata);
147         }
148       }
149       // If a non-Ajax Response or domain object is returned and it cares about
150       // attachments or cacheability, then throw an exception: early rendering
151       // is not permitted in that case. It is the developer's responsibility
152       // to not use early rendering.
153       elseif ($response instanceof AttachmentsInterface || $response instanceof CacheableResponseInterface || $response instanceof CacheableDependencyInterface) {
154         throw new \LogicException(sprintf('The controller result claims to be providing relevant cache metadata, but leaked metadata was detected. Please ensure you are not rendering content too early. Returned object class: %s.', get_class($response)));
155       }
156       else {
157         // A Response or domain object is returned that does not care about
158         // attachments nor cacheability; for instance, a RedirectResponse. It is
159         // safe to discard any early rendering metadata.
160       }
161     }
162
163     return $response;
164   }
165
166   /**
167    * {@inheritdoc}
168    */
169   public static function getSubscribedEvents() {
170     $events[KernelEvents::CONTROLLER][] = ['onController'];
171
172     return $events;
173   }
174
175 }