Renderer.php 31 KB

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