00246943f5471105e4d67b4456b21d3ce9422332
[yaffs-website] / web / core / lib / Drupal / Core / Render / Renderer.php
1 <?php
2
3 namespace Drupal\Core\Render;
4
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;
14
15 /**
16  * Turns a render array into a HTML string.
17  */
18 class Renderer implements RendererInterface {
19
20   /**
21    * The theme manager.
22    *
23    * @var \Drupal\Core\Theme\ThemeManagerInterface
24    */
25   protected $theme;
26
27   /**
28    * The controller resolver.
29    *
30    * @var \Drupal\Core\Controller\ControllerResolverInterface
31    */
32   protected $controllerResolver;
33
34   /**
35    * The element info.
36    *
37    * @var \Drupal\Core\Render\ElementInfoManagerInterface
38    */
39   protected $elementInfo;
40
41   /**
42    * The placeholder generator.
43    *
44    * @var \Drupal\Core\Render\PlaceholderGeneratorInterface
45    */
46   protected $placeholderGenerator;
47
48   /**
49    * The render cache service.
50    *
51    * @var \Drupal\Core\Render\RenderCacheInterface
52    */
53   protected $renderCache;
54
55   /**
56    * The renderer configuration array.
57    *
58    * @var array
59    */
60   protected $rendererConfig;
61
62   /**
63    * Whether we're currently in a ::renderRoot() call.
64    *
65    * @var bool
66    */
67   protected $isRenderingRoot = FALSE;
68
69   /**
70    * The request stack.
71    *
72    * @var \Symfony\Component\HttpFoundation\RequestStack
73    */
74   protected $requestStack;
75
76   /**
77    * The render context collection.
78    *
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.
82    *
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.
88    *
89    * @var \Drupal\Core\Render\RenderContext[]
90    */
91   protected static $contextCollection;
92
93   /**
94    * Constructs a new Renderer.
95    *
96    * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
97    *   The controller resolver.
98    * @param \Drupal\Core\Theme\ThemeManagerInterface $theme
99    *   The theme manager.
100    * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info
101    *   The 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
107    *   The request stack.
108    * @param array $renderer_config
109    *   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     $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;
119
120     // Initialize the context collection if needed.
121     if (!isset(static::$contextCollection)) {
122       static::$contextCollection = new \SplObjectStorage();
123     }
124   }
125
126   /**
127    * {@inheritdoc}
128    */
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.');
134     }
135
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);
140     });
141     $this->isRenderingRoot = FALSE;
142
143     return $output;
144   }
145
146   /**
147    * {@inheritdoc}
148    */
149   public function renderPlain(&$elements) {
150     return $this->executeInRenderContext(new RenderContext(), function () use (&$elements) {
151       return $this->render($elements, TRUE);
152     });
153   }
154
155   /**
156    * {@inheritdoc}
157    */
158   public function renderPlaceholder($placeholder, array $elements) {
159     // Get the render array for the given placeholder
160     $placeholder_elements = $elements['#attached']['placeholders'][$placeholder];
161
162     // Prevent the render array from being auto-placeholdered again.
163     $placeholder_elements['#create_placeholder'] = FALSE;
164
165     // Render the placeholder into markup.
166     $markup = $this->renderPlain($placeholder_elements);
167
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);
172
173     // Remove the placeholder that we've just rendered.
174     unset($elements['#attached']['placeholders'][$placeholder]);
175
176     return $elements;
177   }
178
179   /**
180    * {@inheritdoc}
181    */
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.
194     try {
195       return $this->doRender($elements, $is_root_call);
196     }
197     catch (\Exception $e) {
198       // Mark the ::rootRender() call finished due to this exception & re-throw.
199       $this->isRenderingRoot = FALSE;
200       throw $e;
201     }
202   }
203
204   /**
205    * See the docs for ::render().
206    */
207   protected function doRender(&$elements, $is_root_call = FALSE) {
208     if (empty($elements)) {
209       return '';
210     }
211
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']);
215       }
216       $elements['#access'] = call_user_func($elements['#access_callback'], $elements);
217     }
218
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()) {
226           return '';
227         }
228       }
229       elseif ($elements['#access'] === FALSE) {
230         return '';
231       }
232     }
233
234     // Do not print elements twice.
235     if (!empty($elements['#printed'])) {
236       return '';
237     }
238
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.");
242     }
243     $context->push(new BubbleableMetadata());
244
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);
254       }
255       else {
256         $elements['#cache']['contexts'] = $required_cache_contexts;
257       }
258     }
259
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.
269         if ($is_root_call) {
270           $this->replacePlaceholders($elements);
271         }
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']);
275         }
276         // The render cache item contains all the bubbleable rendering metadata
277         // for the subtree.
278         $context->update($elements);
279         // Render cache hit, so rendering is finished, all necessary info
280         // collected!
281         $context->bubble();
282         return $elements['#markup'];
283       }
284     }
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, [
290       '#cache' => TRUE,
291       '#lazy_builder' => TRUE,
292       '#create_placeholder' => TRUE,
293     ]);
294
295     // If the default values for this element have not been loaded yet, populate
296     // them.
297     if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) {
298       $elements += $this->elementInfo->getInfo($elements['#type']);
299     }
300
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
305       //   lands.
306       if (!is_array($elements['#lazy_builder'])) {
307         throw new \DomainException('The #lazy_builder property must have an array as a value.');
308       }
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.');
311       }
312       if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function ($v) {
313         return is_null($v) || is_scalar($v);
314       }))) {
315         throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL.");
316       }
317       $children = Element::children($elements);
318       if ($children) {
319         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)));
320       }
321       $supported_keys = [
322         '#lazy_builder',
323         '#cache',
324         '#create_placeholder',
325         // The keys below are not actually supported, but these are added
326         // automatically by the Renderer. Adding them as though they are
327         // supported allows us to avoid throwing an exception 100% of the time.
328         '#weight',
329         '#printed'
330       ];
331       $unsupported_keys = array_diff(array_keys($elements), $supported_keys);
332       if (count($unsupported_keys)) {
333         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       }
335     }
336     // Determine whether to do auto-placeholdering.
337     if ($this->placeholderGenerator->canCreatePlaceholder($elements) && $this->placeholderGenerator->shouldAutomaticallyPlaceholder($elements)) {
338       $elements['#create_placeholder'] = TRUE;
339     }
340     // If instructed to create a placeholder, and a #lazy_builder callback is
341     // present (without such a callback, it would be impossible to replace the
342     // placeholder), replace the current element with a placeholder.
343     // @todo remove the isMethodCacheable() check when
344     //       https://www.drupal.org/node/2367555 lands.
345     if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE && $this->requestStack->getCurrentRequest()->isMethodCacheable()) {
346       if (!isset($elements['#lazy_builder'])) {
347         throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.');
348       }
349       $elements = $this->placeholderGenerator->createPlaceholder($elements);
350     }
351     // Build the element if it is still empty.
352     if (isset($elements['#lazy_builder'])) {
353       $callable = $elements['#lazy_builder'][0];
354       $args = $elements['#lazy_builder'][1];
355       if (is_string($callable) && strpos($callable, '::') === FALSE) {
356         $callable = $this->controllerResolver->getControllerFromDefinition($callable);
357       }
358       $new_elements = call_user_func_array($callable, $args);
359       // Retain the original cacheability metadata, plus cache keys.
360       CacheableMetadata::createFromRenderArray($elements)
361         ->merge(CacheableMetadata::createFromRenderArray($new_elements))
362         ->applyTo($new_elements);
363       if (isset($elements['#cache']['keys'])) {
364         $new_elements['#cache']['keys'] = $elements['#cache']['keys'];
365       }
366       $elements = $new_elements;
367       $elements['#lazy_builder_built'] = TRUE;
368     }
369
370     // Make any final changes to the element before it is rendered. This means
371     // that the $element or the children can be altered or corrected before the
372     // element is rendered into the final text.
373     if (isset($elements['#pre_render'])) {
374       foreach ($elements['#pre_render'] as $callable) {
375         if (is_string($callable) && strpos($callable, '::') === FALSE) {
376           $callable = $this->controllerResolver->getControllerFromDefinition($callable);
377         }
378         $elements = call_user_func($callable, $elements);
379       }
380     }
381
382     // All render elements support #markup and #plain_text.
383     if (!empty($elements['#markup']) || !empty($elements['#plain_text'])) {
384       $elements = $this->ensureMarkupIsSafe($elements);
385     }
386
387     // Defaults for bubbleable rendering metadata.
388     $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : [];
389     $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT;
390     $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : [];
391
392     // Allow #pre_render to abort rendering.
393     if (!empty($elements['#printed'])) {
394       // The #printed element contains all the bubbleable rendering metadata for
395       // the subtree.
396       $context->update($elements);
397       // #printed, so rendering is finished, all necessary info collected!
398       $context->bubble();
399       return '';
400     }
401
402     // Add any JavaScript state information associated with the element.
403     if (!empty($elements['#states'])) {
404       drupal_process_states($elements);
405     }
406
407     // Get the children of the element, sorted by weight.
408     $children = Element::children($elements, TRUE);
409
410     // Initialize this element's #children, unless a #pre_render callback
411     // already preset #children.
412     if (!isset($elements['#children'])) {
413       $elements['#children'] = '';
414     }
415
416     // Assume that if #theme is set it represents an implemented hook.
417     $theme_is_implemented = isset($elements['#theme']);
418     // Check the elements for insecure HTML and pass through sanitization.
419     if (isset($elements)) {
420       $markup_keys = [
421         '#description',
422         '#field_prefix',
423         '#field_suffix',
424       ];
425       foreach ($markup_keys as $key) {
426         if (!empty($elements[$key]) && is_scalar($elements[$key])) {
427           $elements[$key] = $this->xssFilterAdminIfUnsafe($elements[$key]);
428         }
429       }
430     }
431
432     // Call the element's #theme function if it is set. Then any children of the
433     // element have to be rendered there. If the internal #render_children
434     // property is set, do not call the #theme function to prevent infinite
435     // recursion.
436     if ($theme_is_implemented && !isset($elements['#render_children'])) {
437       $elements['#children'] = $this->theme->render($elements['#theme'], $elements);
438
439       // If ThemeManagerInterface::render() returns FALSE this means that the
440       // hook in #theme was not found in the registry and so we need to update
441       // our flag accordingly. This is common for theme suggestions.
442       $theme_is_implemented = ($elements['#children'] !== FALSE);
443     }
444
445     // If #theme is not implemented or #render_children is set and the element
446     // has an empty #children attribute, render the children now. This is the
447     // same process as Renderer::render() but is inlined for speed.
448     if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) {
449       foreach ($children as $key) {
450         $elements['#children'] .= $this->doRender($elements[$key]);
451       }
452       $elements['#children'] = Markup::create($elements['#children']);
453     }
454
455     // If #theme is not implemented and the element has raw #markup as a
456     // fallback, prepend the content in #markup to #children. In this case
457     // #children will contain whatever is provided by #pre_render prepended to
458     // what is rendered recursively above. If #theme is implemented then it is
459     // the responsibility of that theme implementation to render #markup if
460     // required. Eventually #theme_wrappers will expect both #markup and
461     // #children to be a single string as #children.
462     if (!$theme_is_implemented && isset($elements['#markup'])) {
463       $elements['#children'] = Markup::create($elements['#markup'] . $elements['#children']);
464     }
465
466     // Let the theme functions in #theme_wrappers add markup around the rendered
467     // children.
468     // #states and #attached have to be processed before #theme_wrappers,
469     // because the #type 'page' render array from drupal_prepare_page() would
470     // render the $page and wrap it into the html.html.twig template without the
471     // attached assets otherwise.
472     // If the internal #render_children property is set, do not call the
473     // #theme_wrappers function(s) to prevent infinite recursion.
474     if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) {
475       foreach ($elements['#theme_wrappers'] as $key => $value) {
476         // If the value of a #theme_wrappers item is an array then the theme
477         // hook is found in the key of the item and the value contains attribute
478         // overrides. Attribute overrides replace key/value pairs in $elements
479         // for only this ThemeManagerInterface::render() call. This allows
480         // #theme hooks and #theme_wrappers hooks to share variable names
481         // without conflict or ambiguity.
482         $wrapper_elements = $elements;
483         if (is_string($key)) {
484           $wrapper_hook = $key;
485           foreach ($value as $attribute => $override) {
486             $wrapper_elements[$attribute] = $override;
487           }
488         }
489         else {
490           $wrapper_hook = $value;
491         }
492
493         $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements);
494       }
495     }
496
497     // Filter the outputted content and make any last changes before the content
498     // is sent to the browser. The changes are made on $content which allows the
499     // outputted text to be filtered.
500     if (isset($elements['#post_render'])) {
501       foreach ($elements['#post_render'] as $callable) {
502         if (is_string($callable) && strpos($callable, '::') === FALSE) {
503           $callable = $this->controllerResolver->getControllerFromDefinition($callable);
504         }
505         $elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
506       }
507     }
508
509     // We store the resulting output in $elements['#markup'], to be consistent
510     // with how render cached output gets stored. This ensures that placeholder
511     // replacement logic gets the same data to work with, no matter if #cache is
512     // disabled, #cache is enabled, there is a cache hit or miss. If
513     // #render_children is set the #prefix and #suffix will have already been
514     // added.
515     if (isset($elements['#render_children'])) {
516       $elements['#markup'] = Markup::create($elements['#children']);
517     }
518     else {
519       $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : '';
520       $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : '';
521       $elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix);
522     }
523
524     // We've rendered this element (and its subtree!), now update the context.
525     $context->update($elements);
526
527     // Cache the processed element if both $pre_bubbling_elements and $elements
528     // have the metadata necessary to generate a cache ID.
529     if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) {
530       if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) {
531         throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.');
532       }
533       $this->renderCache->set($elements, $pre_bubbling_elements);
534       // Update the render context; the render cache implementation may update
535       // the element, and it may have different bubbleable metadata now.
536       // @see \Drupal\Core\Render\PlaceholderingRenderCache::set()
537       $context->pop();
538       $context->push(new BubbleableMetadata());
539       $context->update($elements);
540     }
541
542     // Only when we're in a root (non-recursive) Renderer::render() call,
543     // placeholders must be processed, to prevent breaking the render cache in
544     // case of nested elements with #cache set.
545     //
546     // By running them here, we ensure that:
547     // - they run when #cache is disabled,
548     // - they run when #cache is enabled and there is a cache miss.
549     // Only the case of a cache hit when #cache is enabled, is not handled here,
550     // that is handled earlier in Renderer::render().
551     if ($is_root_call) {
552       $this->replacePlaceholders($elements);
553       // @todo remove as part of https://www.drupal.org/node/2511330.
554       if ($context->count() !== 1) {
555         throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.');
556       }
557     }
558
559     // Rendering is finished, all necessary info collected!
560     $context->bubble();
561
562     $elements['#printed'] = TRUE;
563     return $elements['#markup'];
564   }
565
566   /**
567    * {@inheritdoc}
568    */
569   public function hasRenderContext() {
570     return (bool) $this->getCurrentRenderContext();
571   }
572
573   /**
574    * {@inheritdoc}
575    */
576   public function executeInRenderContext(RenderContext $context, callable $callable) {
577     // Store the current render context.
578     $previous_context = $this->getCurrentRenderContext();
579
580     // Set the provided context and call the callable, it will use that context.
581     $this->setCurrentRenderContext($context);
582     $result = $callable();
583     // @todo Convert to an assertion in https://www.drupal.org/node/2408013
584     if ($context->count() > 1) {
585       throw new \LogicException('Bubbling failed.');
586     }
587
588     // Restore the original render context.
589     $this->setCurrentRenderContext($previous_context);
590
591     return $result;
592   }
593
594   /**
595    * Returns the current render context.
596    *
597    * @return \Drupal\Core\Render\RenderContext
598    *   The current render context.
599    */
600   protected function getCurrentRenderContext() {
601     $request = $this->requestStack->getCurrentRequest();
602     return isset(static::$contextCollection[$request]) ? static::$contextCollection[$request] : NULL;
603   }
604
605   /**
606    * Sets the current render context.
607    *
608    * @param \Drupal\Core\Render\RenderContext|null $context
609    *   The render context. This can be NULL for instance when restoring the
610    *   original render context, which is in fact NULL.
611    *
612    * @return $this
613    */
614   protected function setCurrentRenderContext(RenderContext $context = NULL) {
615     $request = $this->requestStack->getCurrentRequest();
616     static::$contextCollection[$request] = $context;
617     return $this;
618   }
619
620   /**
621    * Replaces placeholders.
622    *
623    * Placeholders may have:
624    * - #lazy_builder callback, to build a render array to be rendered into
625    *   markup that can replace the placeholder
626    * - #cache: to cache the result of the placeholder
627    *
628    * Also merges the bubbleable metadata resulting from the rendering of the
629    * contents of the placeholders. Hence $elements will be contain the entirety
630    * of bubbleable metadata.
631    *
632    * @param array &$elements
633    *   The structured array describing the data being rendered. Including the
634    *   bubbleable metadata associated with the markup that replaced the
635    *   placeholders.
636    *
637    * @returns bool
638    *   Whether placeholders were replaced.
639    *
640    * @see \Drupal\Core\Render\Renderer::renderPlaceholder()
641    */
642   protected function replacePlaceholders(array &$elements) {
643     if (!isset($elements['#attached']['placeholders']) || empty($elements['#attached']['placeholders'])) {
644       return FALSE;
645     }
646
647     // The 'status messages' placeholder needs to be special cased, because it
648     // depends on global state that can be modified when other placeholders are
649     // being rendered: any code can add messages to render.
650     // This violates the principle that each lazy builder must be able to render
651     // itself in isolation, and therefore in any order. However, we cannot
652     // change the way drupal_set_message() works in the Drupal 8 cycle. So we
653     // have to accommodate its special needs.
654     // Allowing placeholders to be rendered in a particular order (in this case:
655     // last) would violate this isolation principle. Thus a monopoly is granted
656     // to this one special case, with this hard-coded solution.
657     // @see \Drupal\Core\Render\Element\StatusMessages
658     // @see https://www.drupal.org/node/2712935#comment-11368923
659
660     // First render all placeholders except 'status messages' placeholders.
661     $message_placeholders = [];
662     foreach ($elements['#attached']['placeholders'] as $placeholder => $placeholder_element) {
663       if (isset($placeholder_element['#lazy_builder']) && $placeholder_element['#lazy_builder'][0] === 'Drupal\Core\Render\Element\StatusMessages::renderMessages') {
664         $message_placeholders[] = $placeholder;
665       }
666       else {
667         $elements = $this->renderPlaceholder($placeholder, $elements);
668       }
669     }
670
671     // Then render 'status messages' placeholders.
672     foreach ($message_placeholders as $message_placeholder) {
673       $elements = $this->renderPlaceholder($message_placeholder, $elements);
674     }
675
676     return TRUE;
677   }
678
679   /**
680    * {@inheritdoc}
681    */
682   public function mergeBubbleableMetadata(array $a, array $b) {
683     $meta_a = BubbleableMetadata::createFromRenderArray($a);
684     $meta_b = BubbleableMetadata::createFromRenderArray($b);
685     $meta_a->merge($meta_b)->applyTo($a);
686     return $a;
687   }
688
689   /**
690    * {@inheritdoc}
691    */
692   public function addCacheableDependency(array &$elements, $dependency) {
693     $meta_a = CacheableMetadata::createFromRenderArray($elements);
694     $meta_b = CacheableMetadata::createFromObject($dependency);
695     $meta_a->merge($meta_b)->applyTo($elements);
696   }
697
698   /**
699    * Applies a very permissive XSS/HTML filter for admin-only use.
700    *
701    * Note: This method only filters if $string is not marked safe already. This
702    * ensures that HTML intended for display is not filtered.
703    *
704    * @param string|\Drupal\Core\Render\Markup $string
705    *   A string.
706    *
707    * @return \Drupal\Core\Render\Markup
708    *   The escaped string wrapped in a Markup object. If the string is an
709    *   instance of \Drupal\Component\Render\MarkupInterface, it won't be escaped
710    *   again.
711    */
712   protected function xssFilterAdminIfUnsafe($string) {
713     if (!($string instanceof MarkupInterface)) {
714       $string = Xss::filterAdmin($string);
715     }
716     return Markup::create($string);
717   }
718
719   /**
720    * Escapes #plain_text or filters #markup as required.
721    *
722    * Drupal uses Twig's auto-escape feature to improve security. This feature
723    * automatically escapes any HTML that is not known to be safe. Due to this
724    * the render system needs to ensure that all markup it generates is marked
725    * safe so that Twig does not do any additional escaping.
726    *
727    * By default all #markup is filtered to protect against XSS using the admin
728    * tag list. Render arrays can alter the list of tags allowed by the filter
729    * using the #allowed_tags property. This value should be an array of tags
730    * that Xss::filter() would accept. Render arrays can escape text instead
731    * of XSS filtering by setting the #plain_text property instead of #markup. If
732    * #plain_text is used #allowed_tags is ignored.
733    *
734    * @param array $elements
735    *   A render array with #markup set.
736    *
737    * @return \Drupal\Component\Render\MarkupInterface|string
738    *   The escaped markup wrapped in a Markup object. If $elements['#markup']
739    *   is an instance of \Drupal\Component\Render\MarkupInterface, it won't be
740    *   escaped or filtered again.
741    *
742    * @see \Drupal\Component\Utility\Html::escape()
743    * @see \Drupal\Component\Utility\Xss::filter()
744    * @see \Drupal\Component\Utility\Xss::filterAdmin()
745    */
746   protected function ensureMarkupIsSafe(array $elements) {
747     if (empty($elements['#markup']) && empty($elements['#plain_text'])) {
748       return $elements;
749     }
750
751     if (!empty($elements['#plain_text'])) {
752       $elements['#markup'] = Markup::create(Html::escape($elements['#plain_text']));
753     }
754     elseif (!($elements['#markup'] instanceof MarkupInterface)) {
755       // The default behaviour is to XSS filter using the admin tag list.
756       $tags = isset($elements['#allowed_tags']) ? $elements['#allowed_tags'] : Xss::getAdminTagList();
757       $elements['#markup'] = Markup::create(Xss::filter($elements['#markup'], $tags));
758     }
759
760     return $elements;
761   }
762
763 }