Mercurial > hg > isophonics-drupal-site
comparison core/lib/Drupal/Core/Render/Renderer.php @ 0:4c8ae668cc8c
Initial import (non-working)
author | Chris Cannam |
---|---|
date | Wed, 29 Nov 2017 16:09:58 +0000 |
parents | |
children | 7a779792577d |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:4c8ae668cc8c |
---|---|
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. | |
513 $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : ''; | |
514 $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : ''; | |
515 | |
516 $elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix); | |
517 | |
518 // We've rendered this element (and its subtree!), now update the context. | |
519 $context->update($elements); | |
520 | |
521 // Cache the processed element if both $pre_bubbling_elements and $elements | |
522 // have the metadata necessary to generate a cache ID. | |
523 if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) { | |
524 if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) { | |
525 throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.'); | |
526 } | |
527 $this->renderCache->set($elements, $pre_bubbling_elements); | |
528 // Update the render context; the render cache implementation may update | |
529 // the element, and it may have different bubbleable metadata now. | |
530 // @see \Drupal\Core\Render\PlaceholderingRenderCache::set() | |
531 $context->pop(); | |
532 $context->push(new BubbleableMetadata()); | |
533 $context->update($elements); | |
534 } | |
535 | |
536 // Only when we're in a root (non-recursive) Renderer::render() call, | |
537 // placeholders must be processed, to prevent breaking the render cache in | |
538 // case of nested elements with #cache set. | |
539 // | |
540 // By running them here, we ensure that: | |
541 // - they run when #cache is disabled, | |
542 // - they run when #cache is enabled and there is a cache miss. | |
543 // Only the case of a cache hit when #cache is enabled, is not handled here, | |
544 // that is handled earlier in Renderer::render(). | |
545 if ($is_root_call) { | |
546 $this->replacePlaceholders($elements); | |
547 // @todo remove as part of https://www.drupal.org/node/2511330. | |
548 if ($context->count() !== 1) { | |
549 throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); | |
550 } | |
551 } | |
552 | |
553 // Rendering is finished, all necessary info collected! | |
554 $context->bubble(); | |
555 | |
556 $elements['#printed'] = TRUE; | |
557 return $elements['#markup']; | |
558 } | |
559 | |
560 /** | |
561 * {@inheritdoc} | |
562 */ | |
563 public function hasRenderContext() { | |
564 return (bool) $this->getCurrentRenderContext(); | |
565 } | |
566 | |
567 /** | |
568 * {@inheritdoc} | |
569 */ | |
570 public function executeInRenderContext(RenderContext $context, callable $callable) { | |
571 // Store the current render context. | |
572 $previous_context = $this->getCurrentRenderContext(); | |
573 | |
574 // Set the provided context and call the callable, it will use that context. | |
575 $this->setCurrentRenderContext($context); | |
576 $result = $callable(); | |
577 // @todo Convert to an assertion in https://www.drupal.org/node/2408013 | |
578 if ($context->count() > 1) { | |
579 throw new \LogicException('Bubbling failed.'); | |
580 } | |
581 | |
582 // Restore the original render context. | |
583 $this->setCurrentRenderContext($previous_context); | |
584 | |
585 return $result; | |
586 } | |
587 | |
588 /** | |
589 * Returns the current render context. | |
590 * | |
591 * @return \Drupal\Core\Render\RenderContext | |
592 * The current render context. | |
593 */ | |
594 protected function getCurrentRenderContext() { | |
595 $request = $this->requestStack->getCurrentRequest(); | |
596 return isset(static::$contextCollection[$request]) ? static::$contextCollection[$request] : NULL; | |
597 } | |
598 | |
599 /** | |
600 * Sets the current render context. | |
601 * | |
602 * @param \Drupal\Core\Render\RenderContext|null $context | |
603 * The render context. This can be NULL for instance when restoring the | |
604 * original render context, which is in fact NULL. | |
605 * | |
606 * @return $this | |
607 */ | |
608 protected function setCurrentRenderContext(RenderContext $context = NULL) { | |
609 $request = $this->requestStack->getCurrentRequest(); | |
610 static::$contextCollection[$request] = $context; | |
611 return $this; | |
612 } | |
613 | |
614 /** | |
615 * Replaces placeholders. | |
616 * | |
617 * Placeholders may have: | |
618 * - #lazy_builder callback, to build a render array to be rendered into | |
619 * markup that can replace the placeholder | |
620 * - #cache: to cache the result of the placeholder | |
621 * | |
622 * Also merges the bubbleable metadata resulting from the rendering of the | |
623 * contents of the placeholders. Hence $elements will be contain the entirety | |
624 * of bubbleable metadata. | |
625 * | |
626 * @param array &$elements | |
627 * The structured array describing the data being rendered. Including the | |
628 * bubbleable metadata associated with the markup that replaced the | |
629 * placeholders. | |
630 * | |
631 * @returns bool | |
632 * Whether placeholders were replaced. | |
633 * | |
634 * @see \Drupal\Core\Render\Renderer::renderPlaceholder() | |
635 */ | |
636 protected function replacePlaceholders(array &$elements) { | |
637 if (!isset($elements['#attached']['placeholders']) || empty($elements['#attached']['placeholders'])) { | |
638 return FALSE; | |
639 } | |
640 | |
641 // The 'status messages' placeholder needs to be special cased, because it | |
642 // depends on global state that can be modified when other placeholders are | |
643 // being rendered: any code can add messages to render. | |
644 // This violates the principle that each lazy builder must be able to render | |
645 // itself in isolation, and therefore in any order. However, we cannot | |
646 // change the way drupal_set_message() works in the Drupal 8 cycle. So we | |
647 // have to accommodate its special needs. | |
648 // Allowing placeholders to be rendered in a particular order (in this case: | |
649 // last) would violate this isolation principle. Thus a monopoly is granted | |
650 // to this one special case, with this hard-coded solution. | |
651 // @see \Drupal\Core\Render\Element\StatusMessages | |
652 // @see https://www.drupal.org/node/2712935#comment-11368923 | |
653 | |
654 // First render all placeholders except 'status messages' placeholders. | |
655 $message_placeholders = []; | |
656 foreach ($elements['#attached']['placeholders'] as $placeholder => $placeholder_element) { | |
657 if (isset($placeholder_element['#lazy_builder']) && $placeholder_element['#lazy_builder'][0] === 'Drupal\Core\Render\Element\StatusMessages::renderMessages') { | |
658 $message_placeholders[] = $placeholder; | |
659 } | |
660 else { | |
661 $elements = $this->renderPlaceholder($placeholder, $elements); | |
662 } | |
663 } | |
664 | |
665 // Then render 'status messages' placeholders. | |
666 foreach ($message_placeholders as $message_placeholder) { | |
667 $elements = $this->renderPlaceholder($message_placeholder, $elements); | |
668 } | |
669 | |
670 return TRUE; | |
671 } | |
672 | |
673 /** | |
674 * {@inheritdoc} | |
675 */ | |
676 public function mergeBubbleableMetadata(array $a, array $b) { | |
677 $meta_a = BubbleableMetadata::createFromRenderArray($a); | |
678 $meta_b = BubbleableMetadata::createFromRenderArray($b); | |
679 $meta_a->merge($meta_b)->applyTo($a); | |
680 return $a; | |
681 } | |
682 | |
683 /** | |
684 * {@inheritdoc} | |
685 */ | |
686 public function addCacheableDependency(array &$elements, $dependency) { | |
687 $meta_a = CacheableMetadata::createFromRenderArray($elements); | |
688 $meta_b = CacheableMetadata::createFromObject($dependency); | |
689 $meta_a->merge($meta_b)->applyTo($elements); | |
690 } | |
691 | |
692 /** | |
693 * Applies a very permissive XSS/HTML filter for admin-only use. | |
694 * | |
695 * Note: This method only filters if $string is not marked safe already. This | |
696 * ensures that HTML intended for display is not filtered. | |
697 * | |
698 * @param string|\Drupal\Core\Render\Markup $string | |
699 * A string. | |
700 * | |
701 * @return \Drupal\Core\Render\Markup | |
702 * The escaped string wrapped in a Markup object. If the string is an | |
703 * instance of \Drupal\Component\Render\MarkupInterface, it won't be escaped | |
704 * again. | |
705 */ | |
706 protected function xssFilterAdminIfUnsafe($string) { | |
707 if (!($string instanceof MarkupInterface)) { | |
708 $string = Xss::filterAdmin($string); | |
709 } | |
710 return Markup::create($string); | |
711 } | |
712 | |
713 /** | |
714 * Escapes #plain_text or filters #markup as required. | |
715 * | |
716 * Drupal uses Twig's auto-escape feature to improve security. This feature | |
717 * automatically escapes any HTML that is not known to be safe. Due to this | |
718 * the render system needs to ensure that all markup it generates is marked | |
719 * safe so that Twig does not do any additional escaping. | |
720 * | |
721 * By default all #markup is filtered to protect against XSS using the admin | |
722 * tag list. Render arrays can alter the list of tags allowed by the filter | |
723 * using the #allowed_tags property. This value should be an array of tags | |
724 * that Xss::filter() would accept. Render arrays can escape text instead | |
725 * of XSS filtering by setting the #plain_text property instead of #markup. If | |
726 * #plain_text is used #allowed_tags is ignored. | |
727 * | |
728 * @param array $elements | |
729 * A render array with #markup set. | |
730 * | |
731 * @return \Drupal\Component\Render\MarkupInterface|string | |
732 * The escaped markup wrapped in a Markup object. If $elements['#markup'] | |
733 * is an instance of \Drupal\Component\Render\MarkupInterface, it won't be | |
734 * escaped or filtered again. | |
735 * | |
736 * @see \Drupal\Component\Utility\Html::escape() | |
737 * @see \Drupal\Component\Utility\Xss::filter() | |
738 * @see \Drupal\Component\Utility\Xss::filterAdmin() | |
739 */ | |
740 protected function ensureMarkupIsSafe(array $elements) { | |
741 if (empty($elements['#markup']) && empty($elements['#plain_text'])) { | |
742 return $elements; | |
743 } | |
744 | |
745 if (!empty($elements['#plain_text'])) { | |
746 $elements['#markup'] = Markup::create(Html::escape($elements['#plain_text'])); | |
747 } | |
748 elseif (!($elements['#markup'] instanceof MarkupInterface)) { | |
749 // The default behaviour is to XSS filter using the admin tag list. | |
750 $tags = isset($elements['#allowed_tags']) ? $elements['#allowed_tags'] : Xss::getAdminTagList(); | |
751 $elements['#markup'] = Markup::create(Xss::filter($elements['#markup'], $tags)); | |
752 } | |
753 | |
754 return $elements; | |
755 } | |
756 | |
757 } |