123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784 |
- <?php
- namespace Drupal\Core\Render;
- use Drupal\Component\Render\MarkupInterface;
- use Drupal\Component\Utility\Html;
- 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\Form\FormHelper;
- use Drupal\Core\Render\Element\RenderCallbackInterface;
- use Drupal\Core\Security\TrustedCallbackInterface;
- use Drupal\Core\Security\DoTrustedCallbackTrait;
- use Drupal\Core\Theme\ThemeManagerInterface;
- use Symfony\Component\HttpFoundation\RequestStack;
- /**
- * Turns a render array into a HTML string.
- */
- class Renderer implements RendererInterface {
- use DoTrustedCallbackTrait;
- /**
- * 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'])) {
- $elements['#access'] = $this->doCallback('#access_callback', $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 its
- // 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',
- // The keys below are not actually supported, but these are added
- // automatically by the Renderer. Adding them as though they are
- // supported allows us to avoid throwing an exception 100% of the time.
- '#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.
- // @todo remove the isMethodCacheable() check when
- // https://www.drupal.org/node/2367555 lands.
- if (isset($elements['#create_placeholder']) && $elements['#create_placeholder'] === TRUE && $this->requestStack->getCurrentRequest()->isMethodCacheable()) {
- 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'])) {
- $new_elements = $this->doCallback('#lazy_builder', $elements['#lazy_builder'][0], $elements['#lazy_builder'][1]);
- // 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;
- }
- // 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) {
- $elements = $this->doCallback('#pre_render', $callable, [$elements]);
- }
- }
- // All render elements support #markup and #plain_text.
- if (isset($elements['#markup']) || isset($elements['#plain_text'])) {
- $elements = $this->ensureMarkupIsSafe($elements);
- }
- // Defaults for bubbleable rendering metadata.
- $elements['#cache']['tags'] = isset($elements['#cache']['tags']) ? $elements['#cache']['tags'] : [];
- $elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT;
- $elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : [];
- // 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'])) {
- FormHelper::processStates($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 = [
- '#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) {
- $elements['#children'] = $this->doCallback('#post_render', $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. If
- // #render_children is set the #prefix and #suffix will have already been
- // added.
- if (isset($elements['#render_children'])) {
- $elements['#markup'] = Markup::create($elements['#children']);
- }
- else {
- $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;
- }
- // The 'status messages' placeholder needs to be special cased, because it
- // depends on global state that can be modified when other placeholders are
- // being rendered: any code can add messages to render.
- // This violates the principle that each lazy builder must be able to render
- // itself in isolation, and therefore in any order. However, we cannot
- // change the way \Drupal\Core\Messenger\Messenger works in the Drupal 8
- // cycle. So we have to accommodate its special needs.
- // Allowing placeholders to be rendered in a particular order (in this case:
- // last) would violate this isolation principle. Thus a monopoly is granted
- // to this one special case, with this hard-coded solution.
- // @see \Drupal\Core\Render\Element\StatusMessages
- // @see https://www.drupal.org/node/2712935#comment-11368923
- // First render all placeholders except 'status messages' placeholders.
- $message_placeholders = [];
- foreach ($elements['#attached']['placeholders'] as $placeholder => $placeholder_element) {
- if (isset($placeholder_element['#lazy_builder']) && $placeholder_element['#lazy_builder'][0] === 'Drupal\Core\Render\Element\StatusMessages::renderMessages') {
- $message_placeholders[] = $placeholder;
- }
- else {
- $elements = $this->renderPlaceholder($placeholder, $elements);
- }
- }
- // Then render 'status messages' placeholders.
- foreach ($message_placeholders as $message_placeholder) {
- $elements = $this->renderPlaceholder($message_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 the string is an
- * instance of \Drupal\Component\Render\MarkupInterface, it won't be escaped
- * again.
- */
- protected function xssFilterAdminIfUnsafe($string) {
- if (!($string instanceof MarkupInterface)) {
- $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 $elements['#markup']
- * is an instance of \Drupal\Component\Render\MarkupInterface, 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::filterAdmin()
- */
- protected function ensureMarkupIsSafe(array $elements) {
- if (isset($elements['#plain_text'])) {
- $elements['#markup'] = Markup::create(Html::escape($elements['#plain_text']));
- }
- elseif (!($elements['#markup'] instanceof MarkupInterface)) {
- // The default behavior 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;
- }
- /**
- * Performs a callback.
- *
- * @param string $callback_type
- * The type of the callback. For example, '#post_render'.
- * @param string|callable $callback
- * The callback to perform.
- * @param array $args
- * The arguments to pass to the callback.
- *
- * @return mixed
- * The callback's return value.
- *
- * @see \Drupal\Core\Security\TrustedCallbackInterface
- */
- protected function doCallback($callback_type, $callback, array $args) {
- if (is_string($callback)) {
- $double_colon = strpos($callback, '::');
- if ($double_colon === FALSE) {
- $callback = $this->controllerResolver->getControllerFromDefinition($callback);
- }
- elseif ($double_colon > 0) {
- $callback = explode('::', $callback, 2);
- }
- }
- $message = sprintf('Render %s callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. Support for this callback implementation is deprecated in 8.8.0 and will be removed in Drupal 9.0.0. See https://www.drupal.org/node/2966725', $callback_type, '%s');
- // Add \Drupal\Core\Render\Element\RenderCallbackInterface as an extra
- // trusted interface so that:
- // - All public methods on Render elements are considered trusted.
- // - Helper classes that contain only callback methods can implement this
- // instead of TrustedCallbackInterface.
- return $this->doTrustedCallback($callback, $args, $message, TrustedCallbackInterface::TRIGGER_SILENCED_DEPRECATION, RenderCallbackInterface::class);
- }
- }
|