Version 1
[yaffs-website] / web / core / lib / Drupal / Core / EventSubscriber / EarlyRenderingControllerWrapperSubscriber.php
diff --git a/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php b/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php
new file mode 100644 (file)
index 0000000..447b918
--- /dev/null
@@ -0,0 +1,175 @@
+<?php
+
+namespace Drupal\Core\EventSubscriber;
+
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Cache\CacheableDependencyInterface;
+use Drupal\Core\Cache\CacheableResponseInterface;
+use Drupal\Core\Controller\ControllerResolverInterface;
+use Drupal\Core\Render\AttachmentsInterface;
+use Drupal\Core\Render\BubbleableMetadata;
+use Drupal\Core\Render\RenderContext;
+use Drupal\Core\Render\RendererInterface;
+use Symfony\Component\EventDispatcher\EventSubscriberInterface;
+use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
+use Symfony\Component\HttpKernel\KernelEvents;
+
+/**
+ * Subscriber that wraps controllers, to handle early rendering.
+ *
+ * When controllers call drupal_render() (RendererInterface::render()) outside
+ * of a render context, we call that "early rendering". Controllers should
+ * return only render arrays, but we cannot prevent controllers from doing early
+ * rendering. The problem with early rendering is that the bubbleable metadata
+ * (cacheability & attachments) are lost.
+ *
+ * This can lead to broken pages (missing assets), stale pages (missing cache
+ * tags causing a page not to be invalidated) or even security problems (missing
+ * cache contexts causing a cached page not to be varied sufficiently).
+ *
+ * This event subscriber wraps all controller executions in a closure that sets
+ * up a render context. Consequently, any early rendering will have their
+ * bubbleable metadata (assets & cacheability) stored on that render context.
+ *
+ * If the render context is empty, then the controller either did not do any
+ * rendering at all, or used the RendererInterface::renderRoot() or
+ * ::renderPlain() methods. In that case, no bubbleable metadata is lost.
+ *
+ * If the render context is not empty, then the controller did use
+ * drupal_render(), and bubbleable metadata was collected. This bubbleable
+ * metadata is then merged onto the render array.
+ *
+ * In other words: this just exists to ease the transition to Drupal 8: it
+ * allows controllers that return render arrays (the majority) and
+ * \Drupal\Core\Ajax\AjaxResponse\AjaxResponse objects (a sizable minority that
+ * often involve a fair amount of rendering) to still do early rendering. But
+ * controllers that return any other kind of response are already expected to
+ * do the right thing, so if early rendering is detected in such a case, an
+ * exception is thrown.
+ *
+ * @see \Drupal\Core\Render\RendererInterface
+ * @see \Drupal\Core\Render\Renderer
+ *
+ * @todo Remove in Drupal 9.0.0, by disallowing early rendering.
+ */
+class EarlyRenderingControllerWrapperSubscriber implements EventSubscriberInterface {
+
+  /**
+   * The controller resolver.
+   *
+   * @var \Drupal\Core\Controller\ControllerResolverInterface
+   */
+  protected $controllerResolver;
+
+  /**
+   * The renderer.
+   *
+   * @var \Drupal\Core\Render\RendererInterface
+   */
+  protected $renderer;
+
+  /**
+   * Constructs a new EarlyRenderingControllerWrapperSubscriber instance.
+   *
+   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
+   *   The controller resolver.
+   * @param \Drupal\Core\Render\RendererInterface $renderer
+   *   The renderer.
+   */
+  public function __construct(ControllerResolverInterface $controller_resolver, RendererInterface $renderer) {
+    $this->controllerResolver = $controller_resolver;
+    $this->renderer = $renderer;
+  }
+
+  /**
+   * Ensures bubbleable metadata from early rendering is not lost.
+   *
+   * @param \Symfony\Component\HttpKernel\Event\FilterControllerEvent $event
+   *   The controller event.
+   */
+  public function onController(FilterControllerEvent $event) {
+    $controller = $event->getController();
+
+    // See \Symfony\Component\HttpKernel\HttpKernel::handleRaw().
+    $arguments = $this->controllerResolver->getArguments($event->getRequest(), $controller);
+
+    $event->setController(function() use ($controller, $arguments) {
+      return $this->wrapControllerExecutionInRenderContext($controller, $arguments);
+    });
+  }
+
+  /**
+   * Wraps a controller execution in a render context.
+   *
+   * @param callable $controller
+   *   The controller to execute.
+   * @param array $arguments
+   *   The arguments to pass to the controller.
+   *
+   * @return mixed
+   *   The return value of the controller.
+   *
+   * @throws \LogicException
+   *   When early rendering has occurred in a controller that returned a
+   *   Response or domain object that cares about attachments or cacheability.
+   *
+   * @see \Symfony\Component\HttpKernel\HttpKernel::handleRaw()
+   */
+  protected function wrapControllerExecutionInRenderContext($controller, array $arguments) {
+    $context = new RenderContext();
+
+    $response = $this->renderer->executeInRenderContext($context, function() use ($controller, $arguments) {
+      // Now call the actual controller, just like HttpKernel does.
+      return call_user_func_array($controller, $arguments);
+    });
+
+    // If early rendering happened, i.e. if code in the controller called
+    // drupal_render() outside of a render context, then the bubbleable metadata
+    // for that is stored in the current render context.
+    if (!$context->isEmpty()) {
+      /** @var \Drupal\Core\Render\BubbleableMetadata $early_rendering_bubbleable_metadata */
+      $early_rendering_bubbleable_metadata = $context->pop();
+
+      // If a render array or AjaxResponse is returned by the controller, merge
+      // the "lost" bubbleable metadata.
+      if (is_array($response)) {
+        BubbleableMetadata::createFromRenderArray($response)
+          ->merge($early_rendering_bubbleable_metadata)
+          ->applyTo($response);
+      }
+      elseif ($response instanceof AjaxResponse) {
+        $response->addAttachments($early_rendering_bubbleable_metadata->getAttachments());
+        // @todo Make AjaxResponse cacheable in
+        //   https://www.drupal.org/node/956186. Meanwhile, allow contrib
+        //   subclasses to be.
+        if ($response instanceof CacheableResponseInterface) {
+          $response->addCacheableDependency($early_rendering_bubbleable_metadata);
+        }
+      }
+      // If a non-Ajax Response or domain object is returned and it cares about
+      // attachments or cacheability, then throw an exception: early rendering
+      // is not permitted in that case. It is the developer's responsibility
+      // to not use early rendering.
+      elseif ($response instanceof AttachmentsInterface || $response instanceof CacheableResponseInterface || $response instanceof CacheableDependencyInterface) {
+        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)));
+      }
+      else {
+        // A Response or domain object is returned that does not care about
+        // attachments nor cacheability; for instance, a RedirectResponse. It is
+        // safe to discard any early rendering metadata.
+      }
+    }
+
+    return $response;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function getSubscribedEvents() {
+    $events[KernelEvents::CONTROLLER][] = ['onController'];
+
+    return $events;
+  }
+
+}