3 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 use Drupal\Core\Controller\ControllerResolverInterface;
12 use Drupal\Core\Theme\ThemeManagerInterface;
13 use Symfony\Component\HttpFoundation\RequestStack;
16 * Turns a render array into a HTML string.
18 class Renderer implements RendererInterface {
23 * @var \Drupal\Core\Theme\ThemeManagerInterface
28 * The controller resolver.
30 * @var \Drupal\Core\Controller\ControllerResolverInterface
32 protected $controllerResolver;
37 * @var \Drupal\Core\Render\ElementInfoManagerInterface
39 protected $elementInfo;
42 * The placeholder generator.
44 * @var \Drupal\Core\Render\PlaceholderGeneratorInterface
46 protected $placeholderGenerator;
49 * The render cache service.
51 * @var \Drupal\Core\Render\RenderCacheInterface
53 protected $renderCache;
56 * The renderer configuration array.
60 protected $rendererConfig;
63 * Whether we're currently in a ::renderRoot() call.
67 protected $isRenderingRoot = FALSE;
72 * @var \Symfony\Component\HttpFoundation\RequestStack
74 protected $requestStack;
77 * The render context collection.
79 * An individual global render context is tied to the current request. We then
80 * need to maintain a different context for each request to correctly handle
81 * rendering in subrequests.
83 * This must be static as long as some controllers rebuild the container
84 * during a request. This causes multiple renderer instances to co-exist
85 * simultaneously, render state getting lost, and therefore causing pages to
86 * fail to render correctly. As soon as it is guaranteed that during a request
87 * the same container is used, it no longer needs to be static.
89 * @var \Drupal\Core\Render\RenderContext[]
91 protected static $contextCollection;
94 * Constructs a new Renderer.
96 * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
97 * The controller resolver.
98 * @param \Drupal\Core\Theme\ThemeManagerInterface $theme
100 * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
102 * @param \Drupal\Core\Render\PlaceholderGeneratorInterface $placeholder_generator
103 * The placeholder generator.
104 * @param \Drupal\Core\Render\RenderCacheInterface $render_cache
105 * The render cache service.
106 * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
108 * @param array $renderer_config
109 * The renderer configuration array.
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 $this->controllerResolver = $controller_resolver;
113 $this->theme = $theme;
114 $this->elementInfo = $element_info;
115 $this->placeholderGenerator = $placeholder_generator;
116 $this->renderCache = $render_cache;
117 $this->rendererConfig = $renderer_config;
118 $this->requestStack = $request_stack;
120 // Initialize the context collection if needed.
121 if (!isset(static::$contextCollection)) {
122 static::$contextCollection = new \SplObjectStorage();
129 public function renderRoot(&$elements) {
130 // Disallow calling ::renderRoot() from within another ::renderRoot() call.
131 if ($this->isRenderingRoot) {
132 $this->isRenderingRoot = FALSE;
133 throw new \LogicException('A stray renderRoot() invocation is causing bubbling of attached assets to break.');
136 // Render in its own render context.
137 $this->isRenderingRoot = TRUE;
138 $output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
139 return $this->render($elements, TRUE);
141 $this->isRenderingRoot = FALSE;
149 public function renderPlain(&$elements) {
150 return $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
151 return $this->render($elements, TRUE);
158 public function renderPlaceholder($placeholder, array $elements) {
159 // Get the render array for the given placeholder
160 $placeholder_elements = $elements['#attached']['placeholders'][$placeholder];
162 // Prevent the render array from being auto-placeholdered again.
163 $placeholder_elements['#create_placeholder'] = FALSE;
165 // Render the placeholder into markup.
166 $markup = $this->renderPlain($placeholder_elements);
168 // Replace the placeholder with its rendered markup, and merge its
169 // bubbleable metadata with the main elements'.
170 $elements['#markup'] = Markup::create(str_replace($placeholder, $markup, $elements['#markup']));
171 $elements = $this->mergeBubbleableMetadata($elements, $placeholder_elements);
173 // Remove the placeholder that we've just rendered.
174 unset($elements['#attached']['placeholders'][$placeholder]);
182 public function render(&$elements, $is_root_call = FALSE) {
183 // Since #pre_render, #post_render, #lazy_builder callbacks and theme
184 // functions or templates may be used for generating a render array's
185 // content, and we might be rendering the main content for the page, it is
186 // possible that any of them throw an exception that will cause a different
187 // page to be rendered (e.g. throwing
188 // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause
189 // the 404 page to be rendered). That page might also use
190 // Renderer::renderRoot() but if exceptions aren't caught here, it will be
191 // impossible to call Renderer::renderRoot() again.
192 // Hence, catch all exceptions, reset the isRenderingRoot property and
193 // re-throw exceptions.
195 return $this->doRender($elements, $is_root_call);
197 catch (\Exception $e) {
198 // Mark the ::rootRender() call finished due to this exception & re-throw.
199 $this->isRenderingRoot = FALSE;
205 * See the docs for ::render().
207 protected function doRender(&$elements, $is_root_call = FALSE) {
208 if (empty($elements)) {
212 if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
213 if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
214 $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
216 $elements['#access'] = call_user_func($elements['#access_callback'], $elements);
219 // Early-return nothing if user does not have access.
220 if (isset($elements['#access'])) {
221 // If #access is an AccessResultInterface object, we must apply it's
222 // cacheability metadata to the render array.
223 if ($elements['#access'] instanceof AccessResultInterface) {
224 $this->addCacheableDependency($elements, $elements['#access']);
225 if (!$elements['#access']->isAllowed()) {
229 elseif ($elements['#access'] === FALSE) {
234 // Do not print elements twice.
235 if (!empty($elements['#printed'])) {
239 $context = $this->getCurrentRenderContext();
240 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.");
243 $context->push(new BubbleableMetadata());
245 // Set the bubbleable rendering metadata that has configurable defaults, if:
246 // - this is the root call, to ensure that the final render array definitely
247 // has these configurable defaults, even when no subtree is render cached.
248 // - this is a render cacheable subtree, to ensure that the cached data has
249 // the configurable defaults (which may affect the ID and invalidation).
250 if ($is_root_call || isset($elements['#cache']['keys'])) {
251 $required_cache_contexts = $this->rendererConfig['required_cache_contexts'];
252 if (isset($elements['#cache']['contexts'])) {
253 $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts);
256 $elements['#cache']['contexts'] = $required_cache_contexts;
260 // Try to fetch the prerendered element from cache, replace any placeholders
261 // and return the final markup.
262 if (isset($elements['#cache']['keys'])) {
263 $cached_element = $this->renderCache->get($elements);
264 if ($cached_element !== FALSE) {
265 $elements = $cached_element;
266 // Only when we're in a root (non-recursive) Renderer::render() call,
267 // placeholders must be processed, to prevent breaking the render cache
268 // in case of nested elements with #cache set.
270 $this->replacePlaceholders($elements);
272 // Mark the element markup as safe if is it a string.
273 if (is_string($elements['#markup'])) {
274 $elements['#markup'] = Markup::create($elements['#markup']);
276 // The render cache item contains all the bubbleable rendering metadata
278 $context->update($elements);
279 // Render cache hit, so rendering is finished, all necessary info
282 return $elements['#markup'];
285 // Two-tier caching: track pre-bubbling elements' #cache, #lazy_builder and
286 // #create_placeholder for later comparison.
287 // @see \Drupal\Core\Render\RenderCacheInterface::get()
288 // @see \Drupal\Core\Render\RenderCacheInterface::set()
289 $pre_bubbling_elements = array_intersect_key($elements, [
291 '#lazy_builder' => TRUE,
292 '#create_placeholder' => TRUE,
295 // If the default values for this element have not been loaded yet, populate
297 if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
298 $elements += $this->elementInfo->getInfo($elements['#type']);
301 // First validate the usage of #lazy_builder; both of the next if-statements
302 // use it if available.
303 if (isset($elements['#lazy_builder'])) {
304 // @todo Convert to assertions once https://www.drupal.org/node/2408013
306 if (!is_array($elements['#lazy_builder'])) {
307 throw new \DomainException('The #lazy_builder property must have an array as a value.');
309 if (count($elements['#lazy_builder']) !== 2) {
310 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.');
312 if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function($v) { return is_null($v) || is_scalar($v); }))) {
313 throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL.");
315 $children = Element::children($elements);
317 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)));
322 '#create_placeholder',
323 // The keys below are not actually supported, but these are added
324 // automatically by the Renderer. Adding them as though they are
325 // supported allows us to avoid throwing an exception 100% of the time.
329 $unsupported_keys = array_diff(array_keys($elements), $supported_keys);
330 if (count($unsupported_keys)) {
331 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 $elements['#create_placeholder'] = TRUE;
338 // If instructed to create a placeholder, and a #lazy_builder callback is
339 // present (without such a callback, it would be impossible to replace the
340 // placeholder), replace the current element with a placeholder.
341 // @todo remove the isMethodSafe() check when
342 // https://www.drupal.org/node/2367555 lands.
343 if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE && $this->requestStack->getCurrentRequest()->isMethodSafe()) {
344 if (!isset($elements['#lazy_builder'])) {
345 throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.');
347 $elements = $this->placeholderGenerator->createPlaceholder($elements);
349 // Build the element if it is still empty.
350 if (isset($elements['#lazy_builder'])) {
351 $callable = $elements['#lazy_builder'][0];
352 $args = $elements['#lazy_builder'][1];
353 if (is_string($callable) && strpos($callable, '::') === FALSE) {
354 $callable = $this->controllerResolver->getControllerFromDefinition($callable);
356 $new_elements = call_user_func_array($callable, $args);
357 // Retain the original cacheability metadata, plus cache keys.
358 CacheableMetadata::createFromRenderArray($elements)
359 ->merge(CacheableMetadata::createFromRenderArray($new_elements))
360 ->applyTo($new_elements);
361 if (isset($elements['#cache']['keys'])) {
362 $new_elements['#cache']['keys'] = $elements['#cache']['keys'];
364 $elements = $new_elements;
365 $elements['#lazy_builder_built'] = TRUE;
368 // Make any final changes to the element before it is rendered. This means
369 // that the $element or the children can be altered or corrected before the
370 // element is rendered into the final text.
371 if (isset($elements['#pre_render'])) {
372 foreach ($elements['#pre_render'] as $callable) {
373 if (is_string($callable) && strpos($callable, '::') === FALSE) {
374 $callable = $this->controllerResolver->getControllerFromDefinition($callable);
376 $elements = call_user_func($callable, $elements);
380 // All render elements support #markup and #plain_text.
381 if (!empty($elements['#markup']) || !empty($elements['#plain_text'])) {
382 $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'] : [];
390 // Allow #pre_render to abort rendering.
391 if (!empty($elements['#printed'])) {
392 // The #printed element contains all the bubbleable rendering metadata for
394 $context->update($elements);
395 // #printed, so rendering is finished, all necessary info collected!
400 // Add any JavaScript state information associated with the element.
401 if (!empty($elements['#states'])) {
402 drupal_process_states($elements);
405 // Get the children of the element, sorted by weight.
406 $children = Element::children($elements, TRUE);
408 // Initialize this element's #children, unless a #pre_render callback
409 // already preset #children.
410 if (!isset($elements['#children'])) {
411 $elements['#children'] = '';
414 // Assume that if #theme is set it represents an implemented hook.
415 $theme_is_implemented = isset($elements['#theme']);
416 // Check the elements for insecure HTML and pass through sanitization.
417 if (isset($elements)) {
423 foreach ($markup_keys as $key) {
424 if (!empty($elements[$key]) && is_scalar($elements[$key])) {
425 $elements[$key] = $this->xssFilterAdminIfUnsafe($elements[$key]);
430 // Call the element's #theme function if it is set. Then any children of the
431 // element have to be rendered there. If the internal #render_children
432 // property is set, do not call the #theme function to prevent infinite
434 if ($theme_is_implemented && !isset($elements['#render_children'])) {
435 $elements['#children'] = $this->theme->render($elements['#theme'], $elements);
437 // If ThemeManagerInterface::render() returns FALSE this means that the
438 // hook in #theme was not found in the registry and so we need to update
439 // our flag accordingly. This is common for theme suggestions.
440 $theme_is_implemented = ($elements['#children'] !== FALSE);
443 // If #theme is not implemented or #render_children is set and the element
444 // has an empty #children attribute, render the children now. This is the
445 // same process as Renderer::render() but is inlined for speed.
446 if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
447 foreach ($children as $key) {
448 $elements['#children'] .= $this->doRender($elements[$key]);
450 $elements['#children'] = Markup::create($elements['#children']);
453 // If #theme is not implemented and the element has raw #markup as a
454 // fallback, prepend the content in #markup to #children. In this case
455 // #children will contain whatever is provided by #pre_render prepended to
456 // what is rendered recursively above. If #theme is implemented then it is
457 // the responsibility of that theme implementation to render #markup if
458 // required. Eventually #theme_wrappers will expect both #markup and
459 // #children to be a single string as #children.
460 if (!$theme_is_implemented && isset($elements['#markup'])) {
461 $elements['#children'] = Markup::create($elements['#markup'] . $elements['#children']);
464 // Let the theme functions in #theme_wrappers add markup around the rendered
466 // #states and #attached have to be processed before #theme_wrappers,
467 // because the #type 'page' render array from drupal_prepare_page() would
468 // render the $page and wrap it into the html.html.twig template without the
469 // attached assets otherwise.
470 // If the internal #render_children property is set, do not call the
471 // #theme_wrappers function(s) to prevent infinite recursion.
472 if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
473 foreach ($elements['#theme_wrappers'] as $key => $value) {
474 // If the value of a #theme_wrappers item is an array then the theme
475 // hook is found in the key of the item and the value contains attribute
476 // overrides. Attribute overrides replace key/value pairs in $elements
477 // for only this ThemeManagerInterface::render() call. This allows
478 // #theme hooks and #theme_wrappers hooks to share variable names
479 // without conflict or ambiguity.
480 $wrapper_elements = $elements;
481 if (is_string($key)) {
482 $wrapper_hook = $key;
483 foreach ($value as $attribute => $override) {
484 $wrapper_elements[$attribute] = $override;
488 $wrapper_hook = $value;
491 $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements);
495 // Filter the outputted content and make any last changes before the content
496 // is sent to the browser. The changes are made on $content which allows the
497 // outputted text to be filtered.
498 if (isset($elements['#post_render'])) {
499 foreach ($elements['#post_render'] as $callable) {
500 if (is_string($callable) && strpos($callable, '::') === FALSE) {
501 $callable = $this->controllerResolver->getControllerFromDefinition($callable);
503 $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
507 // We store the resulting output in $elements['#markup'], to be consistent
508 // with how render cached output gets stored. This ensures that placeholder
509 // replacement logic gets the same data to work with, no matter if #cache is
510 // disabled, #cache is enabled, there is a cache hit or miss.
511 $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : '';
512 $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : '';
514 $elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix);
516 // We've rendered this element (and its subtree!), now update the context.
517 $context->update($elements);
519 // Cache the processed element if both $pre_bubbling_elements and $elements
520 // have the metadata necessary to generate a cache ID.
521 if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) {
522 if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) {
523 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 // Update the render context; the render cache implementation may update
527 // the element, and it may have different bubbleable metadata now.
528 // @see \Drupal\Core\Render\PlaceholderingRenderCache::set()
530 $context->push(new BubbleableMetadata());
531 $context->update($elements);
534 // Only when we're in a root (non-recursive) Renderer::render() call,
535 // placeholders must be processed, to prevent breaking the render cache in
536 // case of nested elements with #cache set.
538 // By running them here, we ensure that:
539 // - they run when #cache is disabled,
540 // - they run when #cache is enabled and there is a cache miss.
541 // Only the case of a cache hit when #cache is enabled, is not handled here,
542 // that is handled earlier in Renderer::render().
544 $this->replacePlaceholders($elements);
545 // @todo remove as part of https://www.drupal.org/node/2511330.
546 if ($context->count() !== 1) {
547 throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
551 // Rendering is finished, all necessary info collected!
554 $elements['#printed'] = TRUE;
555 return $elements['#markup'];
561 public function hasRenderContext() {
562 return (bool) $this->getCurrentRenderContext();
568 public function executeInRenderContext(RenderContext $context, callable $callable) {
569 // Store the current render context.
570 $previous_context = $this->getCurrentRenderContext();
572 // Set the provided context and call the callable, it will use that context.
573 $this->setCurrentRenderContext($context);
574 $result = $callable();
575 // @todo Convert to an assertion in https://www.drupal.org/node/2408013
576 if ($context->count() > 1) {
577 throw new \LogicException('Bubbling failed.');
580 // Restore the original render context.
581 $this->setCurrentRenderContext($previous_context);
587 * Returns the current render context.
589 * @return \Drupal\Core\Render\RenderContext
590 * The current render context.
592 protected function getCurrentRenderContext() {
593 $request = $this->requestStack->getCurrentRequest();
594 return isset(static::$contextCollection[$request]) ? static::$contextCollection[$request] : NULL;
598 * Sets the current render context.
600 * @param \Drupal\Core\Render\RenderContext|null $context
601 * The render context. This can be NULL for instance when restoring the
602 * original render context, which is in fact NULL.
606 protected function setCurrentRenderContext(RenderContext $context = NULL) {
607 $request = $this->requestStack->getCurrentRequest();
608 static::$contextCollection[$request] = $context;
613 * Replaces placeholders.
615 * Placeholders may have:
616 * - #lazy_builder callback, to build a render array to be rendered into
617 * markup that can replace the placeholder
618 * - #cache: to cache the result of the placeholder
620 * Also merges the bubbleable metadata resulting from the rendering of the
621 * contents of the placeholders. Hence $elements will be contain the entirety
622 * of bubbleable metadata.
624 * @param array &$elements
625 * The structured array describing the data being rendered. Including the
626 * bubbleable metadata associated with the markup that replaced the
630 * Whether placeholders were replaced.
632 * @see \Drupal\Core\Render\Renderer::renderPlaceholder()
634 protected function replacePlaceholders(array &$elements) {
635 if (!isset($elements['#attached']['placeholders']) || empty($elements['#attached']['placeholders'])) {
639 // The 'status messages' placeholder needs to be special cased, because it
640 // depends on global state that can be modified when other placeholders are
641 // being rendered: any code can add messages to render.
642 // This violates the principle that each lazy builder must be able to render
643 // itself in isolation, and therefore in any order. However, we cannot
644 // change the way drupal_set_message() works in the Drupal 8 cycle. So we
645 // have to accommodate its special needs.
646 // Allowing placeholders to be rendered in a particular order (in this case:
647 // last) would violate this isolation principle. Thus a monopoly is granted
648 // to this one special case, with this hard-coded solution.
649 // @see \Drupal\Core\Render\Element\StatusMessages
650 // @see https://www.drupal.org/node/2712935#comment-11368923
652 // First render all placeholders except 'status messages' placeholders.
653 $message_placeholders = [];
654 foreach ($elements['#attached']['placeholders'] as $placeholder => $placeholder_element) {
655 if (isset($placeholder_element['#lazy_builder']) && $placeholder_element['#lazy_builder'][0] === 'Drupal\Core\Render\Element\StatusMessages::renderMessages') {
656 $message_placeholders[] = $placeholder;
659 $elements = $this->renderPlaceholder($placeholder, $elements);
663 // Then render 'status messages' placeholders.
664 foreach ($message_placeholders as $message_placeholder) {
665 $elements = $this->renderPlaceholder($message_placeholder, $elements);
674 public function mergeBubbleableMetadata(array $a, array $b) {
675 $meta_a = BubbleableMetadata::createFromRenderArray($a);
676 $meta_b = BubbleableMetadata::createFromRenderArray($b);
677 $meta_a->merge($meta_b)->applyTo($a);
684 public function addCacheableDependency(array &$elements, $dependency) {
685 $meta_a = CacheableMetadata::createFromRenderArray($elements);
686 $meta_b = CacheableMetadata::createFromObject($dependency);
687 $meta_a->merge($meta_b)->applyTo($elements);
691 * Applies a very permissive XSS/HTML filter for admin-only use.
693 * Note: This method only filters if $string is not marked safe already. This
694 * ensures that HTML intended for display is not filtered.
696 * @param string|\Drupal\Core\Render\Markup $string
699 * @return \Drupal\Core\Render\Markup
700 * The escaped string wrapped in a Markup object. If the string is an
701 * instance of \Drupal\Component\Render\MarkupInterface, it won't be escaped
704 protected function xssFilterAdminIfUnsafe($string) {
705 if (!($string instanceof MarkupInterface)) {
706 $string = Xss::filterAdmin($string);
708 return Markup::create($string);
712 * Escapes #plain_text or filters #markup as required.
714 * Drupal uses Twig's auto-escape feature to improve security. This feature
715 * automatically escapes any HTML that is not known to be safe. Due to this
716 * the render system needs to ensure that all markup it generates is marked
717 * safe so that Twig does not do any additional escaping.
719 * By default all #markup is filtered to protect against XSS using the admin
720 * tag list. Render arrays can alter the list of tags allowed by the filter
721 * using the #allowed_tags property. This value should be an array of tags
722 * that Xss::filter() would accept. Render arrays can escape text instead
723 * of XSS filtering by setting the #plain_text property instead of #markup. If
724 * #plain_text is used #allowed_tags is ignored.
726 * @param array $elements
727 * A render array with #markup set.
729 * @return \Drupal\Component\Render\MarkupInterface|string
730 * The escaped markup wrapped in a Markup object. If $elements['#markup']
731 * is an instance of \Drupal\Component\Render\MarkupInterface, it won't be
732 * escaped or filtered again.
734 * @see \Drupal\Component\Utility\Html::escape()
735 * @see \Drupal\Component\Utility\Xss::filter()
736 * @see \Drupal\Component\Utility\Xss::filterAdmin()
738 protected function ensureMarkupIsSafe(array $elements) {
739 if (empty($elements['#markup']) && empty($elements['#plain_text'])) {
743 if (!empty($elements['#plain_text'])) {
744 $elements['#markup'] = Markup::create(Html::escape($elements['#plain_text']));
746 elseif (!($elements['#markup'] instanceof MarkupInterface)) {
747 // The default behaviour is to XSS filter using the admin tag list.
748 $tags = isset($elements['#allowed_tags']) ? $elements['#allowed_tags'] : Xss::getAdminTagList();
749 $elements['#markup'] = Markup::create(Xss::filter($elements['#markup'], $tags));