/**
  * 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);
             }
         } 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;
 }
예제 #2
0
 /**
  * {@inheritdoc}
  */
 public function render()
 {
     $build = array();
     $build['#markup'] = $this->renderer->executeInRenderContext(new RenderContext(), function () {
         return $this->view->style_plugin->render();
     });
     $this->view->element['#content_type'] = $this->getMimeType();
     $this->view->element['#cache_properties'][] = '#content_type';
     // Encode and wrap the output in a pre tag if this is for a live preview.
     if (!empty($this->view->live_preview)) {
         $build['#prefix'] = '<pre>';
         $build['#plain_text'] = $build['#markup'];
         $build['#suffix'] = '</pre>';
         unset($build['#markup']);
     } elseif ($this->view->getRequest()->getFormat($this->view->element['#content_type']) !== 'html') {
         // This display plugin is primarily for returning non-HTML formats.
         // However, we still invoke the renderer to collect cacheability metadata.
         // Because the renderer is designed for HTML rendering, it filters
         // #markup for XSS unless it is already known to be safe, but that filter
         // only works for HTML. Therefore, we mark the contents as safe to bypass
         // the filter. So long as we are returning this in a non-HTML response
         // (checked above), this is safe, because an XSS attack only works when
         // executed by an HTML agent.
         // @todo Decide how to support non-HTML in the render API in
         //   https://www.drupal.org/node/2501313.
         $build['#markup'] = ViewsRenderPipelineMarkup::create($build['#markup']);
     }
     parent::applyDisplayCachablityMetadata($build);
     return $build;
 }
 /**
  * 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()) {
         // If a render array is returned by the controller, merge the "lost"
         // bubbleable metadata.
         if (is_array($response)) {
             $early_rendering_bubbleable_metadata = $context->pop();
             BubbleableMetadata::createFromRenderArray($response)->merge($early_rendering_bubbleable_metadata)->applyTo($response);
         } 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. E.g. a RedirectResponse. It is safe to
             // discard any early rendering metadata.
         }
     }
     return $response;
 }
예제 #4
0
 /**
  * Prepares the HTML body: wraps the main content in #type 'page'.
  *
  * @param array $main_content
  *   The render array representing the main content.
  * @param \Symfony\Component\HttpFoundation\Request $request
  *   The request object, for context.
  * @param \Drupal\Core\Routing\RouteMatchInterface $route_match
  *   The route match, for context.
  *
  * @return array
  *   An array with two values:
  *   0. A #type 'page' render array.
  *   1. The page title.
  *
  * @throws \LogicException
  *   If the selected display variant does not implement PageVariantInterface.
  */
 protected function prepare(array $main_content, Request $request, RouteMatchInterface $route_match)
 {
     // If the _controller result already is #type => page,
     // we have no work to do: The "main content" already is an entire "page"
     // (see html.html.twig).
     if (isset($main_content['#type']) && $main_content['#type'] === 'page') {
         $page = $main_content;
     } else {
         // Select the page display variant to be used to render this main content,
         // default to the built-in "simple page".
         $event = new PageDisplayVariantSelectionEvent('simple_page', $route_match);
         $this->eventDispatcher->dispatch(RenderEvents::SELECT_PAGE_DISPLAY_VARIANT, $event);
         $variant_id = $event->getPluginId();
         // We must render the main content now already, because it might provide a
         // title. We set its $is_root_call parameter to FALSE, to ensure
         // placeholders are not yet replaced. This is essentially "pre-rendering"
         // the main content, the "full rendering" will happen in
         // ::renderResponse().
         // @todo Remove this once https://www.drupal.org/node/2359901 lands.
         if (!empty($main_content)) {
             $this->renderer->executeInRenderContext(new RenderContext(), function () use(&$main_content) {
                 if (isset($main_content['#cache']['keys'])) {
                     // Retain #title, otherwise, dynamically generated titles would be
                     // missing for controllers whose entire returned render array is
                     // render cached.
                     $main_content['#cache_properties'][] = '#title';
                 }
                 return $this->renderer->render($main_content, FALSE);
             });
             $main_content = $this->renderCache->getCacheableRenderArray($main_content) + ['#title' => isset($main_content['#title']) ? $main_content['#title'] : NULL];
         }
         // Instantiate the page display, and give it the main content.
         $page_display = $this->displayVariantManager->createInstance($variant_id);
         if (!$page_display instanceof PageVariantInterface) {
             throw new \LogicException('Cannot render the main content for this page because the provided display variant does not implement PageVariantInterface.');
         }
         $page_display->setMainContent($main_content)->setConfiguration($event->getPluginConfiguration());
         // Generate a #type => page render array using the page display variant,
         // the page display will build the content for the various page regions.
         $page = array('#type' => 'page');
         $page += $page_display->build();
     }
     // $page is now fully built. Find all non-empty page regions, and add a
     // theme wrapper function that allows them to be consistently themed.
     $regions = \Drupal::theme()->getActiveTheme()->getRegions();
     foreach ($regions as $region) {
         if (!empty($page[$region])) {
             $page[$region]['#theme_wrappers'][] = 'region';
             $page[$region]['#region'] = $region;
         }
     }
     // Allow hooks to add attachments to $page['#attached'].
     $this->invokePageAttachmentHooks($page);
     // Determine the title: use the title provided by the main content if any,
     // otherwise get it from the routing information.
     $title = isset($main_content['#title']) ? $main_content['#title'] : $this->titleResolver->getTitle($request, $route_match->getRouteObject());
     return [$page, $title];
 }
예제 #5
0
 /**
  * {@inheritdoc}
  */
 public function mail($module, $key, $to, $langcode, $params = array(), $reply = NULL, $send = TRUE)
 {
     // Mailing can invoke rendering (e.g., generating URLs, replacing tokens),
     // but e-mails are not HTTP responses: they're not cached, they don't have
     // attachments. Therefore we perform mailing inside its own render context,
     // to ensure it doesn't leak into the render context for the HTTP response
     // to the current request.
     return $this->renderer->executeInRenderContext(new RenderContext(), function () use($module, $key, $to, $langcode, $params, $reply, $send) {
         return $this->doMail($module, $key, $to, $langcode, $params, $reply, $send);
     });
 }
예제 #6
0
 /**
  * Loads and renders a view via AJAX.
  *
  * @param \Symfony\Component\HttpFoundation\Request $request
  *   The current request object.
  *
  * @return \Drupal\views\Ajax\ViewAjaxResponse
  *   The view response as ajax response.
  *
  * @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
  *   Thrown when the view was not found.
  */
 public function ajaxView(Request $request)
 {
     $name = $request->request->get('view_name');
     $display_id = $request->request->get('view_display_id');
     if (isset($name) && isset($display_id)) {
         $args = $request->request->get('view_args');
         $args = isset($args) && $args !== '' ? explode('/', $args) : array();
         // Arguments can be empty, make sure they are passed on as NULL so that
         // argument validation is not triggered.
         $args = array_map(function ($arg) {
             return $arg == '' ? NULL : $arg;
         }, $args);
         $path = $request->request->get('view_path');
         $dom_id = $request->request->get('view_dom_id');
         $dom_id = isset($dom_id) ? preg_replace('/[^a-zA-Z0-9_-]+/', '-', $dom_id) : NULL;
         $pager_element = $request->request->get('pager_element');
         $pager_element = isset($pager_element) ? intval($pager_element) : NULL;
         $response = new ViewAjaxResponse();
         // Remove all of this stuff from the query of the request so it doesn't
         // end up in pagers and tablesort URLs.
         foreach (array('view_name', 'view_display_id', 'view_args', 'view_path', 'view_dom_id', 'pager_element', 'view_base_path', AjaxResponseSubscriber::AJAX_REQUEST_PARAMETER) as $key) {
             $request->query->remove($key);
             $request->request->remove($key);
         }
         // Load the view.
         if (!($entity = $this->storage->load($name))) {
             throw new NotFoundHttpException();
         }
         $view = $this->executableFactory->get($entity);
         if ($view && $view->access($display_id)) {
             $response->setView($view);
             // Fix the current path for paging.
             if (!empty($path)) {
                 $this->currentPath->setPath('/' . $path, $request);
             }
             // Add all POST data, because AJAX is always a post and many things,
             // such as tablesorts, exposed filters and paging assume GET.
             $request_all = $request->request->all();
             $query_all = $request->query->all();
             $request->query->replace($request_all + $query_all);
             // Overwrite the destination.
             // @see the redirect.destination service.
             $origin_destination = $path;
             // Remove some special parameters you never want to have part of the
             // destination query.
             $used_query_parameters = $request->query->all();
             // @todo Remove this parsing once these are removed from the request in
             //   https://www.drupal.org/node/2504709.
             unset($used_query_parameters[FormBuilderInterface::AJAX_FORM_REQUEST], $used_query_parameters[MainContentViewSubscriber::WRAPPER_FORMAT], $used_query_parameters['ajax_page_state']);
             $query = UrlHelper::buildQuery($used_query_parameters);
             if ($query != '') {
                 $origin_destination .= '?' . $query;
             }
             $this->redirectDestination->set($origin_destination);
             // Override the display's pager_element with the one actually used.
             if (isset($pager_element)) {
                 $response->addCommand(new ScrollTopCommand(".js-view-dom-id-{$dom_id}"));
                 $view->displayHandlers->get($display_id)->setOption('pager_element', $pager_element);
             }
             // Reuse the same DOM id so it matches that in drupalSettings.
             $view->dom_id = $dom_id;
             $context = new RenderContext();
             $preview = $this->renderer->executeInRenderContext($context, function () use($view, $display_id, $args) {
                 return $view->preview($display_id, $args);
             });
             if (!$context->isEmpty()) {
                 $bubbleable_metadata = $context->pop();
                 BubbleableMetadata::createFromRenderArray($preview)->merge($bubbleable_metadata)->applyTo($preview);
             }
             $response->addCommand(new ReplaceCommand(".js-view-dom-id-{$dom_id}", $preview));
             return $response;
         } else {
             throw new AccessDeniedHttpException();
         }
     } else {
         throw new NotFoundHttpException();
     }
 }