Renderer.php 30.4 KB
Newer Older
1 2 3 4
<?php

namespace Drupal\Core\Render;

5
use Drupal\Component\Render\MarkupInterface;
6
use Drupal\Component\Utility\Html;
7
use Drupal\Component\Utility\Xss;
8
use Drupal\Core\Access\AccessResultInterface;
9
use Drupal\Core\Cache\Cache;
10
use Drupal\Core\Cache\CacheableMetadata;
11 12
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
13
use Symfony\Component\HttpFoundation\RequestStack;
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

/**
 * Turns a render array into a HTML string.
 */
class Renderer implements RendererInterface {

  /**
   * The theme manager.
   *
   * @var \Drupal\Core\Theme\ThemeManagerInterface
   */
  protected $theme;

  /**
   * The controller resolver.
   *
   * @var \Drupal\Core\Controller\ControllerResolverInterface
   */
  protected $controllerResolver;

  /**
   * The element info.
   *
   * @var \Drupal\Core\Render\ElementInfoManagerInterface
   */
  protected $elementInfo;

41 42 43 44 45 46 47
  /**
   * The placeholder generator.
   *
   * @var \Drupal\Core\Render\PlaceholderGeneratorInterface
   */
  protected $placeholderGenerator;

48
  /**
49
   * The render cache service.
50
   *
51
   * @var \Drupal\Core\Render\RenderCacheInterface
52
   */
53
  protected $renderCache;
54

55 56 57 58 59 60 61
  /**
   * The renderer configuration array.
   *
   * @var array
   */
  protected $rendererConfig;

62
  /**
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
   * Whether we're currently in a ::renderRoot() call.
   *
   * @var bool
   */
  protected $isRenderingRoot = FALSE;

  /**
   * The request stack.
   *
   * @var \Symfony\Component\HttpFoundation\RequestStack
   */
  protected $requestStack;

  /**
   * The render context collection.
   *
   * An individual global render context is tied to the current request. We then
   * need to maintain a different context for each request to correctly handle
   * rendering in subrequests.
82
   *
83 84 85 86 87 88
   * This must be static as long as some controllers rebuild the container
   * during a request. This causes multiple renderer instances to co-exist
   * simultaneously, render state getting lost, and therefore causing pages to
   * fail to render correctly. As soon as it is guaranteed that during a request
   * the same container is used, it no longer needs to be static.
   *
89
   * @var \Drupal\Core\Render\RenderContext[]
90
   */
91
  protected static $contextCollection;
92

93 94 95 96 97 98 99 100 101
  /**
   * Constructs a new Renderer.
   *
   * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
   *   The controller resolver.
   * @param \Drupal\Core\Theme\ThemeManagerInterface $theme
   *   The theme manager.
   * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
   *   The element info.
102 103
   * @param \Drupal\Core\Render\PlaceholderGeneratorInterface $placeholder_generator
   *   The placeholder generator.
104 105
   * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
   *   The render cache service.
106 107
   * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
   *   The request stack.
108 109
   * @param array $renderer_config
   *   The renderer configuration array.
110
   */
111
  public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, PlaceholderGeneratorInterface $placeholder_generator, RenderCacheInterface $render_cache, RequestStack $request_stack, array $renderer_config) {
112 113 114
    $this->controllerResolver = $controller_resolver;
    $this->theme = $theme;
    $this->elementInfo = $element_info;
115
    $this->placeholderGenerator = $placeholder_generator;
116
    $this->renderCache = $render_cache;
117
    $this->rendererConfig = $renderer_config;
118 119 120 121 122 123
    $this->requestStack = $request_stack;

    // Initialize the context collection if needed.
    if (!isset(static::$contextCollection)) {
      static::$contextCollection = new \SplObjectStorage();
    }
124 125 126 127 128 129
  }

  /**
   * {@inheritdoc}
   */
  public function renderRoot(&$elements) {
130 131 132 133 134 135 136 137 138 139 140 141 142 143
    // Disallow calling ::renderRoot() from within another ::renderRoot() call.
    if ($this->isRenderingRoot) {
      $this->isRenderingRoot = FALSE;
      throw new \LogicException('A stray renderRoot() invocation is causing bubbling of attached assets to break.');
    }

    // Render in its own render context.
    $this->isRenderingRoot = TRUE;
    $output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
      return $this->render($elements, TRUE);
    });
    $this->isRenderingRoot = FALSE;

    return $output;
144 145 146 147 148
  }

  /**
   * {@inheritdoc}
   */
149
  public function renderPlain(&$elements) {
150 151 152
    return $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
      return $this->render($elements, TRUE);
    });
153
  }
154

155
  /**
156
   * {@inheritdoc}
157
   */
158
  public function renderPlaceholder($placeholder, array $elements) {
159 160 161
    // Get the render array for the given placeholder
    $placeholder_elements = $elements['#attached']['placeholders'][$placeholder];

162 163 164
    // Prevent the render array from being auto-placeholdered again.
    $placeholder_elements['#create_placeholder'] = FALSE;

165 166 167 168 169
    // Render the placeholder into markup.
    $markup = $this->renderPlain($placeholder_elements);

    // Replace the placeholder with its rendered markup, and merge its
    // bubbleable metadata with the main elements'.
170
    $elements['#markup'] = Markup::create(str_replace($placeholder, $markup, $elements['#markup']));
171 172 173 174 175 176 177 178
    $elements = $this->mergeBubbleableMetadata($elements, $placeholder_elements);

    // Remove the placeholder that we've just rendered.
    unset($elements['#attached']['placeholders'][$placeholder]);

    return $elements;
  }

179 180 181 182
  /**
   * {@inheritdoc}
   */
  public function render(&$elements, $is_root_call = FALSE) {
183 184 185 186 187
    // Since #pre_render, #post_render, #lazy_builder callbacks and theme
    // functions or templates may be used for generating a render array's
    // content, and we might be rendering the main content for the page, it is
    // possible that any of them throw an exception that will cause a different
    // page to be rendered (e.g. throwing
188
    // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause
189 190 191 192 193
    // the 404 page to be rendered). That page might also use
    // Renderer::renderRoot() but if exceptions aren't caught here, it will be
    // impossible to call Renderer::renderRoot() again.
    // Hence, catch all exceptions, reset the isRenderingRoot property and
    // re-throw exceptions.
194 195 196 197
    try {
      return $this->doRender($elements, $is_root_call);
    }
    catch (\Exception $e) {
198 199
      // Mark the ::rootRender() call finished due to this exception & re-throw.
      $this->isRenderingRoot = FALSE;
200 201 202 203 204 205 206 207
      throw $e;
    }
  }

  /**
   * See the docs for ::render().
   */
  protected function doRender(&$elements, $is_root_call = FALSE) {
208 209 210 211
    if (empty($elements)) {
      return '';
    }

212 213 214 215 216 217 218 219
    if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
      if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
        $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
      }
      $elements['#access'] = call_user_func($elements['#access_callback'], $elements);
    }

    // Early-return nothing if user does not have access.
220 221 222 223 224 225 226 227 228 229 230 231
    if (isset($elements['#access'])) {
      // If #access is an AccessResultInterface object, we must apply it's
      // cacheability metadata to the render array.
      if ($elements['#access'] instanceof AccessResultInterface) {
        $this->addCacheableDependency($elements, $elements['#access']);
        if (!$elements['#access']->isAllowed()) {
          return '';
        }
      }
      elseif ($elements['#access'] === FALSE) {
        return '';
      }
232 233 234 235 236 237 238
    }

    // Do not print elements twice.
    if (!empty($elements['#printed'])) {
      return '';
    }

239 240
    $context = $this->getCurrentRenderContext();
    if (!isset($context)) {
241
      throw new \LogicException("Render context is empty, because render() was called outside of a renderRoot() or renderPlain() call. Use renderPlain()/renderRoot() or #lazy_builder/#pre_render instead.");
242
    }
243
    $context->push(new BubbleableMetadata());
244

245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
    // Set the bubbleable rendering metadata that has configurable defaults, if:
    // - this is the root call, to ensure that the final render array definitely
    //   has these configurable defaults, even when no subtree is render cached.
    // - this is a render cacheable subtree, to ensure that the cached data has
    //   the configurable defaults (which may affect the ID and invalidation).
    if ($is_root_call || isset($elements['#cache']['keys'])) {
      $required_cache_contexts = $this->rendererConfig['required_cache_contexts'];
      if (isset($elements['#cache']['contexts'])) {
        $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts);
      }
      else {
        $elements['#cache']['contexts'] = $required_cache_contexts;
      }
    }

260 261
    // Try to fetch the prerendered element from cache, replace any placeholders
    // and return the final markup.
262
    if (isset($elements['#cache']['keys'])) {
263
      $cached_element = $this->renderCache->get($elements);
264 265
      if ($cached_element !== FALSE) {
        $elements = $cached_element;
266 267 268
        // Only when we're in a root (non-recursive) Renderer::render() call,
        // placeholders must be processed, to prevent breaking the render cache
        // in case of nested elements with #cache set.
269
        if ($is_root_call) {
270
          $this->replacePlaceholders($elements);
271
        }
272 273
        // Mark the element markup as safe if is it a string.
        if (is_string($elements['#markup'])) {
274
          $elements['#markup'] = Markup::create($elements['#markup']);
275
        }
276 277
        // The render cache item contains all the bubbleable rendering metadata
        // for the subtree.
278
        $context->update($elements);
279 280
        // Render cache hit, so rendering is finished, all necessary info
        // collected!
281
        $context->bubble();
282 283 284
        return $elements['#markup'];
      }
    }
285 286
    // Two-tier caching: track pre-bubbling elements' #cache, #lazy_builder and
    // #create_placeholder for later comparison.
287 288
    // @see \Drupal\Core\Render\RenderCacheInterface::get()
    // @see \Drupal\Core\Render\RenderCacheInterface::set()
289 290 291 292 293
    $pre_bubbling_elements = array_intersect_key($elements, [
      '#cache' => TRUE,
      '#lazy_builder' => TRUE,
      '#create_placeholder' => TRUE,
    ]);
294 295 296 297 298 299 300

    // If the default values for this element have not been loaded yet, populate
    // them.
    if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
      $elements += $this->elementInfo->getInfo($elements['#type']);
    }

301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322
    // First validate the usage of #lazy_builder; both of the next if-statements
    // use it if available.
    if (isset($elements['#lazy_builder'])) {
      // @todo Convert to assertions once https://www.drupal.org/node/2408013
      //   lands.
      if (!is_array($elements['#lazy_builder'])) {
        throw new \DomainException('The #lazy_builder property must have an array as a value.');
      }
      if (count($elements['#lazy_builder']) !== 2) {
        throw new \DomainException('The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.');
      }
      if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function($v) { return is_null($v) || is_scalar($v); }))) {
        throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL.");
      }
      $children = Element::children($elements);
      if ($children) {
        throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no children can exist; all children must be generated by the #lazy_builder callback. You specified the following children: %s.', implode(', ', $children)));
      }
      $supported_keys = [
        '#lazy_builder',
        '#cache',
        '#create_placeholder',
323 324 325
        // The keys below are not actually supported, but these are added
        // automatically by the Renderer. Adding them as though they are
        // supported allows us to avoid throwing an exception 100% of the time.
326 327 328 329 330 331 332 333
        '#weight',
        '#printed'
      ];
      $unsupported_keys = array_diff(array_keys($elements), $supported_keys);
      if (count($unsupported_keys)) {
        throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys)));
      }
    }
334
    // Determine whether to do auto-placeholdering.
335
    if ($this->placeholderGenerator->canCreatePlaceholder($elements) && $this->placeholderGenerator->shouldAutomaticallyPlaceholder($elements)) {
336 337
      $elements['#create_placeholder'] = TRUE;
    }
338 339 340
    // If instructed to create a placeholder, and a #lazy_builder callback is
    // present (without such a callback, it would be impossible to replace the
    // placeholder), replace the current element with a placeholder.
341
    // @todo remove the isMethodCacheable() check when
342
    //       https://www.drupal.org/node/2367555 lands.
343
    if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE && $this->requestStack->getCurrentRequest()->isMethodCacheable()) {
344 345 346
      if (!isset($elements['#lazy_builder'])) {
        throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.');
      }
347
      $elements = $this->placeholderGenerator->createPlaceholder($elements);
348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366
    }
    // Build the element if it is still empty.
    if (isset($elements['#lazy_builder'])) {
      $callable = $elements['#lazy_builder'][0];
      $args = $elements['#lazy_builder'][1];
      if (is_string($callable) && strpos($callable, '::') === FALSE) {
        $callable = $this->controllerResolver->getControllerFromDefinition($callable);
      }
      $new_elements = call_user_func_array($callable, $args);
      // Retain the original cacheability metadata, plus cache keys.
      CacheableMetadata::createFromRenderArray($elements)
        ->merge(CacheableMetadata::createFromRenderArray($new_elements))
        ->applyTo($new_elements);
      if (isset($elements['#cache']['keys'])) {
        $new_elements['#cache']['keys'] = $elements['#cache']['keys'];
      }
      $elements = $new_elements;
      $elements['#lazy_builder_built'] = TRUE;
    }
367

368 369 370 371 372 373 374 375
    // Make any final changes to the element before it is rendered. This means
    // that the $element or the children can be altered or corrected before the
    // element is rendered into the final text.
    if (isset($elements['#pre_render'])) {
      foreach ($elements['#pre_render'] as $callable) {
        if (is_string($callable) && strpos($callable, '::') === FALSE) {
          $callable = $this->controllerResolver->getControllerFromDefinition($callable);
        }
376
        $elements = call_user_func($callable, $elements);
377 378 379
      }
    }

380 381 382 383 384
    // All render elements support #markup and #plain_text.
    if (!empty($elements['#markup']) || !empty($elements['#plain_text'])) {
      $elements = $this->ensureMarkupIsSafe($elements);
    }

385
    // Defaults for bubbleable rendering metadata.
386
    $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : [];
387
    $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT;
388
    $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : [];
389 390 391 392 393

    // Allow #pre_render to abort rendering.
    if (!empty($elements['#printed'])) {
      // The #printed element contains all the bubbleable rendering metadata for
      // the subtree.
394
      $context->update($elements);
395
      // #printed, so rendering is finished, all necessary info collected!
396
      $context->bubble();
397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417
      return '';
    }

    // Add any JavaScript state information associated with the element.
    if (!empty($elements['#states'])) {
      drupal_process_states($elements);
    }

    // Get the children of the element, sorted by weight.
    $children = Element::children($elements, TRUE);

    // Initialize this element's #children, unless a #pre_render callback
    // already preset #children.
    if (!isset($elements['#children'])) {
      $elements['#children'] = '';
    }

    // Assume that if #theme is set it represents an implemented hook.
    $theme_is_implemented = isset($elements['#theme']);
    // Check the elements for insecure HTML and pass through sanitization.
    if (isset($elements)) {
418
      $markup_keys = [
419 420 421
        '#description',
        '#field_prefix',
        '#field_suffix',
422
      ];
423 424
      foreach ($markup_keys as $key) {
        if (!empty($elements[$key]) && is_scalar($elements[$key])) {
425
          $elements[$key] = $this->xssFilterAdminIfUnsafe($elements[$key]);
426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447
        }
      }
    }

    // Call the element's #theme function if it is set. Then any children of the
    // element have to be rendered there. If the internal #render_children
    // property is set, do not call the #theme function to prevent infinite
    // recursion.
    if ($theme_is_implemented && !isset($elements['#render_children'])) {
      $elements['#children'] = $this->theme->render($elements['#theme'], $elements);

      // If ThemeManagerInterface::render() returns FALSE this means that the
      // hook in #theme was not found in the registry and so we need to update
      // our flag accordingly. This is common for theme suggestions.
      $theme_is_implemented = ($elements['#children'] !== FALSE);
    }

    // If #theme is not implemented or #render_children is set and the element
    // has an empty #children attribute, render the children now. This is the
    // same process as Renderer::render() but is inlined for speed.
    if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
      foreach ($children as $key) {
448
        $elements['#children'] .= $this->doRender($elements[$key]);
449
      }
450
      $elements['#children'] = Markup::create($elements['#children']);
451 452 453 454 455 456 457 458 459 460
    }

    // If #theme is not implemented and the element has raw #markup as a
    // fallback, prepend the content in #markup to #children. In this case
    // #children will contain whatever is provided by #pre_render prepended to
    // what is rendered recursively above. If #theme is implemented then it is
    // the responsibility of that theme implementation to render #markup if
    // required. Eventually #theme_wrappers will expect both #markup and
    // #children to be a single string as #children.
    if (!$theme_is_implemented && isset($elements['#markup'])) {
461
      $elements['#children'] = Markup::create($elements['#markup'] . $elements['#children']);
462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507
    }

    // Let the theme functions in #theme_wrappers add markup around the rendered
    // children.
    // #states and #attached have to be processed before #theme_wrappers,
    // because the #type 'page' render array from drupal_prepare_page() would
    // render the $page and wrap it into the html.html.twig template without the
    // attached assets otherwise.
    // If the internal #render_children property is set, do not call the
    // #theme_wrappers function(s) to prevent infinite recursion.
    if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
      foreach ($elements['#theme_wrappers'] as $key => $value) {
        // If the value of a #theme_wrappers item is an array then the theme
        // hook is found in the key of the item and the value contains attribute
        // overrides. Attribute overrides replace key/value pairs in $elements
        // for only this ThemeManagerInterface::render() call. This allows
        // #theme hooks and #theme_wrappers hooks to share variable names
        // without conflict or ambiguity.
        $wrapper_elements = $elements;
        if (is_string($key)) {
          $wrapper_hook = $key;
          foreach ($value as $attribute => $override) {
            $wrapper_elements[$attribute] = $override;
          }
        }
        else {
          $wrapper_hook = $value;
        }

        $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements);
      }
    }

    // Filter the outputted content and make any last changes before the content
    // is sent to the browser. The changes are made on $content which allows the
    // outputted text to be filtered.
    if (isset($elements['#post_render'])) {
      foreach ($elements['#post_render'] as $callable) {
        if (is_string($callable) && strpos($callable, '::') === FALSE) {
          $callable = $this->controllerResolver->getControllerFromDefinition($callable);
        }
        $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
      }
    }

    // We store the resulting output in $elements['#markup'], to be consistent
508 509 510
    // with how render cached output gets stored. This ensures that placeholder
    // replacement logic gets the same data to work with, no matter if #cache is
    // disabled, #cache is enabled, there is a cache hit or miss.
511 512
    $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : '';
    $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : '';
513

514
    $elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix);
515

516
    // We've rendered this element (and its subtree!), now update the context.
517
    $context->update($elements);
518

519 520 521 522 523 524
    // Cache the processed element if both $pre_bubbling_elements and $elements
    // have the metadata necessary to generate a cache ID.
    if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) {
      if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) {
        throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.');
      }
525
      $this->renderCache->set($elements, $pre_bubbling_elements);
526 527 528 529 530 531
      // Update the render context; the render cache implementation may update
      // the element, and it may have different bubbleable metadata now.
      // @see \Drupal\Core\Render\PlaceholderingRenderCache::set()
      $context->pop();
      $context->push(new BubbleableMetadata());
      $context->update($elements);
532 533
    }

534 535 536
    // Only when we're in a root (non-recursive) Renderer::render() call,
    // placeholders must be processed, to prevent breaking the render cache in
    // case of nested elements with #cache set.
537 538 539 540 541 542 543
    //
    // By running them here, we ensure that:
    // - they run when #cache is disabled,
    // - they run when #cache is enabled and there is a cache miss.
    // Only the case of a cache hit when #cache is enabled, is not handled here,
    // that is handled earlier in Renderer::render().
    if ($is_root_call) {
544
      $this->replacePlaceholders($elements);
545
      // @todo remove as part of https://www.drupal.org/node/2511330.
546
      if ($context->count() !== 1) {
547 548 549 550 551
        throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
      }
    }

    // Rendering is finished, all necessary info collected!
552
    $context->bubble();
553 554

    $elements['#printed'] = TRUE;
555
    return $elements['#markup'];
556 557
  }

558 559 560 561 562 563 564
  /**
   * {@inheritdoc}
   */
  public function hasRenderContext() {
    return (bool) $this->getCurrentRenderContext();
  }

565
  /**
566
   * {@inheritdoc}
567
   */
568 569
  public function executeInRenderContext(RenderContext $context, callable $callable) {
    // Store the current render context.
570
    $previous_context = $this->getCurrentRenderContext();
571 572

    // Set the provided context and call the callable, it will use that context.
573
    $this->setCurrentRenderContext($context);
574 575
    $result = $callable();
    // @todo Convert to an assertion in https://www.drupal.org/node/2408013
576
    if ($context->count() > 1) {
577
      throw new \LogicException('Bubbling failed.');
578 579
    }

580
    // Restore the original render context.
581
    $this->setCurrentRenderContext($previous_context);
582 583

    return $result;
584 585
  }

586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
  /**
   * Returns the current render context.
   *
   * @return \Drupal\Core\Render\RenderContext
   *   The current render context.
   */
  protected function getCurrentRenderContext() {
    $request = $this->requestStack->getCurrentRequest();
    return isset(static::$contextCollection[$request]) ? static::$contextCollection[$request] : NULL;
  }

  /**
   * Sets the current render context.
   *
   * @param \Drupal\Core\Render\RenderContext|null $context
   *   The render context. This can be NULL for instance when restoring the
   *   original render context, which is in fact NULL.
   *
   * @return $this
   */
  protected function setCurrentRenderContext(RenderContext $context = NULL) {
    $request = $this->requestStack->getCurrentRequest();
    static::$contextCollection[$request] = $context;
    return $this;
  }

612
  /**
613
   * Replaces placeholders.
614
   *
615 616 617 618
   * Placeholders may have:
   * - #lazy_builder callback, to build a render array to be rendered into
   *   markup that can replace the placeholder
   * - #cache: to cache the result of the placeholder
619
   *
620 621 622
   * Also merges the bubbleable metadata resulting from the rendering of the
   * contents of the placeholders. Hence $elements will be contain the entirety
   * of bubbleable metadata.
623 624
   *
   * @param array &$elements
625 626 627 628 629 630
   *   The structured array describing the data being rendered. Including the
   *   bubbleable metadata associated with the markup that replaced the
   *   placeholders.
   *
   * @returns bool
   *   Whether placeholders were replaced.
631 632
   *
   * @see \Drupal\Core\Render\Renderer::renderPlaceholder()
633
   */
634 635 636 637
  protected function replacePlaceholders(array &$elements) {
    if (!isset($elements['#attached']['placeholders']) || empty($elements['#attached']['placeholders'])) {
      return FALSE;
    }
638

639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665
    // The 'status messages' placeholder needs to be special cased, because it
    // depends on global state that can be modified when other placeholders are
    // being rendered: any code can add messages to render.
    // This violates the principle that each lazy builder must be able to render
    // itself in isolation, and therefore in any order. However, we cannot
    // change the way drupal_set_message() works in the Drupal 8 cycle. So we
    // have to accommodate its special needs.
    // Allowing placeholders to be rendered in a particular order (in this case:
    // last) would violate this isolation principle. Thus a monopoly is granted
    // to this one special case, with this hard-coded solution.
    // @see \Drupal\Core\Render\Element\StatusMessages
    // @see https://www.drupal.org/node/2712935#comment-11368923

    // First render all placeholders except 'status messages' placeholders.
    $message_placeholders = [];
    foreach ($elements['#attached']['placeholders'] as $placeholder => $placeholder_element) {
      if (isset($placeholder_element['#lazy_builder']) && $placeholder_element['#lazy_builder'][0] === 'Drupal\Core\Render\Element\StatusMessages::renderMessages') {
        $message_placeholders[] = $placeholder;
      }
      else {
        $elements = $this->renderPlaceholder($placeholder, $elements);
      }
    }

    // Then render 'status messages' placeholders.
    foreach ($message_placeholders as $message_placeholder) {
      $elements = $this->renderPlaceholder($message_placeholder, $elements);
666
    }
667 668 669 670

    return TRUE;
  }

671 672 673
  /**
   * {@inheritdoc}
   */
674
  public function mergeBubbleableMetadata(array $a, array $b) {
675 676 677 678 679 680
    $meta_a = BubbleableMetadata::createFromRenderArray($a);
    $meta_b = BubbleableMetadata::createFromRenderArray($b);
    $meta_a->merge($meta_b)->applyTo($a);
    return $a;
  }

681 682 683
  /**
   * {@inheritdoc}
   */
684
  public function addCacheableDependency(array &$elements, $dependency) {
685 686
    $meta_a = CacheableMetadata::createFromRenderArray($elements);
    $meta_b = CacheableMetadata::createFromObject($dependency);
687 688 689
    $meta_a->merge($meta_b)->applyTo($elements);
  }

690 691 692 693 694 695
  /**
   * Applies a very permissive XSS/HTML filter for admin-only use.
   *
   * Note: This method only filters if $string is not marked safe already. This
   * ensures that HTML intended for display is not filtered.
   *
696
   * @param string|\Drupal\Core\Render\Markup $string
697 698
   *   A string.
   *
699
   * @return \Drupal\Core\Render\Markup
700 701 702
   *   The escaped string wrapped in a Markup object. If the string is an
   *   instance of \Drupal\Component\Render\MarkupInterface, it won't be escaped
   *   again.
703 704
   */
  protected function xssFilterAdminIfUnsafe($string) {
705
    if (!($string instanceof MarkupInterface)) {
706 707
      $string = Xss::filterAdmin($string);
    }
708
    return Markup::create($string);
709 710
  }

711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728
  /**
   * Escapes #plain_text or filters #markup as required.
   *
   * Drupal uses Twig's auto-escape feature to improve security. This feature
   * automatically escapes any HTML that is not known to be safe. Due to this
   * the render system needs to ensure that all markup it generates is marked
   * safe so that Twig does not do any additional escaping.
   *
   * By default all #markup is filtered to protect against XSS using the admin
   * tag list. Render arrays can alter the list of tags allowed by the filter
   * using the #allowed_tags property. This value should be an array of tags
   * that Xss::filter() would accept. Render arrays can escape text instead
   * of XSS filtering by setting the #plain_text property instead of #markup. If
   * #plain_text is used #allowed_tags is ignored.
   *
   * @param array $elements
   *   A render array with #markup set.
   *
729
   * @return \Drupal\Component\Render\MarkupInterface|string
730 731
   *   The escaped markup wrapped in a Markup object. If $elements['#markup']
   *   is an instance of \Drupal\Component\Render\MarkupInterface, it won't be
732 733 734 735
   *   escaped or filtered again.
   *
   * @see \Drupal\Component\Utility\Html::escape()
   * @see \Drupal\Component\Utility\Xss::filter()
736
   * @see \Drupal\Component\Utility\Xss::filterAdmin()
737 738 739 740 741 742 743
   */
  protected function ensureMarkupIsSafe(array $elements) {
    if (empty($elements['#markup']) && empty($elements['#plain_text'])) {
      return $elements;
    }

    if (!empty($elements['#plain_text'])) {
744
      $elements['#markup'] = Markup::create(Html::escape($elements['#plain_text']));
745
    }
746
    elseif (!($elements['#markup'] instanceof MarkupInterface)) {
747 748
      // The default behaviour is to XSS filter using the admin tag list.
      $tags = isset($elements['#allowed_tags']) ? $elements['#allowed_tags'] : Xss::getAdminTagList();
749
      $elements['#markup'] = Markup::create(Xss::filter($elements['#markup'], $tags));
750 751 752 753 754
    }

    return $elements;
  }

755
}