Code Coverage |
||||||||||
Classes and Traits |
Functions and Methods |
Lines |
||||||||
| Total | |
0.00% |
0 / 1 |
|
60.00% |
9 / 15 |
CRAP | |
92.68% |
190 / 205 |
| Renderer | |
0.00% |
0 / 1 |
|
62.50% |
10 / 16 |
111.49 | |
92.68% |
190 / 205 |
| __construct | |
0.00% |
0 / 1 |
2.00 | |
90.00% |
9 / 10 |
|||
| renderRoot | |
100.00% |
1 / 1 |
2 | |
100.00% |
8 / 8 |
|||
| anonymous function | |
100.00% |
1 / 1 |
2 | |
100.00% |
0 / 0 |
|||
| renderPlain | |
100.00% |
1 / 1 |
1 | |
100.00% |
2 / 2 |
|||
| renderPlaceholder | |
100.00% |
1 / 1 |
1 | |
100.00% |
7 / 7 |
|||
| render | |
100.00% |
1 / 1 |
2 | |
100.00% |
4 / 4 |
|||
| doRender | |
0.00% |
0 / 1 |
77 | |
95.45% |
42 / 44 |
|||
| hasRenderContext | |
0.00% |
0 / 1 |
2 | |
0.00% |
0 / 1 |
|||
| executeInRenderContext | |
0.00% |
0 / 1 |
2.01 | |
85.71% |
6 / 7 |
|||
| getCurrentRenderContext | |
100.00% |
1 / 1 |
2 | |
100.00% |
2 / 2 |
|||
| setCurrentRenderContext | |
100.00% |
1 / 1 |
1 | |
100.00% |
3 / 3 |
|||
| replacePlaceholders | |
100.00% |
1 / 1 |
4 | |
100.00% |
5 / 5 |
|||
| mergeBubbleableMetadata | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
| addCacheableDependency | |
100.00% |
1 / 1 |
1 | |
100.00% |
4 / 4 |
|||
| xssFilterAdminIfUnsafe | |
0.00% |
0 / 1 |
6 | |
0.00% |
0 / 3 |
|||
| ensureMarkupIsSafe | |
0.00% |
0 / 1 |
6.56 | |
75.00% |
6 / 8 |
|||
| <?php | |
| /** | |
| * @file | |
| * Contains \Drupal\Core\Render\Renderer. | |
| */ | |
| namespace Drupal\Core\Render; | |
| use Drupal\Component\Utility\Html; | |
| use Drupal\Component\Utility\SafeMarkup; | |
| use Drupal\Component\Utility\Xss; | |
| use Drupal\Core\Access\AccessResultInterface; | |
| use Drupal\Core\Cache\Cache; | |
| use Drupal\Core\Cache\CacheableMetadata; | |
| use Drupal\Core\Controller\ControllerResolverInterface; | |
| use Drupal\Core\Theme\ThemeManagerInterface; | |
| use Symfony\Component\HttpFoundation\RequestStack; | |
| /** | |
| * Turns a render array into a HTML string. | |
| */ | |
| class Renderer implements RendererInterface { | |
| /** | |
| * The theme manager. | |
| * | |
| * @var \Drupal\Core\Theme\ThemeManagerInterface | |
| */ | |
| protected $theme; | |
| /** | |
| * The controller resolver. | |
| * | |
| * @var \Drupal\Core\Controller\ControllerResolverInterface | |
| */ | |
| protected $controllerResolver; | |
| /** | |
| * The element info. | |
| * | |
| * @var \Drupal\Core\Render\ElementInfoManagerInterface | |
| */ | |
| protected $elementInfo; | |
| /** | |
| * The placeholder generator. | |
| * | |
| * @var \Drupal\Core\Render\PlaceholderGeneratorInterface | |
| */ | |
| protected $placeholderGenerator; | |
| /** | |
| * The render cache service. | |
| * | |
| * @var \Drupal\Core\Render\RenderCacheInterface | |
| */ | |
| protected $renderCache; | |
| /** | |
| * The renderer configuration array. | |
| * | |
| * @var array | |
| */ | |
| protected $rendererConfig; | |
| /** | |
| * Whether we're currently in a ::renderRoot() call. | |
| * | |
| * @var bool | |
| */ | |
| protected $isRenderingRoot = FALSE; | |
| /** | |
| * The request stack. | |
| * | |
| * @var \Symfony\Component\HttpFoundation\RequestStack | |
| */ | |
| protected $requestStack; | |
| /** | |
| * The render context collection. | |
| * | |
| * An individual global render context is tied to the current request. We then | |
| * need to maintain a different context for each request to correctly handle | |
| * rendering in subrequests. | |
| * | |
| * This must be static as long as some controllers rebuild the container | |
| * during a request. This causes multiple renderer instances to co-exist | |
| * simultaneously, render state getting lost, and therefore causing pages to | |
| * fail to render correctly. As soon as it is guaranteed that during a request | |
| * the same container is used, it no longer needs to be static. | |
| * | |
| * @var \Drupal\Core\Render\RenderContext[] | |
| */ | |
| protected static $contextCollection; | |
| /** | |
| * Constructs a new Renderer. | |
| * | |
| * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver | |
| * The controller resolver. | |
| * @param \Drupal\Core\Theme\ThemeManagerInterface $theme | |
| * The theme manager. | |
| * @param \Drupal\Core\Render\ElementInfoManagerInterface $element_info | |
| * The element info. | |
| * @param \Drupal\Core\Render\PlaceholderGeneratorInterface $placeholder_generator | |
| * The placeholder generator. | |
| * @param \Drupal\Core\Render\RenderCacheInterface $render_cache | |
| * The render cache service. | |
| * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack | |
| * The request stack. | |
| * @param array $renderer_config | |
| * The renderer configuration array. | |
| */ | |
| public function __construct(ControllerResolverInterface $controller_resolver, ThemeManagerInterface $theme, ElementInfoManagerInterface $element_info, PlaceholderGeneratorInterface $placeholder_generator, RenderCacheInterface $render_cache, RequestStack $request_stack, array $renderer_config) { | |
| $this->controllerResolver = $controller_resolver; | |
| $this->theme = $theme; | |
| $this->elementInfo = $element_info; | |
| $this->placeholderGenerator = $placeholder_generator; | |
| $this->renderCache = $render_cache; | |
| $this->rendererConfig = $renderer_config; | |
| $this->requestStack = $request_stack; | |
| // Initialize the context collection if needed. | |
| if (!isset(static::$contextCollection)) { | |
| static::$contextCollection = new \SplObjectStorage(); | |
| } | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function renderRoot(&$elements) { | |
| // Disallow calling ::renderRoot() from within another ::renderRoot() call. | |
| if ($this->isRenderingRoot) { | |
| $this->isRenderingRoot = FALSE; | |
| throw new \LogicException('A stray renderRoot() invocation is causing bubbling of attached assets to break.'); | |
| } | |
| // Render in its own render context. | |
| $this->isRenderingRoot = TRUE; | |
| $output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) { | |
| return $this->render($elements, TRUE); | |
| }); | |
| $this->isRenderingRoot = FALSE; | |
| return $output; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function renderPlain(&$elements) { | |
| return $this->executeInRenderContext(new RenderContext(), function () use (&$elements) { | |
| return $this->render($elements, TRUE); | |
| }); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function renderPlaceholder($placeholder, array $elements) { | |
| // Get the render array for the given placeholder | |
| $placeholder_elements = $elements['#attached']['placeholders'][$placeholder]; | |
| // Prevent the render array from being auto-placeholdered again. | |
| $placeholder_elements['#create_placeholder'] = FALSE; | |
| // Render the placeholder into markup. | |
| $markup = $this->renderPlain($placeholder_elements); | |
| // Replace the placeholder with its rendered markup, and merge its | |
| // bubbleable metadata with the main elements'. | |
| $elements['#markup'] = Markup::create(str_replace($placeholder, $markup, $elements['#markup'])); | |
| $elements = $this->mergeBubbleableMetadata($elements, $placeholder_elements); | |
| // Remove the placeholder that we've just rendered. | |
| unset($elements['#attached']['placeholders'][$placeholder]); | |
| return $elements; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function render(&$elements, $is_root_call = FALSE) { | |
| // Since #pre_render, #post_render, #lazy_builder callbacks and theme | |
| // functions or templates may be used for generating a render array's | |
| // content, and we might be rendering the main content for the page, it is | |
| // possible that any of them throw an exception that will cause a different | |
| // page to be rendered (e.g. throwing | |
| // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause | |
| // the 404 page to be rendered). That page might also use | |
| // Renderer::renderRoot() but if exceptions aren't caught here, it will be | |
| // impossible to call Renderer::renderRoot() again. | |
| // Hence, catch all exceptions, reset the isRenderingRoot property and | |
| // re-throw exceptions. | |
| try { | |
| return $this->doRender($elements, $is_root_call); | |
| } | |
| catch (\Exception $e) { | |
| // Mark the ::rootRender() call finished due to this exception & re-throw. | |
| $this->isRenderingRoot = FALSE; | |
| throw $e; | |
| } | |
| } | |
| /** | |
| * See the docs for ::render(). | |
| */ | |
| protected function doRender(&$elements, $is_root_call = FALSE) { | |
| if (empty($elements)) { | |
| return ''; | |
| } | |
| if (!isset($elements['#access']) && isset($elements['#access_callback'])) { | |
| if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { | |
| $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']); | |
| } | |
| $elements['#access'] = call_user_func($elements['#access_callback'], $elements); | |
| } | |
| // Early-return nothing if user does not have access. | |
| if (isset($elements['#access'])) { | |
| // If #access is an AccessResultInterface object, we must apply it's | |
| // cacheability metadata to the render array. | |
| if ($elements['#access'] instanceof AccessResultInterface) { | |
| $this->addCacheableDependency($elements, $elements['#access']); | |
| if (!$elements['#access']->isAllowed()) { | |
| return ''; | |
| } | |
| } | |
| elseif ($elements['#access'] === FALSE) { | |
| return ''; | |
| } | |
| } | |
| // Do not print elements twice. | |
| if (!empty($elements['#printed'])) { | |
| return ''; | |
| } | |
| $context = $this->getCurrentRenderContext(); | |
| if (!isset($context)) { | |
| 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."); | |
| } | |
| $context->push(new BubbleableMetadata()); | |
| // Set the bubbleable rendering metadata that has configurable defaults, if: | |
| // - this is the root call, to ensure that the final render array definitely | |
| // has these configurable defaults, even when no subtree is render cached. | |
| // - this is a render cacheable subtree, to ensure that the cached data has | |
| // the configurable defaults (which may affect the ID and invalidation). | |
| if ($is_root_call || isset($elements['#cache']['keys'])) { | |
| $required_cache_contexts = $this->rendererConfig['required_cache_contexts']; | |
| if (isset($elements['#cache']['contexts'])) { | |
| $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts); | |
| } | |
| else { | |
| $elements['#cache']['contexts'] = $required_cache_contexts; | |
| } | |
| } | |
| // Try to fetch the prerendered element from cache, replace any placeholders | |
| // and return the final markup. | |
| if (isset($elements['#cache']['keys'])) { | |
| $cached_element = $this->renderCache->get($elements); | |
| if ($cached_element !== FALSE) { | |
| $elements = $cached_element; | |
| // Only when we're in a root (non-recursive) Renderer::render() call, | |
| // placeholders must be processed, to prevent breaking the render cache | |
| // in case of nested elements with #cache set. | |
| if ($is_root_call) { | |
| $this->replacePlaceholders($elements); | |
| } | |
| // Mark the element markup as safe if is it a string. | |
| if (is_string($elements['#markup'])) { | |
| $elements['#markup'] = Markup::create($elements['#markup']); | |
| } | |
| // The render cache item contains all the bubbleable rendering metadata | |
| // for the subtree. | |
| $context->update($elements); | |
| // Render cache hit, so rendering is finished, all necessary info | |
| // collected! | |
| $context->bubble(); | |
| return $elements['#markup']; | |
| } | |
| } | |
| // Two-tier caching: track pre-bubbling elements' #cache, #lazy_builder and | |
| // #create_placeholder for later comparison. | |
| // @see \Drupal\Core\Render\RenderCacheInterface::get() | |
| // @see \Drupal\Core\Render\RenderCacheInterface::set() | |
| $pre_bubbling_elements = array_intersect_key($elements, [ | |
| '#cache' => TRUE, | |
| '#lazy_builder' => TRUE, | |
| '#create_placeholder' => TRUE, | |
| ]); | |
| // If the default values for this element have not been loaded yet, populate | |
| // them. | |
| if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { | |
| $elements += $this->elementInfo->getInfo($elements['#type']); | |
| } | |
| // First validate the usage of #lazy_builder; both of the next if-statements | |
| // use it if available. | |
| if (isset($elements['#lazy_builder'])) { | |
| // @todo Convert to assertions once https://www.drupal.org/node/2408013 | |
| // lands. | |
| if (!is_array($elements['#lazy_builder'])) { | |
| throw new \DomainException('The #lazy_builder property must have an array as a value.'); | |
| } | |
| if (count($elements['#lazy_builder']) !== 2) { | |
| throw new \DomainException('The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.'); | |
| } | |
| if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function($v) { return is_null($v) || is_scalar($v); }))) { | |
| throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL."); | |
| } | |
| $children = Element::children($elements); | |
| if ($children) { | |
| throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no children can exist; all children must be generated by the #lazy_builder callback. You specified the following children: %s.', implode(', ', $children))); | |
| } | |
| $supported_keys = [ | |
| '#lazy_builder', | |
| '#cache', | |
| '#create_placeholder', | |
| // These keys are not actually supported, but they are added automatically | |
| // by the Renderer, so we don't crash on them; them being missing when | |
| // their #lazy_builder callback is invoked won't surprise the developer. | |
| '#weight', | |
| '#printed' | |
| ]; | |
| $unsupported_keys = array_diff(array_keys($elements), $supported_keys); | |
| if (count($unsupported_keys)) { | |
| throw new \DomainException(sprintf('When a #lazy_builder callback is specified, no properties can exist; all properties must be generated by the #lazy_builder callback. You specified the following properties: %s.', implode(', ', $unsupported_keys))); | |
| } | |
| } | |
| // Determine whether to do auto-placeholdering. | |
| if ($this->placeholderGenerator->canCreatePlaceholder($elements) && $this->placeholderGenerator->shouldAutomaticallyPlaceholder($elements)) { | |
| $elements['#create_placeholder'] = TRUE; | |
| } | |
| // If instructed to create a placeholder, and a #lazy_builder callback is | |
| // present (without such a callback, it would be impossible to replace the | |
| // placeholder), replace the current element with a placeholder. | |
| if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE) { | |
| if (!isset($elements['#lazy_builder'])) { | |
| throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.'); | |
| } | |
| $elements = $this->placeholderGenerator->createPlaceholder($elements); | |
| } | |
| // Build the element if it is still empty. | |
| if (isset($elements['#lazy_builder'])) { | |
| $callable = $elements['#lazy_builder'][0]; | |
| $args = $elements['#lazy_builder'][1]; | |
| if (is_string($callable) && strpos($callable, '::') === FALSE) { | |
| $callable = $this->controllerResolver->getControllerFromDefinition($callable); | |
| } | |
| $new_elements = call_user_func_array($callable, $args); | |
| // Retain the original cacheability metadata, plus cache keys. | |
| CacheableMetadata::createFromRenderArray($elements) | |
| ->merge(CacheableMetadata::createFromRenderArray($new_elements)) | |
| ->applyTo($new_elements); | |
| if (isset($elements['#cache']['keys'])) { | |
| $new_elements['#cache']['keys'] = $elements['#cache']['keys']; | |
| } | |
| $elements = $new_elements; | |
| $elements['#lazy_builder_built'] = TRUE; | |
| } | |
| // All render elements support #markup and #plain_text. | |
| if (!empty($elements['#markup']) || !empty($elements['#plain_text'])) { | |
| $elements = $this->ensureMarkupIsSafe($elements); | |
| } | |
| // Make any final changes to the element before it is rendered. This means | |
| // that the $element or the children can be altered or corrected before the | |
| // element is rendered into the final text. | |
| if (isset($elements['#pre_render'])) { | |
| foreach ($elements['#pre_render'] as $callable) { | |
| if (is_string($callable) && strpos($callable, '::') === FALSE) { | |
| $callable = $this->controllerResolver->getControllerFromDefinition($callable); | |
| } | |
| $elements = call_user_func($callable, $elements); | |
| } | |
| } | |
| // Defaults for bubbleable rendering metadata. | |
| $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : array(); | |
| $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT; | |
| $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); | |
| // Allow #pre_render to abort rendering. | |
| if (!empty($elements['#printed'])) { | |
| // The #printed element contains all the bubbleable rendering metadata for | |
| // the subtree. | |
| $context->update($elements); | |
| // #printed, so rendering is finished, all necessary info collected! | |
| $context->bubble(); | |
| return ''; | |
| } | |
| // Add any JavaScript state information associated with the element. | |
| if (!empty($elements['#states'])) { | |
| drupal_process_states($elements); | |
| } | |
| // Get the children of the element, sorted by weight. | |
| $children = Element::children($elements, TRUE); | |
| // Initialize this element's #children, unless a #pre_render callback | |
| // already preset #children. | |
| if (!isset($elements['#children'])) { | |
| $elements['#children'] = ''; | |
| } | |
| // Assume that if #theme is set it represents an implemented hook. | |
| $theme_is_implemented = isset($elements['#theme']); | |
| // Check the elements for insecure HTML and pass through sanitization. | |
| if (isset($elements)) { | |
| $markup_keys = array( | |
| '#description', | |
| '#field_prefix', | |
| '#field_suffix', | |
| ); | |
| foreach ($markup_keys as $key) { | |
| if (!empty($elements[$key]) && is_scalar($elements[$key])) { | |
| $elements[$key] = $this->xssFilterAdminIfUnsafe($elements[$key]); | |
| } | |
| } | |
| } | |
| // Call the element's #theme function if it is set. Then any children of the | |
| // element have to be rendered there. If the internal #render_children | |
| // property is set, do not call the #theme function to prevent infinite | |
| // recursion. | |
| if ($theme_is_implemented && !isset($elements['#render_children'])) { | |
| $elements['#children'] = $this->theme->render($elements['#theme'], $elements); | |
| // If ThemeManagerInterface::render() returns FALSE this means that the | |
| // hook in #theme was not found in the registry and so we need to update | |
| // our flag accordingly. This is common for theme suggestions. | |
| $theme_is_implemented = ($elements['#children'] !== FALSE); | |
| } | |
| // If #theme is not implemented or #render_children is set and the element | |
| // has an empty #children attribute, render the children now. This is the | |
| // same process as Renderer::render() but is inlined for speed. | |
| if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { | |
| foreach ($children as $key) { | |
| $elements['#children'] .= $this->doRender($elements[$key]); | |
| } | |
| $elements['#children'] = Markup::create($elements['#children']); | |
| } | |
| // If #theme is not implemented and the element has raw #markup as a | |
| // fallback, prepend the content in #markup to #children. In this case | |
| // #children will contain whatever is provided by #pre_render prepended to | |
| // what is rendered recursively above. If #theme is implemented then it is | |
| // the responsibility of that theme implementation to render #markup if | |
| // required. Eventually #theme_wrappers will expect both #markup and | |
| // #children to be a single string as #children. | |
| if (!$theme_is_implemented && isset($elements['#markup'])) { | |
| $elements['#children'] = Markup::create($elements['#markup'] . $elements['#children']); | |
| } | |
| // Let the theme functions in #theme_wrappers add markup around the rendered | |
| // children. | |
| // #states and #attached have to be processed before #theme_wrappers, | |
| // because the #type 'page' render array from drupal_prepare_page() would | |
| // render the $page and wrap it into the html.html.twig template without the | |
| // attached assets otherwise. | |
| // If the internal #render_children property is set, do not call the | |
| // #theme_wrappers function(s) to prevent infinite recursion. | |
| if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) { | |
| foreach ($elements['#theme_wrappers'] as $key => $value) { | |
| // If the value of a #theme_wrappers item is an array then the theme | |
| // hook is found in the key of the item and the value contains attribute | |
| // overrides. Attribute overrides replace key/value pairs in $elements | |
| // for only this ThemeManagerInterface::render() call. This allows | |
| // #theme hooks and #theme_wrappers hooks to share variable names | |
| // without conflict or ambiguity. | |
| $wrapper_elements = $elements; | |
| if (is_string($key)) { | |
| $wrapper_hook = $key; | |
| foreach ($value as $attribute => $override) { | |
| $wrapper_elements[$attribute] = $override; | |
| } | |
| } | |
| else { | |
| $wrapper_hook = $value; | |
| } | |
| $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements); | |
| } | |
| } | |
| // Filter the outputted content and make any last changes before the content | |
| // is sent to the browser. The changes are made on $content which allows the | |
| // outputted text to be filtered. | |
| if (isset($elements['#post_render'])) { | |
| foreach ($elements['#post_render'] as $callable) { | |
| if (is_string($callable) && strpos($callable, '::') === FALSE) { | |
| $callable = $this->controllerResolver->getControllerFromDefinition($callable); | |
| } | |
| $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); | |
| } | |
| } | |
| // We store the resulting output in $elements['#markup'], to be consistent | |
| // with how render cached output gets stored. This ensures that placeholder | |
| // replacement logic gets the same data to work with, no matter if #cache is | |
| // disabled, #cache is enabled, there is a cache hit or miss. | |
| $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : ''; | |
| $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : ''; | |
| $elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix); | |
| // We've rendered this element (and its subtree!), now update the context. | |
| $context->update($elements); | |
| // Cache the processed element if both $pre_bubbling_elements and $elements | |
| // have the metadata necessary to generate a cache ID. | |
| if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) { | |
| if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) { | |
| throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.'); | |
| } | |
| $this->renderCache->set($elements, $pre_bubbling_elements); | |
| // Update the render context; the render cache implementation may update | |
| // the element, and it may have different bubbleable metadata now. | |
| // @see \Drupal\Core\Render\PlaceholderingRenderCache::set() | |
| $context->pop(); | |
| $context->push(new BubbleableMetadata()); | |
| $context->update($elements); | |
| } | |
| // Only when we're in a root (non-recursive) Renderer::render() call, | |
| // placeholders must be processed, to prevent breaking the render cache in | |
| // case of nested elements with #cache set. | |
| // | |
| // By running them here, we ensure that: | |
| // - they run when #cache is disabled, | |
| // - they run when #cache is enabled and there is a cache miss. | |
| // Only the case of a cache hit when #cache is enabled, is not handled here, | |
| // that is handled earlier in Renderer::render(). | |
| if ($is_root_call) { | |
| $this->replacePlaceholders($elements); | |
| // @todo remove as part of https://www.drupal.org/node/2511330. | |
| if ($context->count() !== 1) { | |
| throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); | |
| } | |
| } | |
| // Rendering is finished, all necessary info collected! | |
| $context->bubble(); | |
| $elements['#printed'] = TRUE; | |
| return $elements['#markup']; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function hasRenderContext() { | |
| return (bool) $this->getCurrentRenderContext(); | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function executeInRenderContext(RenderContext $context, callable $callable) { | |
| // Store the current render context. | |
| $previous_context = $this->getCurrentRenderContext(); | |
| // Set the provided context and call the callable, it will use that context. | |
| $this->setCurrentRenderContext($context); | |
| $result = $callable(); | |
| // @todo Convert to an assertion in https://www.drupal.org/node/2408013 | |
| if ($context->count() > 1) { | |
| throw new \LogicException('Bubbling failed.'); | |
| } | |
| // Restore the original render context. | |
| $this->setCurrentRenderContext($previous_context); | |
| return $result; | |
| } | |
| /** | |
| * Returns the current render context. | |
| * | |
| * @return \Drupal\Core\Render\RenderContext | |
| * The current render context. | |
| */ | |
| protected function getCurrentRenderContext() { | |
| $request = $this->requestStack->getCurrentRequest(); | |
| return isset(static::$contextCollection[$request]) ? static::$contextCollection[$request] : NULL; | |
| } | |
| /** | |
| * Sets the current render context. | |
| * | |
| * @param \Drupal\Core\Render\RenderContext|null $context | |
| * The render context. This can be NULL for instance when restoring the | |
| * original render context, which is in fact NULL. | |
| * | |
| * @return $this | |
| */ | |
| protected function setCurrentRenderContext(RenderContext $context = NULL) { | |
| $request = $this->requestStack->getCurrentRequest(); | |
| static::$contextCollection[$request] = $context; | |
| return $this; | |
| } | |
| /** | |
| * Replaces placeholders. | |
| * | |
| * Placeholders may have: | |
| * - #lazy_builder callback, to build a render array to be rendered into | |
| * markup that can replace the placeholder | |
| * - #cache: to cache the result of the placeholder | |
| * | |
| * Also merges the bubbleable metadata resulting from the rendering of the | |
| * contents of the placeholders. Hence $elements will be contain the entirety | |
| * of bubbleable metadata. | |
| * | |
| * @param array &$elements | |
| * The structured array describing the data being rendered. Including the | |
| * bubbleable metadata associated with the markup that replaced the | |
| * placeholders. | |
| * | |
| * @returns bool | |
| * Whether placeholders were replaced. | |
| * | |
| * @see \Drupal\Core\Render\Renderer::renderPlaceholder() | |
| */ | |
| protected function replacePlaceholders(array &$elements) { | |
| if (!isset($elements['#attached']['placeholders']) || empty($elements['#attached']['placeholders'])) { | |
| return FALSE; | |
| } | |
| foreach (array_keys($elements['#attached']['placeholders']) as $placeholder) { | |
| $elements = $this->renderPlaceholder($placeholder, $elements); | |
| } | |
| return TRUE; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function mergeBubbleableMetadata(array $a, array $b) { | |
| $meta_a = BubbleableMetadata::createFromRenderArray($a); | |
| $meta_b = BubbleableMetadata::createFromRenderArray($b); | |
| $meta_a->merge($meta_b)->applyTo($a); | |
| return $a; | |
| } | |
| /** | |
| * {@inheritdoc} | |
| */ | |
| public function addCacheableDependency(array &$elements, $dependency) { | |
| $meta_a = CacheableMetadata::createFromRenderArray($elements); | |
| $meta_b = CacheableMetadata::createFromObject($dependency); | |
| $meta_a->merge($meta_b)->applyTo($elements); | |
| } | |
| /** | |
| * Applies a very permissive XSS/HTML filter for admin-only use. | |
| * | |
| * Note: This method only filters if $string is not marked safe already. This | |
| * ensures that HTML intended for display is not filtered. | |
| * | |
| * @param string|\Drupal\Core\Render\Markup $string | |
| * A string. | |
| * | |
| * @return \Drupal\Core\Render\Markup | |
| * The escaped string wrapped in a Markup object. If | |
| * SafeMarkup::isSafe($string) returns TRUE, it won't be escaped again. | |
| */ | |
| protected function xssFilterAdminIfUnsafe($string) { | |
| if (!SafeMarkup::isSafe($string)) { | |
| $string = Xss::filterAdmin($string); | |
| } | |
| return Markup::create($string); | |
| } | |
| /** | |
| * Escapes #plain_text or filters #markup as required. | |
| * | |
| * Drupal uses Twig's auto-escape feature to improve security. This feature | |
| * automatically escapes any HTML that is not known to be safe. Due to this | |
| * the render system needs to ensure that all markup it generates is marked | |
| * safe so that Twig does not do any additional escaping. | |
| * | |
| * By default all #markup is filtered to protect against XSS using the admin | |
| * tag list. Render arrays can alter the list of tags allowed by the filter | |
| * using the #allowed_tags property. This value should be an array of tags | |
| * that Xss::filter() would accept. Render arrays can escape text instead | |
| * of XSS filtering by setting the #plain_text property instead of #markup. If | |
| * #plain_text is used #allowed_tags is ignored. | |
| * | |
| * @param array $elements | |
| * A render array with #markup set. | |
| * | |
| * @return \Drupal\Component\Render\MarkupInterface|string | |
| * The escaped markup wrapped in a Markup object. If | |
| * SafeMarkup::isSafe($elements['#markup']) returns TRUE, it won't be | |
| * escaped or filtered again. | |
| * | |
| * @see \Drupal\Component\Utility\Html::escape() | |
| * @see \Drupal\Component\Utility\Xss::filter() | |
| * @see \Drupal\Component\Utility\Xss::adminFilter() | |
| */ | |
| protected function ensureMarkupIsSafe(array $elements) { | |
| if (empty($elements['#markup']) && empty($elements['#plain_text'])) { | |
| return $elements; | |
| } | |
| if (!empty($elements['#plain_text'])) { | |
| $elements['#markup'] = Markup::create(Html::escape($elements['#plain_text'])); | |
| } | |
| elseif (!SafeMarkup::isSafe($elements['#markup'])) { | |
| // The default behaviour is to XSS filter using the admin tag list. | |
| $tags = isset($elements['#allowed_tags']) ? $elements['#allowed_tags'] : Xss::getAdminTagList(); | |
| $elements['#markup'] = Markup::create(Xss::filter($elements['#markup'], $tags)); | |
| } | |
| return $elements; | |
| } | |
| } |