annotate core/lib/Drupal/Core/Render/Renderer.php @ 19:fa3358dc1485 tip

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