Chris@0: controllerResolver = $controller_resolver; Chris@0: $this->theme = $theme; Chris@0: $this->elementInfo = $element_info; Chris@0: $this->placeholderGenerator = $placeholder_generator; Chris@0: $this->renderCache = $render_cache; Chris@0: $this->rendererConfig = $renderer_config; Chris@0: $this->requestStack = $request_stack; Chris@0: Chris@0: // Initialize the context collection if needed. Chris@0: if (!isset(static::$contextCollection)) { Chris@0: static::$contextCollection = new \SplObjectStorage(); Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function renderRoot(&$elements) { Chris@0: // Disallow calling ::renderRoot() from within another ::renderRoot() call. Chris@0: if ($this->isRenderingRoot) { Chris@0: $this->isRenderingRoot = FALSE; Chris@0: throw new \LogicException('A stray renderRoot() invocation is causing bubbling of attached assets to break.'); Chris@0: } Chris@0: Chris@0: // Render in its own render context. Chris@0: $this->isRenderingRoot = TRUE; Chris@0: $output = $this->executeInRenderContext(new RenderContext(), function () use (&$elements) { Chris@0: return $this->render($elements, TRUE); Chris@0: }); Chris@0: $this->isRenderingRoot = FALSE; Chris@0: Chris@0: return $output; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function renderPlain(&$elements) { Chris@0: return $this->executeInRenderContext(new RenderContext(), function () use (&$elements) { Chris@0: return $this->render($elements, TRUE); Chris@0: }); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function renderPlaceholder($placeholder, array $elements) { Chris@0: // Get the render array for the given placeholder Chris@0: $placeholder_elements = $elements['#attached']['placeholders'][$placeholder]; Chris@0: Chris@0: // Prevent the render array from being auto-placeholdered again. Chris@0: $placeholder_elements['#create_placeholder'] = FALSE; Chris@0: Chris@0: // Render the placeholder into markup. Chris@0: $markup = $this->renderPlain($placeholder_elements); Chris@0: Chris@0: // Replace the placeholder with its rendered markup, and merge its Chris@0: // bubbleable metadata with the main elements'. Chris@0: $elements['#markup'] = Markup::create(str_replace($placeholder, $markup, $elements['#markup'])); Chris@0: $elements = $this->mergeBubbleableMetadata($elements, $placeholder_elements); Chris@0: Chris@0: // Remove the placeholder that we've just rendered. Chris@0: unset($elements['#attached']['placeholders'][$placeholder]); Chris@0: Chris@0: return $elements; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function render(&$elements, $is_root_call = FALSE) { Chris@0: // Since #pre_render, #post_render, #lazy_builder callbacks and theme Chris@0: // functions or templates may be used for generating a render array's Chris@0: // content, and we might be rendering the main content for the page, it is Chris@0: // possible that any of them throw an exception that will cause a different Chris@0: // page to be rendered (e.g. throwing Chris@0: // \Symfony\Component\HttpKernel\Exception\NotFoundHttpException will cause Chris@0: // the 404 page to be rendered). That page might also use Chris@0: // Renderer::renderRoot() but if exceptions aren't caught here, it will be Chris@0: // impossible to call Renderer::renderRoot() again. Chris@0: // Hence, catch all exceptions, reset the isRenderingRoot property and Chris@0: // re-throw exceptions. Chris@0: try { Chris@0: return $this->doRender($elements, $is_root_call); Chris@0: } Chris@0: catch (\Exception $e) { Chris@0: // Mark the ::rootRender() call finished due to this exception & re-throw. Chris@0: $this->isRenderingRoot = FALSE; Chris@0: throw $e; Chris@0: } Chris@0: } Chris@0: Chris@0: /** Chris@0: * See the docs for ::render(). Chris@0: */ Chris@0: protected function doRender(&$elements, $is_root_call = FALSE) { Chris@0: if (empty($elements)) { Chris@0: return ''; Chris@0: } Chris@0: Chris@0: if (!isset($elements['#access']) && isset($elements['#access_callback'])) { Chris@0: if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) { Chris@0: $elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']); Chris@0: } Chris@0: $elements['#access'] = call_user_func($elements['#access_callback'], $elements); Chris@0: } Chris@0: Chris@0: // Early-return nothing if user does not have access. Chris@0: if (isset($elements['#access'])) { Chris@14: // If #access is an AccessResultInterface object, we must apply its Chris@0: // cacheability metadata to the render array. Chris@0: if ($elements['#access'] instanceof AccessResultInterface) { Chris@0: $this->addCacheableDependency($elements, $elements['#access']); Chris@0: if (!$elements['#access']->isAllowed()) { Chris@0: return ''; Chris@0: } Chris@0: } Chris@0: elseif ($elements['#access'] === FALSE) { Chris@0: return ''; Chris@0: } Chris@0: } Chris@0: Chris@0: // Do not print elements twice. Chris@0: if (!empty($elements['#printed'])) { Chris@0: return ''; Chris@0: } Chris@0: Chris@0: $context = $this->getCurrentRenderContext(); Chris@0: if (!isset($context)) { Chris@0: 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."); Chris@0: } Chris@0: $context->push(new BubbleableMetadata()); Chris@0: Chris@0: // Set the bubbleable rendering metadata that has configurable defaults, if: Chris@0: // - this is the root call, to ensure that the final render array definitely Chris@0: // has these configurable defaults, even when no subtree is render cached. Chris@0: // - this is a render cacheable subtree, to ensure that the cached data has Chris@0: // the configurable defaults (which may affect the ID and invalidation). Chris@0: if ($is_root_call || isset($elements['#cache']['keys'])) { Chris@0: $required_cache_contexts = $this->rendererConfig['required_cache_contexts']; Chris@0: if (isset($elements['#cache']['contexts'])) { Chris@0: $elements['#cache']['contexts'] = Cache::mergeContexts($elements['#cache']['contexts'], $required_cache_contexts); Chris@0: } Chris@0: else { Chris@0: $elements['#cache']['contexts'] = $required_cache_contexts; Chris@0: } Chris@0: } Chris@0: Chris@0: // Try to fetch the prerendered element from cache, replace any placeholders Chris@0: // and return the final markup. Chris@0: if (isset($elements['#cache']['keys'])) { Chris@0: $cached_element = $this->renderCache->get($elements); Chris@0: if ($cached_element !== FALSE) { Chris@0: $elements = $cached_element; Chris@0: // Only when we're in a root (non-recursive) Renderer::render() call, Chris@0: // placeholders must be processed, to prevent breaking the render cache Chris@0: // in case of nested elements with #cache set. Chris@0: if ($is_root_call) { Chris@0: $this->replacePlaceholders($elements); Chris@0: } Chris@0: // Mark the element markup as safe if is it a string. Chris@0: if (is_string($elements['#markup'])) { Chris@0: $elements['#markup'] = Markup::create($elements['#markup']); Chris@0: } Chris@0: // The render cache item contains all the bubbleable rendering metadata Chris@0: // for the subtree. Chris@0: $context->update($elements); Chris@0: // Render cache hit, so rendering is finished, all necessary info Chris@0: // collected! Chris@0: $context->bubble(); Chris@0: return $elements['#markup']; Chris@0: } Chris@0: } Chris@0: // Two-tier caching: track pre-bubbling elements' #cache, #lazy_builder and Chris@0: // #create_placeholder for later comparison. Chris@0: // @see \Drupal\Core\Render\RenderCacheInterface::get() Chris@0: // @see \Drupal\Core\Render\RenderCacheInterface::set() Chris@0: $pre_bubbling_elements = array_intersect_key($elements, [ Chris@0: '#cache' => TRUE, Chris@0: '#lazy_builder' => TRUE, Chris@0: '#create_placeholder' => TRUE, Chris@0: ]); Chris@0: Chris@0: // If the default values for this element have not been loaded yet, populate Chris@0: // them. Chris@0: if (isset($elements['#type']) && empty($elements['#defaults_loaded'])) { Chris@0: $elements += $this->elementInfo->getInfo($elements['#type']); Chris@0: } Chris@0: Chris@0: // First validate the usage of #lazy_builder; both of the next if-statements Chris@0: // use it if available. Chris@0: if (isset($elements['#lazy_builder'])) { Chris@0: // @todo Convert to assertions once https://www.drupal.org/node/2408013 Chris@0: // lands. Chris@0: if (!is_array($elements['#lazy_builder'])) { Chris@0: throw new \DomainException('The #lazy_builder property must have an array as a value.'); Chris@0: } Chris@0: if (count($elements['#lazy_builder']) !== 2) { Chris@0: 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.'); Chris@0: } Chris@0: if (count($elements['#lazy_builder'][1]) !== count(array_filter($elements['#lazy_builder'][1], function ($v) { Chris@0: return is_null($v) || is_scalar($v); Chris@0: }))) { Chris@0: throw new \DomainException("A #lazy_builder callback's context may only contain scalar values or NULL."); Chris@0: } Chris@0: $children = Element::children($elements); Chris@0: if ($children) { Chris@0: 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))); Chris@0: } Chris@0: $supported_keys = [ Chris@0: '#lazy_builder', Chris@0: '#cache', Chris@0: '#create_placeholder', Chris@0: // The keys below are not actually supported, but these are added Chris@0: // automatically by the Renderer. Adding them as though they are Chris@0: // supported allows us to avoid throwing an exception 100% of the time. Chris@0: '#weight', Chris@17: '#printed', Chris@0: ]; Chris@0: $unsupported_keys = array_diff(array_keys($elements), $supported_keys); Chris@0: if (count($unsupported_keys)) { Chris@0: 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))); Chris@0: } Chris@0: } Chris@0: // Determine whether to do auto-placeholdering. Chris@0: if ($this->placeholderGenerator->canCreatePlaceholder($elements) && $this->placeholderGenerator->shouldAutomaticallyPlaceholder($elements)) { Chris@0: $elements['#create_placeholder'] = TRUE; Chris@0: } Chris@0: // If instructed to create a placeholder, and a #lazy_builder callback is Chris@0: // present (without such a callback, it would be impossible to replace the Chris@0: // placeholder), replace the current element with a placeholder. Chris@0: // @todo remove the isMethodCacheable() check when Chris@0: // https://www.drupal.org/node/2367555 lands. Chris@0: if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE && $this->requestStack->getCurrentRequest()->isMethodCacheable()) { Chris@0: if (!isset($elements['#lazy_builder'])) { Chris@0: throw new \LogicException('When #create_placeholder is set, a #lazy_builder callback must be present as well.'); Chris@0: } Chris@0: $elements = $this->placeholderGenerator->createPlaceholder($elements); Chris@0: } Chris@0: // Build the element if it is still empty. Chris@0: if (isset($elements['#lazy_builder'])) { Chris@0: $callable = $elements['#lazy_builder'][0]; Chris@0: $args = $elements['#lazy_builder'][1]; Chris@0: if (is_string($callable) && strpos($callable, '::') === FALSE) { Chris@0: $callable = $this->controllerResolver->getControllerFromDefinition($callable); Chris@0: } Chris@0: $new_elements = call_user_func_array($callable, $args); Chris@0: // Retain the original cacheability metadata, plus cache keys. Chris@0: CacheableMetadata::createFromRenderArray($elements) Chris@0: ->merge(CacheableMetadata::createFromRenderArray($new_elements)) Chris@0: ->applyTo($new_elements); Chris@0: if (isset($elements['#cache']['keys'])) { Chris@0: $new_elements['#cache']['keys'] = $elements['#cache']['keys']; Chris@0: } Chris@0: $elements = $new_elements; Chris@0: $elements['#lazy_builder_built'] = TRUE; Chris@0: } Chris@0: Chris@0: // Make any final changes to the element before it is rendered. This means Chris@0: // that the $element or the children can be altered or corrected before the Chris@0: // element is rendered into the final text. Chris@0: if (isset($elements['#pre_render'])) { Chris@0: foreach ($elements['#pre_render'] as $callable) { Chris@0: if (is_string($callable) && strpos($callable, '::') === FALSE) { Chris@0: $callable = $this->controllerResolver->getControllerFromDefinition($callable); Chris@0: } Chris@0: $elements = call_user_func($callable, $elements); Chris@0: } Chris@0: } Chris@0: Chris@0: // All render elements support #markup and #plain_text. Chris@16: if (isset($elements['#markup']) || isset($elements['#plain_text'])) { Chris@0: $elements = $this->ensureMarkupIsSafe($elements); Chris@0: } Chris@0: Chris@0: // Defaults for bubbleable rendering metadata. Chris@0: $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : []; Chris@0: $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT; Chris@0: $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : []; Chris@0: Chris@0: // Allow #pre_render to abort rendering. Chris@0: if (!empty($elements['#printed'])) { Chris@0: // The #printed element contains all the bubbleable rendering metadata for Chris@0: // the subtree. Chris@0: $context->update($elements); Chris@0: // #printed, so rendering is finished, all necessary info collected! Chris@0: $context->bubble(); Chris@0: return ''; Chris@0: } Chris@0: Chris@0: // Add any JavaScript state information associated with the element. Chris@0: if (!empty($elements['#states'])) { Chris@0: drupal_process_states($elements); Chris@0: } Chris@0: Chris@0: // Get the children of the element, sorted by weight. Chris@0: $children = Element::children($elements, TRUE); Chris@0: Chris@0: // Initialize this element's #children, unless a #pre_render callback Chris@0: // already preset #children. Chris@0: if (!isset($elements['#children'])) { Chris@0: $elements['#children'] = ''; Chris@0: } Chris@0: Chris@0: // Assume that if #theme is set it represents an implemented hook. Chris@0: $theme_is_implemented = isset($elements['#theme']); Chris@0: // Check the elements for insecure HTML and pass through sanitization. Chris@0: if (isset($elements)) { Chris@0: $markup_keys = [ Chris@0: '#description', Chris@0: '#field_prefix', Chris@0: '#field_suffix', Chris@0: ]; Chris@0: foreach ($markup_keys as $key) { Chris@0: if (!empty($elements[$key]) && is_scalar($elements[$key])) { Chris@0: $elements[$key] = $this->xssFilterAdminIfUnsafe($elements[$key]); Chris@0: } Chris@0: } Chris@0: } Chris@0: Chris@0: // Call the element's #theme function if it is set. Then any children of the Chris@0: // element have to be rendered there. If the internal #render_children Chris@0: // property is set, do not call the #theme function to prevent infinite Chris@0: // recursion. Chris@0: if ($theme_is_implemented && !isset($elements['#render_children'])) { Chris@0: $elements['#children'] = $this->theme->render($elements['#theme'], $elements); Chris@0: Chris@0: // If ThemeManagerInterface::render() returns FALSE this means that the Chris@0: // hook in #theme was not found in the registry and so we need to update Chris@0: // our flag accordingly. This is common for theme suggestions. Chris@0: $theme_is_implemented = ($elements['#children'] !== FALSE); Chris@0: } Chris@0: Chris@0: // If #theme is not implemented or #render_children is set and the element Chris@0: // has an empty #children attribute, render the children now. This is the Chris@0: // same process as Renderer::render() but is inlined for speed. Chris@0: if ((!$theme_is_implemented || isset($elements['#render_children'])) && empty($elements['#children'])) { Chris@0: foreach ($children as $key) { Chris@0: $elements['#children'] .= $this->doRender($elements[$key]); Chris@0: } Chris@0: $elements['#children'] = Markup::create($elements['#children']); Chris@0: } Chris@0: Chris@0: // If #theme is not implemented and the element has raw #markup as a Chris@0: // fallback, prepend the content in #markup to #children. In this case Chris@0: // #children will contain whatever is provided by #pre_render prepended to Chris@0: // what is rendered recursively above. If #theme is implemented then it is Chris@0: // the responsibility of that theme implementation to render #markup if Chris@0: // required. Eventually #theme_wrappers will expect both #markup and Chris@0: // #children to be a single string as #children. Chris@0: if (!$theme_is_implemented && isset($elements['#markup'])) { Chris@0: $elements['#children'] = Markup::create($elements['#markup'] . $elements['#children']); Chris@0: } Chris@0: Chris@0: // Let the theme functions in #theme_wrappers add markup around the rendered Chris@0: // children. Chris@0: // #states and #attached have to be processed before #theme_wrappers, Chris@0: // because the #type 'page' render array from drupal_prepare_page() would Chris@0: // render the $page and wrap it into the html.html.twig template without the Chris@0: // attached assets otherwise. Chris@0: // If the internal #render_children property is set, do not call the Chris@0: // #theme_wrappers function(s) to prevent infinite recursion. Chris@0: if (isset($elements['#theme_wrappers']) && !isset($elements['#render_children'])) { Chris@0: foreach ($elements['#theme_wrappers'] as $key => $value) { Chris@0: // If the value of a #theme_wrappers item is an array then the theme Chris@0: // hook is found in the key of the item and the value contains attribute Chris@0: // overrides. Attribute overrides replace key/value pairs in $elements Chris@0: // for only this ThemeManagerInterface::render() call. This allows Chris@0: // #theme hooks and #theme_wrappers hooks to share variable names Chris@0: // without conflict or ambiguity. Chris@0: $wrapper_elements = $elements; Chris@0: if (is_string($key)) { Chris@0: $wrapper_hook = $key; Chris@0: foreach ($value as $attribute => $override) { Chris@0: $wrapper_elements[$attribute] = $override; Chris@0: } Chris@0: } Chris@0: else { Chris@0: $wrapper_hook = $value; Chris@0: } Chris@0: Chris@0: $elements['#children'] = $this->theme->render($wrapper_hook, $wrapper_elements); Chris@0: } Chris@0: } Chris@0: Chris@0: // Filter the outputted content and make any last changes before the content Chris@0: // is sent to the browser. The changes are made on $content which allows the Chris@0: // outputted text to be filtered. Chris@0: if (isset($elements['#post_render'])) { Chris@0: foreach ($elements['#post_render'] as $callable) { Chris@0: if (is_string($callable) && strpos($callable, '::') === FALSE) { Chris@0: $callable = $this->controllerResolver->getControllerFromDefinition($callable); Chris@0: } Chris@0: $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); Chris@0: } Chris@0: } Chris@0: Chris@0: // We store the resulting output in $elements['#markup'], to be consistent Chris@0: // with how render cached output gets stored. This ensures that placeholder Chris@0: // replacement logic gets the same data to work with, no matter if #cache is Chris@12: // disabled, #cache is enabled, there is a cache hit or miss. If Chris@12: // #render_children is set the #prefix and #suffix will have already been Chris@12: // added. Chris@12: if (isset($elements['#render_children'])) { Chris@12: $elements['#markup'] = Markup::create($elements['#children']); Chris@12: } Chris@12: else { Chris@12: $prefix = isset($elements['#prefix']) ? $this->xssFilterAdminIfUnsafe($elements['#prefix']) : ''; Chris@12: $suffix = isset($elements['#suffix']) ? $this->xssFilterAdminIfUnsafe($elements['#suffix']) : ''; Chris@12: $elements['#markup'] = Markup::create($prefix . $elements['#children'] . $suffix); Chris@12: } Chris@0: Chris@0: // We've rendered this element (and its subtree!), now update the context. Chris@0: $context->update($elements); Chris@0: Chris@0: // Cache the processed element if both $pre_bubbling_elements and $elements Chris@0: // have the metadata necessary to generate a cache ID. Chris@0: if (isset($pre_bubbling_elements['#cache']['keys']) && isset($elements['#cache']['keys'])) { Chris@0: if ($pre_bubbling_elements['#cache']['keys'] !== $elements['#cache']['keys']) { Chris@0: throw new \LogicException('Cache keys may not be changed after initial setup. Use the contexts property instead to bubble additional metadata.'); Chris@0: } Chris@0: $this->renderCache->set($elements, $pre_bubbling_elements); Chris@0: // Update the render context; the render cache implementation may update Chris@0: // the element, and it may have different bubbleable metadata now. Chris@0: // @see \Drupal\Core\Render\PlaceholderingRenderCache::set() Chris@0: $context->pop(); Chris@0: $context->push(new BubbleableMetadata()); Chris@0: $context->update($elements); Chris@0: } Chris@0: Chris@0: // Only when we're in a root (non-recursive) Renderer::render() call, Chris@0: // placeholders must be processed, to prevent breaking the render cache in Chris@0: // case of nested elements with #cache set. Chris@0: // Chris@0: // By running them here, we ensure that: Chris@0: // - they run when #cache is disabled, Chris@0: // - they run when #cache is enabled and there is a cache miss. Chris@0: // Only the case of a cache hit when #cache is enabled, is not handled here, Chris@0: // that is handled earlier in Renderer::render(). Chris@0: if ($is_root_call) { Chris@0: $this->replacePlaceholders($elements); Chris@0: // @todo remove as part of https://www.drupal.org/node/2511330. Chris@0: if ($context->count() !== 1) { Chris@0: throw new \LogicException('A stray drupal_render() invocation with $is_root_call = TRUE is causing bubbling of attached assets to break.'); Chris@0: } Chris@0: } Chris@0: Chris@0: // Rendering is finished, all necessary info collected! Chris@0: $context->bubble(); Chris@0: Chris@0: $elements['#printed'] = TRUE; Chris@0: return $elements['#markup']; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function hasRenderContext() { Chris@0: return (bool) $this->getCurrentRenderContext(); Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function executeInRenderContext(RenderContext $context, callable $callable) { Chris@0: // Store the current render context. Chris@0: $previous_context = $this->getCurrentRenderContext(); Chris@0: Chris@0: // Set the provided context and call the callable, it will use that context. Chris@0: $this->setCurrentRenderContext($context); Chris@0: $result = $callable(); Chris@0: // @todo Convert to an assertion in https://www.drupal.org/node/2408013 Chris@0: if ($context->count() > 1) { Chris@0: throw new \LogicException('Bubbling failed.'); Chris@0: } Chris@0: Chris@0: // Restore the original render context. Chris@0: $this->setCurrentRenderContext($previous_context); Chris@0: Chris@0: return $result; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Returns the current render context. Chris@0: * Chris@0: * @return \Drupal\Core\Render\RenderContext Chris@0: * The current render context. Chris@0: */ Chris@0: protected function getCurrentRenderContext() { Chris@0: $request = $this->requestStack->getCurrentRequest(); Chris@0: return isset(static::$contextCollection[$request]) ? static::$contextCollection[$request] : NULL; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Sets the current render context. Chris@0: * Chris@0: * @param \Drupal\Core\Render\RenderContext|null $context Chris@0: * The render context. This can be NULL for instance when restoring the Chris@0: * original render context, which is in fact NULL. Chris@0: * Chris@0: * @return $this Chris@0: */ Chris@0: protected function setCurrentRenderContext(RenderContext $context = NULL) { Chris@0: $request = $this->requestStack->getCurrentRequest(); Chris@0: static::$contextCollection[$request] = $context; Chris@0: return $this; Chris@0: } Chris@0: Chris@0: /** Chris@0: * Replaces placeholders. Chris@0: * Chris@0: * Placeholders may have: Chris@0: * - #lazy_builder callback, to build a render array to be rendered into Chris@0: * markup that can replace the placeholder Chris@0: * - #cache: to cache the result of the placeholder Chris@0: * Chris@0: * Also merges the bubbleable metadata resulting from the rendering of the Chris@0: * contents of the placeholders. Hence $elements will be contain the entirety Chris@0: * of bubbleable metadata. Chris@0: * Chris@0: * @param array &$elements Chris@0: * The structured array describing the data being rendered. Including the Chris@0: * bubbleable metadata associated with the markup that replaced the Chris@0: * placeholders. Chris@0: * Chris@0: * @returns bool Chris@0: * Whether placeholders were replaced. Chris@0: * Chris@0: * @see \Drupal\Core\Render\Renderer::renderPlaceholder() Chris@0: */ Chris@0: protected function replacePlaceholders(array &$elements) { Chris@0: if (!isset($elements['#attached']['placeholders']) || empty($elements['#attached']['placeholders'])) { Chris@0: return FALSE; Chris@0: } Chris@0: Chris@0: // The 'status messages' placeholder needs to be special cased, because it Chris@0: // depends on global state that can be modified when other placeholders are Chris@0: // being rendered: any code can add messages to render. Chris@0: // This violates the principle that each lazy builder must be able to render Chris@0: // itself in isolation, and therefore in any order. However, we cannot Chris@17: // change the way \Drupal\Core\Messenger\Messenger works in the Drupal 8 Chris@17: // cycle. So we have to accommodate its special needs. Chris@0: // Allowing placeholders to be rendered in a particular order (in this case: Chris@0: // last) would violate this isolation principle. Thus a monopoly is granted Chris@0: // to this one special case, with this hard-coded solution. Chris@0: // @see \Drupal\Core\Render\Element\StatusMessages Chris@0: // @see https://www.drupal.org/node/2712935#comment-11368923 Chris@0: Chris@0: // First render all placeholders except 'status messages' placeholders. Chris@0: $message_placeholders = []; Chris@0: foreach ($elements['#attached']['placeholders'] as $placeholder => $placeholder_element) { Chris@0: if (isset($placeholder_element['#lazy_builder']) && $placeholder_element['#lazy_builder'][0] === 'Drupal\Core\Render\Element\StatusMessages::renderMessages') { Chris@0: $message_placeholders[] = $placeholder; Chris@0: } Chris@0: else { Chris@0: $elements = $this->renderPlaceholder($placeholder, $elements); Chris@0: } Chris@0: } Chris@0: Chris@0: // Then render 'status messages' placeholders. Chris@0: foreach ($message_placeholders as $message_placeholder) { Chris@0: $elements = $this->renderPlaceholder($message_placeholder, $elements); Chris@0: } Chris@0: Chris@0: return TRUE; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function mergeBubbleableMetadata(array $a, array $b) { Chris@0: $meta_a = BubbleableMetadata::createFromRenderArray($a); Chris@0: $meta_b = BubbleableMetadata::createFromRenderArray($b); Chris@0: $meta_a->merge($meta_b)->applyTo($a); Chris@0: return $a; Chris@0: } Chris@0: Chris@0: /** Chris@0: * {@inheritdoc} Chris@0: */ Chris@0: public function addCacheableDependency(array &$elements, $dependency) { Chris@0: $meta_a = CacheableMetadata::createFromRenderArray($elements); Chris@0: $meta_b = CacheableMetadata::createFromObject($dependency); Chris@0: $meta_a->merge($meta_b)->applyTo($elements); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Applies a very permissive XSS/HTML filter for admin-only use. Chris@0: * Chris@0: * Note: This method only filters if $string is not marked safe already. This Chris@0: * ensures that HTML intended for display is not filtered. Chris@0: * Chris@0: * @param string|\Drupal\Core\Render\Markup $string Chris@0: * A string. Chris@0: * Chris@0: * @return \Drupal\Core\Render\Markup Chris@0: * The escaped string wrapped in a Markup object. If the string is an Chris@0: * instance of \Drupal\Component\Render\MarkupInterface, it won't be escaped Chris@0: * again. Chris@0: */ Chris@0: protected function xssFilterAdminIfUnsafe($string) { Chris@0: if (!($string instanceof MarkupInterface)) { Chris@0: $string = Xss::filterAdmin($string); Chris@0: } Chris@0: return Markup::create($string); Chris@0: } Chris@0: Chris@0: /** Chris@0: * Escapes #plain_text or filters #markup as required. Chris@0: * Chris@0: * Drupal uses Twig's auto-escape feature to improve security. This feature Chris@0: * automatically escapes any HTML that is not known to be safe. Due to this Chris@0: * the render system needs to ensure that all markup it generates is marked Chris@0: * safe so that Twig does not do any additional escaping. Chris@0: * Chris@0: * By default all #markup is filtered to protect against XSS using the admin Chris@0: * tag list. Render arrays can alter the list of tags allowed by the filter Chris@0: * using the #allowed_tags property. This value should be an array of tags Chris@0: * that Xss::filter() would accept. Render arrays can escape text instead Chris@0: * of XSS filtering by setting the #plain_text property instead of #markup. If Chris@0: * #plain_text is used #allowed_tags is ignored. Chris@0: * Chris@0: * @param array $elements Chris@0: * A render array with #markup set. Chris@0: * Chris@0: * @return \Drupal\Component\Render\MarkupInterface|string Chris@0: * The escaped markup wrapped in a Markup object. If $elements['#markup'] Chris@0: * is an instance of \Drupal\Component\Render\MarkupInterface, it won't be Chris@0: * escaped or filtered again. Chris@0: * Chris@0: * @see \Drupal\Component\Utility\Html::escape() Chris@0: * @see \Drupal\Component\Utility\Xss::filter() Chris@0: * @see \Drupal\Component\Utility\Xss::filterAdmin() Chris@0: */ Chris@0: protected function ensureMarkupIsSafe(array $elements) { Chris@16: if (isset($elements['#plain_text'])) { Chris@0: $elements['#markup'] = Markup::create(Html::escape($elements['#plain_text'])); Chris@0: } Chris@0: elseif (!($elements['#markup'] instanceof MarkupInterface)) { Chris@0: // The default behaviour is to XSS filter using the admin tag list. Chris@0: $tags = isset($elements['#allowed_tags']) ? $elements['#allowed_tags'] : Xss::getAdminTagList(); Chris@0: $elements['#markup'] = Markup::create(Xss::filter($elements['#markup'], $tags)); Chris@0: } Chris@0: Chris@0: return $elements; Chris@0: } Chris@0: Chris@0: }