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 }
|