Security update for Core, with self-updated composer
[yaffs-website] / web / core / tests / Drupal / Tests / Core / Render / RendererPlaceholdersTest.php
1 <?php
2
3 /**
4  * @file
5  * Contains \Drupal\Tests\Core\Render\RendererPlaceholdersTest.
6  */
7
8 namespace Drupal\Tests\Core\Render;
9
10 use Drupal\Component\Utility\Crypt;
11 use Drupal\Component\Utility\Html;
12 use Drupal\Core\Cache\Cache;
13 use Drupal\Core\Render\Markup;
14 use Drupal\Core\Render\RenderContext;
15
16 /**
17  * @coversDefaultClass \Drupal\Core\Render\Renderer
18  * @covers \Drupal\Core\Render\RenderCache
19  * @covers \Drupal\Core\Render\PlaceholderingRenderCache
20  * @group Render
21  */
22 class RendererPlaceholdersTest extends RendererTestBase {
23
24   /**
25    * {@inheritdoc}
26    */
27   protected function setUp() {
28     // Disable the required cache contexts, so that this test can test just the
29     // placeholder replacement behavior.
30     $this->rendererConfig['required_cache_contexts'] = [];
31
32     parent::setUp();
33   }
34
35   /**
36    * Provides the two classes of placeholders: cacheable and uncacheable.
37    *
38    * i.e. with or without #cache[keys].
39    *
40    * Also, different types:
41    * - A) automatically generated placeholder
42    *   - 1) manually triggered (#create_placeholder = TRUE)
43    *   - 2) automatically triggered (based on max-age = 0 at the top level)
44    *   - 3) automatically triggered (based on high cardinality cache contexts at
45    *        the top level)
46    *   - 4) automatically triggered (based on high-invalidation frequency cache
47    *        tags at the top level)
48    *   - 5) automatically triggered (based on max-age = 0 in its subtree, i.e.
49    *        via bubbling)
50    *   - 6) automatically triggered (based on high cardinality cache contexts in
51    *        its subtree, i.e. via bubbling)
52    *   - 7) automatically triggered (based on high-invalidation frequency cache
53    *        tags in its subtree, i.e. via bubbling)
54    * - B) manually generated placeholder
55    *
56    * So, in total 2*8 = 16 permutations. (On one axis: uncacheable vs.
57    * uncacheable = 2; on the other axis: A1–7 and B = 8.)
58    *
59    * @todo Case A5 is not yet supported by core. So that makes for only 14
60    *   permutations currently, instead of 16. That will be done in
61    *   https://www.drupal.org/node/2559847
62    *
63    * @return array
64    */
65   public function providerPlaceholders() {
66     $args = [$this->randomContextValue()];
67
68     $generate_placeholder_markup = function ($cache_keys = NULL) use ($args) {
69       $token_render_array = [
70         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
71       ];
72       if (is_array($cache_keys)) {
73         $token_render_array['#cache']['keys'] = $cache_keys;
74       }
75       $token = Crypt::hashBase64(serialize($token_render_array));
76       // \Drupal\Core\Render\Markup::create() is necessary as the render
77       // system would mangle this markup. As this is exactly what happens at
78       // runtime this is a valid use-case.
79       return Markup::create('<drupal-render-placeholder callback="Drupal\Tests\Core\Render\PlaceholdersTest::callback" arguments="' . '0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>');
80     };
81
82     $extract_placeholder_render_array = function ($placeholder_render_array) {
83       return array_intersect_key($placeholder_render_array, ['#lazy_builder' => TRUE, '#cache' => TRUE]);
84     };
85
86     // Note the presence of '#create_placeholder'.
87     $base_element_a1 = [
88       '#attached' => [
89         'drupalSettings' => [
90           'foo' => 'bar',
91         ],
92       ],
93       'placeholder' => [
94         '#cache' => [
95           'contexts' => [],
96         ],
97         '#create_placeholder' => TRUE,
98         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
99       ],
100     ];
101     // Note the absence of '#create_placeholder', presence of max-age=0 at the
102     // top level.
103     $base_element_a2 = [
104       '#attached' => [
105         'drupalSettings' => [
106           'foo' => 'bar',
107         ],
108       ],
109       'placeholder' => [
110         '#cache' => [
111           'contexts' => [],
112           'max-age' => 0,
113         ],
114         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
115       ],
116     ];
117     // Note the absence of '#create_placeholder', presence of high cardinality
118     // cache context at the top level.
119     $base_element_a3 = [
120       '#attached' => [
121         'drupalSettings' => [
122           'foo' => 'bar',
123         ],
124       ],
125       'placeholder' => [
126         '#cache' => [
127           'contexts' => ['user'],
128         ],
129         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
130       ],
131     ];
132     // Note the absence of '#create_placeholder', presence of high-invalidation
133     // frequency cache tag at the top level.
134     $base_element_a4 = [
135       '#attached' => [
136         'drupalSettings' => [
137           'foo' => 'bar',
138         ],
139       ],
140       'placeholder' => [
141         '#cache' => [
142           'contexts' => [],
143           'tags' => ['current-temperature'],
144         ],
145         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
146       ],
147     ];
148     // Note the absence of '#create_placeholder', presence of max-age=0 created
149     // by the #lazy_builder callback.
150     // @todo in https://www.drupal.org/node/2559847
151     $base_element_a5 = [];
152     // Note the absence of '#create_placeholder', presence of high cardinality
153     // cache context created by the #lazy_builder callback.
154     // @see \Drupal\Tests\Core\Render\PlaceholdersTest::callbackPerUser()
155     $base_element_a6 = [
156       '#attached' => [
157         'drupalSettings' => [
158           'foo' => 'bar',
159         ],
160       ],
161       'placeholder' => [
162         '#cache' => [
163           'contexts' => [],
164         ],
165         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callbackPerUser', $args],
166       ],
167     ];
168     // Note the absence of '#create_placeholder', presence of high-invalidation
169     // frequency cache tag created by the #lazy_builder callback.
170     // @see \Drupal\Tests\Core\Render\PlaceholdersTest::callbackTagCurrentTemperature()
171     $base_element_a7 = [
172       '#attached' => [
173         'drupalSettings' => [
174           'foo' => 'bar',
175         ],
176       ],
177       'placeholder' => [
178         '#cache' => [
179           'contexts' => [],
180         ],
181         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callbackTagCurrentTemperature', $args],
182       ],
183     ];
184     // Note the absence of '#create_placeholder', but the presence of
185     // '#attached[placeholders]'.
186     $base_element_b = [
187       '#markup' => $generate_placeholder_markup(),
188       '#attached' => [
189         'drupalSettings' => [
190           'foo' => 'bar',
191         ],
192         'placeholders' => [
193           (string) $generate_placeholder_markup() => [
194             '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
195           ],
196         ],
197       ],
198     ];
199
200     $keys = ['placeholder', 'output', 'can', 'be', 'render', 'cached', 'too'];
201
202     $cases = [];
203
204     // Case one: render array that has a placeholder that is:
205     // - automatically created, but manually triggered (#create_placeholder = TRUE)
206     // - uncacheable
207     $element_without_cache_keys = $base_element_a1;
208     $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a1['placeholder']);
209     $cases[] = [
210       $element_without_cache_keys,
211       $args,
212       $expected_placeholder_render_array,
213       FALSE,
214       [],
215       [],
216       [],
217     ];
218
219     // Case two: render array that has a placeholder that is:
220     // - automatically created, but manually triggered (#create_placeholder = TRUE)
221     // - cacheable
222     $element_with_cache_keys = $base_element_a1;
223     $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
224     $expected_placeholder_render_array['#cache']['keys'] = $keys;
225     $cases[] = [
226       $element_with_cache_keys,
227       $args,
228       $expected_placeholder_render_array,
229       $keys,
230       [],
231       [],
232       [
233         '#markup' => '<p>This is a rendered placeholder!</p>',
234         '#attached' => [
235           'drupalSettings' => [
236             'dynamic_animal' => $args[0],
237           ],
238         ],
239         '#cache' => [
240           'contexts' => [],
241           'tags' => [],
242           'max-age' => Cache::PERMANENT,
243         ],
244       ],
245     ];
246
247     // Case three: render array that has a placeholder that is:
248     // - automatically created, and automatically triggered due to max-age=0
249     // - uncacheable
250     $element_without_cache_keys = $base_element_a2;
251     $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a2['placeholder']);
252     $cases[] = [
253       $element_without_cache_keys,
254       $args,
255       $expected_placeholder_render_array,
256       FALSE,
257       [],
258       [],
259       [],
260     ];
261
262     // Case four: render array that has a placeholder that is:
263     // - automatically created, but automatically triggered due to max-age=0
264     // - cacheable
265     $element_with_cache_keys = $base_element_a2;
266     $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
267     $expected_placeholder_render_array['#cache']['keys'] = $keys;
268     $cases[] = [
269       $element_with_cache_keys,
270       $args,
271       $expected_placeholder_render_array,
272       FALSE,
273       [],
274       [],
275       [],
276     ];
277
278     // Case five: render array that has a placeholder that is:
279     // - automatically created, and automatically triggered due to high
280     //   cardinality cache contexts
281     // - uncacheable
282     $element_without_cache_keys = $base_element_a3;
283     $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a3['placeholder']);
284     $cases[] = [
285       $element_without_cache_keys,
286       $args,
287       $expected_placeholder_render_array,
288       FALSE,
289       [],
290       [],
291       [],
292     ];
293
294     // Case six: render array that has a placeholder that is:
295     // - automatically created, and automatically triggered due to high
296     //   cardinality cache contexts
297     // - cacheable
298     $element_with_cache_keys = $base_element_a3;
299     $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
300     $expected_placeholder_render_array['#cache']['keys'] = $keys;
301     // The CID parts here consist of the cache keys plus the 'user' cache
302     // context, which in this unit test is simply the given cache context token,
303     // see \Drupal\Tests\Core\Render\RendererTestBase::setUp().
304     $cid_parts = array_merge($keys, ['user']);
305     $cases[] = [
306       $element_with_cache_keys,
307       $args,
308       $expected_placeholder_render_array,
309       $cid_parts,
310       [],
311       [],
312       [
313         '#markup' => '<p>This is a rendered placeholder!</p>',
314         '#attached' => [
315           'drupalSettings' => [
316             'dynamic_animal' => $args[0],
317           ],
318         ],
319         '#cache' => [
320           'contexts' => ['user'],
321           'tags' => [],
322           'max-age' => Cache::PERMANENT,
323         ],
324       ],
325     ];
326
327     // Case seven: render array that has a placeholder that is:
328     // - automatically created, and automatically triggered due to high
329     //   invalidation frequency cache tags
330     // - uncacheable
331     $element_without_cache_keys = $base_element_a4;
332     $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a4['placeholder']);
333     $cases[] = [
334       $element_without_cache_keys,
335       $args,
336       $expected_placeholder_render_array,
337       FALSE,
338       [],
339       [],
340       [],
341     ];
342
343     // Case eight: render array that has a placeholder that is:
344     // - automatically created, and automatically triggered due to high
345     //   invalidation frequency cache tags
346     // - cacheable
347     $element_with_cache_keys = $base_element_a4;
348     $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
349     $expected_placeholder_render_array['#cache']['keys'] = $keys;
350     $cases[] = [
351       $element_with_cache_keys,
352       $args,
353       $expected_placeholder_render_array,
354       $keys,
355       [],
356       [],
357       [
358         '#markup' => '<p>This is a rendered placeholder!</p>',
359         '#attached' => [
360           'drupalSettings' => [
361             'dynamic_animal' => $args[0],
362           ],
363         ],
364         '#cache' => [
365           'contexts' => [],
366           'tags' => ['current-temperature'],
367           'max-age' => Cache::PERMANENT,
368         ],
369       ],
370     ];
371
372     // Case nine: render array that DOES NOT have a placeholder that is:
373     // - NOT created, despite max-age=0 that is bubbled
374     // - uncacheable
375     // (because the render element with #lazy_builder does not have #cache[keys]
376     // and hence the max-age=0 bubbles up further)
377     // @todo in https://www.drupal.org/node/2559847
378
379     // Case ten: render array that has a placeholder that is:
380     // - automatically created, and automatically triggered due to max-age=0
381     //   that is bubbled
382     // - cacheable
383     // @todo in https://www.drupal.org/node/2559847
384
385     // Case eleven: render array that DOES NOT have a placeholder that is:
386     // - NOT created, despite high cardinality cache contexts that are bubbled
387     // - uncacheable
388     $element_without_cache_keys = $base_element_a6;
389     $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a6['placeholder']);
390     $cases[] = [
391       $element_without_cache_keys,
392       $args,
393       $expected_placeholder_render_array,
394       FALSE,
395       ['user'],
396       [],
397       [],
398     ];
399
400     // Case twelve: render array that has a placeholder that is:
401     // - automatically created, and automatically triggered due to high
402     //   cardinality cache contexts that are bubbled
403     // - cacheable
404     $element_with_cache_keys = $base_element_a6;
405     $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
406     $expected_placeholder_render_array['#cache']['keys'] = $keys;
407     $cases[] = [
408       $element_with_cache_keys,
409       $args,
410       $expected_placeholder_render_array,
411       $keys,
412       ['user'],
413       [],
414       [
415         '#markup' => '<p>This is a rendered placeholder!</p>',
416         '#attached' => [
417           'drupalSettings' => [
418             'dynamic_animal' => $args[0],
419           ],
420         ],
421         '#cache' => [
422           'contexts' => ['user'],
423           'tags' => [],
424           'max-age' => Cache::PERMANENT,
425         ],
426       ],
427     ];
428
429     // Case thirteen: render array that has a placeholder that is:
430     // - automatically created, and automatically triggered due to high
431     //   invalidation frequency cache tags that are bubbled
432     // - uncacheable
433     $element_without_cache_keys = $base_element_a7;
434     $expected_placeholder_render_array = $extract_placeholder_render_array($base_element_a7['placeholder']);
435     $cases[] = [
436       $element_without_cache_keys,
437       $args,
438       $expected_placeholder_render_array,
439       FALSE,
440       [],
441       ['current-temperature'],
442       [],
443     ];
444
445     // Case fourteen: render array that has a placeholder that is:
446     // - automatically created, and automatically triggered due to high
447     //   invalidation frequency cache tags that are bubbled
448     // - cacheable
449     $element_with_cache_keys = $base_element_a7;
450     $element_with_cache_keys['placeholder']['#cache']['keys'] = $keys;
451     $expected_placeholder_render_array['#cache']['keys'] = $keys;
452     $cases[] = [
453       $element_with_cache_keys,
454       $args,
455       $expected_placeholder_render_array,
456       $keys,
457       [],
458       [],
459       [
460         '#markup' => '<p>This is a rendered placeholder!</p>',
461         '#attached' => [
462           'drupalSettings' => [
463             'dynamic_animal' => $args[0],
464           ],
465         ],
466         '#cache' => [
467           'contexts' => [],
468           'tags' => ['current-temperature'],
469           'max-age' => Cache::PERMANENT,
470         ],
471       ],
472     ];
473
474     // Case fifteen: render array that has a placeholder that is:
475     // - manually created
476     // - uncacheable
477     $x = $base_element_b;
478     $expected_placeholder_render_array = $x['#attached']['placeholders'][(string) $generate_placeholder_markup()];
479     unset($x['#attached']['placeholders'][(string) $generate_placeholder_markup()]['#cache']);
480     $cases[] = [
481       $x,
482       $args,
483       $expected_placeholder_render_array,
484       FALSE,
485       [],
486       [],
487       [],
488     ];
489
490     // Case sixteen: render array that has a placeholder that is:
491     // - manually created
492     // - cacheable
493     $x = $base_element_b;
494     $x['#markup'] = $placeholder_markup = $generate_placeholder_markup($keys);
495     $placeholder_markup = (string) $placeholder_markup;
496     $x['#attached']['placeholders'] = [
497       $placeholder_markup => [
498         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
499         '#cache' => ['keys' => $keys],
500       ],
501     ];
502     $expected_placeholder_render_array = $x['#attached']['placeholders'][$placeholder_markup];
503     $cases[] = [
504       $x,
505       $args,
506       $expected_placeholder_render_array,
507       $keys,
508       [],
509       [],
510       [
511         '#markup' => '<p>This is a rendered placeholder!</p>',
512         '#attached' => [
513           'drupalSettings' => [
514             'dynamic_animal' => $args[0],
515           ],
516         ],
517         '#cache' => [
518           'contexts' => [],
519           'tags' => [],
520           'max-age' => Cache::PERMANENT,
521         ],
522       ],
523     ];
524
525     return $cases;
526   }
527
528   /**
529    * Generates an element with a placeholder.
530    *
531    * @return array
532    *   An array containing:
533    *   - A render array containing a placeholder.
534    *   - The context used for that #lazy_builder callback.
535    */
536   protected function generatePlaceholderElement() {
537     $args = [$this->randomContextValue()];
538     $test_element = [];
539     $test_element['#attached']['drupalSettings']['foo'] = 'bar';
540     $test_element['placeholder']['#cache']['keys'] = ['placeholder', 'output', 'can', 'be', 'render', 'cached', 'too'];
541     $test_element['placeholder']['#cache']['contexts'] = [];
542     $test_element['placeholder']['#create_placeholder'] = TRUE;
543     $test_element['placeholder']['#lazy_builder'] = ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args];
544
545     return [$test_element, $args];
546   }
547
548   /**
549    * @param false|array $cid_parts
550    * @param string[] $bubbled_cache_contexts
551    *   Additional cache contexts that were bubbled when the placeholder was
552    *   rendered.
553    * @param array $expected_data
554    *   A render array with the expected values.
555    */
556   protected function assertPlaceholderRenderCache($cid_parts, array $bubbled_cache_contexts, array $expected_data) {
557     if ($cid_parts !== FALSE) {
558       if ($bubbled_cache_contexts) {
559         // Verify render cached placeholder.
560         $cached_element = $this->memoryCache->get(implode(':', $cid_parts))->data;
561         $expected_redirect_element = [
562           '#cache_redirect' => TRUE,
563           '#cache' => $expected_data['#cache'] + [
564             'keys' => $cid_parts,
565             'bin' => 'render',
566           ],
567         ];
568         $this->assertEquals($expected_redirect_element, $cached_element, 'The correct cache redirect exists.');
569       }
570
571       // Verify render cached placeholder.
572       $cached = $this->memoryCache->get(implode(':', array_merge($cid_parts, $bubbled_cache_contexts)));
573       $cached_element = $cached->data;
574       $this->assertEquals($expected_data, $cached_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by the placeholder being replaced.');
575     }
576   }
577   /**
578    * @covers ::render
579    * @covers ::doRender
580    *
581    * @dataProvider providerPlaceholders
582    */
583   public function testUncacheableParent($element, $args, array $expected_placeholder_render_array, $placeholder_cid_parts, array $bubbled_cache_contexts, array $bubbled_cache_tags, array $placeholder_expected_render_cache_array) {
584     if ($placeholder_cid_parts) {
585       $this->setupMemoryCache();
586     }
587     else {
588       $this->setUpUnusedCache();
589     }
590
591     $this->setUpRequest('GET');
592
593     // No #cache on parent element.
594     $element['#prefix'] = '<p>#cache disabled</p>';
595     $output = $this->renderer->renderRoot($element);
596     $this->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
597     $this->assertSame('<p>#cache disabled</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
598     $expected_js_settings = [
599       'foo' => 'bar',
600       'dynamic_animal' => $args[0],
601     ];
602     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
603     $this->assertPlaceholderRenderCache($placeholder_cid_parts, $bubbled_cache_contexts, $placeholder_expected_render_cache_array);
604   }
605
606   /**
607    * @covers ::render
608    * @covers ::doRender
609    * @covers \Drupal\Core\Render\RenderCache::get
610    * @covers \Drupal\Core\Render\RenderCache::set
611    * @covers \Drupal\Core\Render\RenderCache::createCacheID
612    *
613    * @dataProvider providerPlaceholders
614    */
615   public function testCacheableParent($test_element, $args, array $expected_placeholder_render_array, $placeholder_cid_parts, array $bubbled_cache_contexts, array $bubbled_cache_tags, array $placeholder_expected_render_cache_array) {
616     $element = $test_element;
617     $this->setupMemoryCache();
618
619     $this->setUpRequest('GET');
620
621     $token = Crypt::hashBase64(serialize($expected_placeholder_render_array));
622     $placeholder_callback = $expected_placeholder_render_array['#lazy_builder'][0];
623     $expected_placeholder_markup = '<drupal-render-placeholder callback="' . $placeholder_callback . '" arguments="0=' . $args[0] . '" token="' . $token . '"></drupal-render-placeholder>';
624     $this->assertSame($expected_placeholder_markup, Html::normalize($expected_placeholder_markup), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.');
625
626     // GET request: #cache enabled, cache miss.
627     $element['#cache'] = ['keys' => ['placeholder_test_GET']];
628     $element['#prefix'] = '<p>#cache enabled, GET</p>';
629     $output = $this->renderer->renderRoot($element);
630     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
631     $this->assertTrue(isset($element['#printed']), 'No cache hit');
632     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
633     $expected_js_settings = [
634       'foo' => 'bar',
635       'dynamic_animal' => $args[0],
636     ];
637     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
638     $this->assertPlaceholderRenderCache($placeholder_cid_parts, $bubbled_cache_contexts, $placeholder_expected_render_cache_array);
639
640     // GET request: validate cached data.
641     $cached = $this->memoryCache->get('placeholder_test_GET');
642     // There are three edge cases, where the shape of the render cache item for
643     // the parent (with CID 'placeholder_test_GET') is vastly different. These
644     // are the cases where:
645     // - the placeholder is uncacheable (because it has no #cache[keys]), and;
646     // - cacheability metadata that meets auto_placeholder_conditions is bubbled
647     $has_uncacheable_lazy_builder = !isset($test_element['placeholder']['#cache']['keys']) && isset($test_element['placeholder']['#lazy_builder']);
648     // Edge cases: always where both bubbling of an auto-placeholdering
649     // condition happens from within a #lazy_builder that is uncacheable.
650     // - uncacheable + A5 (cache max-age)
651     // @todo in https://www.drupal.org/node/2559847
652     // - uncacheable + A6 (cache context)
653     $edge_case_a6_uncacheable = $has_uncacheable_lazy_builder && $test_element['placeholder']['#lazy_builder'][0] === 'Drupal\Tests\Core\Render\PlaceholdersTest::callbackPerUser';
654     // - uncacheable + A7 (cache tag)
655     $edge_case_a7_uncacheable = $has_uncacheable_lazy_builder && $test_element['placeholder']['#lazy_builder'][0] === 'Drupal\Tests\Core\Render\PlaceholdersTest::callbackTagCurrentTemperature';
656     // The redirect-cacheable edge case: a high-cardinality cache context is
657     // bubbled from a #lazy_builder callback for an uncacheable placeholder. The
658     // element containing the uncacheable placeholder has cache keys set, and
659     // due to the bubbled cache contexts it creates a cache redirect.
660     if ($edge_case_a6_uncacheable) {
661       $cached_element = $cached->data;
662       $expected_redirect = [
663         '#cache_redirect' => TRUE,
664         '#cache' => [
665           'keys' => ['placeholder_test_GET'],
666           'contexts' => ['user'],
667           'tags' => [],
668           'max-age' => Cache::PERMANENT,
669           'bin' => 'render',
670         ],
671       ];
672       $this->assertEquals($expected_redirect, $cached_element);
673       // Follow the redirect.
674       $cached_element = $this->memoryCache->get('placeholder_test_GET:' . implode(':', $bubbled_cache_contexts))->data;
675       $expected_element = [
676         '#markup' => '<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>',
677         '#attached' => [
678           'drupalSettings' => [
679             'foo' => 'bar',
680             'dynamic_animal' => $args[0],
681           ],
682         ],
683         '#cache' => [
684           'contexts' => $bubbled_cache_contexts,
685           'tags' => [],
686           'max-age' => Cache::PERMANENT,
687         ],
688       ];
689       $this->assertEquals($expected_element, $cached_element, 'The parent is render cached with a redirect in ase a cache context is bubbled from an uncacheable child (no #cache[keys]) with a #lazy_builder.');
690     }
691     // The normally cacheable edge case: a high-invalidation frequency cache tag
692     // is bubbled from a #lazy_builder callback for an uncacheable placeholder.
693     // The element containing the uncacheable placeholder has cache keys set,
694     // and also has the bubbled cache tags.
695     elseif ($edge_case_a7_uncacheable) {
696       $cached_element = $cached->data;
697       $expected_element = [
698         '#markup' => '<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>',
699         '#attached' => [
700           'drupalSettings' => [
701             'foo' => 'bar',
702             'dynamic_animal' => $args[0],
703           ],
704         ],
705         '#cache' => [
706           'contexts' => [],
707           'tags' => $bubbled_cache_tags,
708           'max-age' => Cache::PERMANENT,
709         ],
710       ];
711       $this->assertEquals($expected_element, $cached_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by placeholder #lazy_builder callbacks.');
712     }
713     // The regular case.
714     else {
715       $cached_element = $cached->data;
716       $expected_element = [
717         '#markup' => '<p>#cache enabled, GET</p>' . $expected_placeholder_markup,
718         '#attached' => [
719           'drupalSettings' => [
720             'foo' => 'bar',
721           ],
722           'placeholders' => [
723             $expected_placeholder_markup => [
724               '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', $args],
725             ],
726           ],
727         ],
728         '#cache' => [
729           'contexts' => [],
730           'tags' => $bubbled_cache_tags,
731           'max-age' => Cache::PERMANENT,
732         ],
733       ];
734       $expected_element['#attached']['placeholders'][$expected_placeholder_markup] = $expected_placeholder_render_array;
735       $this->assertEquals($expected_element, $cached_element, 'The correct data is cached: the stored #markup and #attached properties are not affected by placeholder #lazy_builder callbacks.');
736     }
737
738     // GET request: #cache enabled, cache hit.
739     $element = $test_element;
740     $element['#cache'] = ['keys' => ['placeholder_test_GET']];
741     $element['#prefix'] = '<p>#cache enabled, GET</p>';
742     $output = $this->renderer->renderRoot($element);
743     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
744     $this->assertFalse(isset($element['#printed']), 'Cache hit');
745     $this->assertSame('<p>#cache enabled, GET</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
746     $expected_js_settings = [
747       'foo' => 'bar',
748       'dynamic_animal' => $args[0],
749     ];
750     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
751   }
752
753   /**
754    * @covers ::render
755    * @covers ::doRender
756    * @covers \Drupal\Core\Render\RenderCache::get
757    * @covers ::replacePlaceholders
758    *
759    * @dataProvider providerPlaceholders
760    */
761   public function testCacheableParentWithPostRequest($test_element, $args) {
762     $this->setUpUnusedCache();
763
764     // Verify behavior when handling a non-GET request, e.g. a POST request:
765     // also in that case, placeholders must be replaced.
766     $this->setUpRequest('POST');
767
768     // POST request: #cache enabled, cache miss.
769     $element = $test_element;
770     $element['#cache'] = ['keys' => ['placeholder_test_POST']];
771     $element['#prefix'] = '<p>#cache enabled, POST</p>';
772     $output = $this->renderer->renderRoot($element);
773     $this->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', (string) $output, 'Output is overridden.');
774     $this->assertTrue(isset($element['#printed']), 'No cache hit');
775     $this->assertSame('<p>#cache enabled, POST</p><p>This is a rendered placeholder!</p>', (string) $element['#markup'], '#markup is overridden.');
776     $expected_js_settings = [
777       'foo' => 'bar',
778       'dynamic_animal' => $args[0],
779     ];
780     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the one added by the placeholder #lazy_builder callback exist.');
781
782     // Even when the child element's placeholder is cacheable, it should not
783     // generate a render cache item.
784     $this->assertPlaceholderRenderCache(FALSE, [], []);
785   }
786
787   /**
788    * @covers ::render
789    * @covers ::doRender
790    * @covers \Drupal\Core\Render\RenderCache::get
791    * @covers \Drupal\Core\Render\PlaceholderingRenderCache::get
792    * @covers \Drupal\Core\Render\PlaceholderingRenderCache::set
793    * @covers ::replacePlaceholders
794    *
795    * @dataProvider providerPlaceholders
796    */
797   public function testPlaceholderingDisabledForPostRequests($test_element, $args) {
798     $this->setUpUnusedCache();
799     $this->setUpRequest('POST');
800
801     $element = $test_element;
802
803     // Render without replacing placeholders, to allow this test to see which
804     // #attached[placeholders] there are, if any.
805     $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$element) {
806       return $this->renderer->render($element);
807     });
808     // Only test cases where the placeholders have been specified manually are
809     // allowed to have placeholders. This means that of the different situations
810     // listed in providerPlaceholders(), only type B can have attached
811     // placeholders. Everything else, whether:
812     // 1. manual placeholdering
813     // 2. automatic placeholdering via already-present cacheability metadata
814     // 3. automatic placeholdering via bubbled cacheability metadata
815     // All three of those should NOT result in placeholders.
816     if (!isset($test_element['#attached']['placeholders'])) {
817       $this->assertFalse(isset($element['#attached']['placeholders']), 'No placeholders created.');
818     }
819   }
820
821   /**
822    * Tests a placeholder that adds another placeholder.
823    *
824    * E.g. when rendering a node in a placeholder the rendering of that node
825    * needs a placeholder of its own to be executed (to render the node links).
826    *
827    * @covers ::render
828    * @covers ::doRender
829    * @covers ::replacePlaceholders
830    */
831   public function testRecursivePlaceholder() {
832     $args = [$this->randomContextValue()];
833     $element = [];
834     $element['#create_placeholder'] = TRUE;
835     $element['#lazy_builder'] = ['Drupal\Tests\Core\Render\RecursivePlaceholdersTest::callback', $args];
836
837     $output = $this->renderer->renderRoot($element);
838     $this->assertEquals('<p>This is a rendered placeholder!</p>', $output, 'The output has been modified by the indirect, recursive placeholder #lazy_builder callback.');
839     $this->assertSame((string) $element['#markup'], '<p>This is a rendered placeholder!</p>', '#markup is overridden by the indirect, recursive placeholder #lazy_builder callback.');
840     $expected_js_settings = [
841       'dynamic_animal' => $args[0],
842     ];
843     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified by the indirect, recursive placeholder #lazy_builder callback.');
844   }
845
846   /**
847    * @covers ::render
848    * @covers ::doRender
849    */
850   public function testInvalidLazyBuilder() {
851     $element = [];
852     $element['#lazy_builder'] = '\Drupal\Tests\Core\Render\PlaceholdersTest::callback';
853
854     $this->setExpectedException(\DomainException::class, 'The #lazy_builder property must have an array as a value.');
855     $this->renderer->renderRoot($element);
856   }
857
858   /**
859    * @covers ::render
860    * @covers ::doRender
861    */
862   public function testInvalidLazyBuilderArguments() {
863     $element = [];
864     $element['#lazy_builder'] = ['\Drupal\Tests\Core\Render\PlaceholdersTest::callback', 'arg1', 'arg2'];
865
866     $this->setExpectedException(\DomainException::class, 'The #lazy_builder property must have an array as a value, containing two values: the callback, and the arguments for the callback.');
867     $this->renderer->renderRoot($element);
868   }
869
870   /**
871    * @covers ::render
872    * @covers ::doRender
873    *
874    * @see testNonScalarLazybuilderCallbackContext
875    */
876   public function testScalarLazybuilderCallbackContext() {
877     $element = [];
878     $element['#lazy_builder'] = [
879       '\Drupal\Tests\Core\Render\PlaceholdersTest::callback',
880       [
881         'string' => 'foo',
882         'bool' => TRUE,
883         'int' => 1337,
884         'float' => 3.14,
885         'null' => NULL,
886       ],
887     ];
888
889     $result = $this->renderer->renderRoot($element);
890     $this->assertInstanceOf('\Drupal\Core\Render\Markup', $result);
891     $this->assertEquals('<p>This is a rendered placeholder!</p>', (string) $result);
892   }
893
894   /**
895    * @covers ::render
896    * @covers ::doRender
897    */
898   public function testNonScalarLazybuilderCallbackContext() {
899     $element = [];
900     $element['#lazy_builder'] = [
901       '\Drupal\Tests\Core\Render\PlaceholdersTest::callback',
902       [
903         'string' => 'foo',
904         'bool' => TRUE,
905         'int' => 1337,
906         'float' => 3.14,
907         'null' => NULL,
908         // array is not one of the scalar types.
909         'array' => ['hi!'],
910       ],
911     ];
912
913     $this->setExpectedException(\DomainException::class, "A #lazy_builder callback's context may only contain scalar values or NULL.");
914     $this->renderer->renderRoot($element);
915   }
916
917   /**
918    * @covers ::render
919    * @covers ::doRender
920    */
921   public function testChildrenPlusBuilder() {
922     $element = [];
923     $element['#lazy_builder'] = ['Drupal\Tests\Core\Render\RecursivePlaceholdersTest::callback', []];
924     $element['child_a']['#markup'] = 'Oh hai!';
925     $element['child_b']['#markup'] = 'kthxbai';
926
927     $this->setExpectedException(\DomainException::class, '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: child_a, child_b.');
928     $this->renderer->renderRoot($element);
929   }
930
931   /**
932    * @covers ::render
933    * @covers ::doRender
934    */
935   public function testPropertiesPlusBuilder() {
936     $element = [];
937     $element['#lazy_builder'] = ['Drupal\Tests\Core\Render\RecursivePlaceholdersTest::callback', []];
938     $element['#llama'] = '#awesome';
939     $element['#piglet'] = '#cute';
940
941     $this->setExpectedException(\DomainException::class, '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: #llama, #piglet.');
942     $this->renderer->renderRoot($element);
943   }
944
945   /**
946    * @covers ::render
947    * @covers ::doRender
948    */
949   public function testCreatePlaceholderPropertyWithoutLazyBuilder() {
950     $element = [];
951     $element['#create_placeholder'] = TRUE;
952
953     $this->setExpectedException(\LogicException::class, 'When #create_placeholder is set, a #lazy_builder callback must be present as well.');
954     $this->renderer->renderRoot($element);
955   }
956
957   /**
958    * Create an element with a child and subchild. Each element has the same
959    * #lazy_builder callback, but with different contexts. They don't modify
960    * markup, only attach additional drupalSettings.
961    *
962    * @covers ::render
963    * @covers ::doRender
964    * @covers \Drupal\Core\Render\RenderCache::get
965    * @covers ::replacePlaceholders
966    */
967   public function testRenderChildrenPlaceholdersDifferentArguments() {
968     $this->setUpRequest();
969     $this->setupMemoryCache();
970     $this->cacheContextsManager->expects($this->any())
971       ->method('convertTokensToKeys')
972       ->willReturnArgument(0);
973     $this->controllerResolver->expects($this->any())
974       ->method('getControllerFromDefinition')
975       ->willReturnArgument(0);
976     $this->setupThemeManagerForDetails();
977
978     $args_1 = ['foo', TRUE];
979     $args_2 = ['bar', TRUE];
980     $args_3 = ['baz', TRUE];
981     $test_element = $this->generatePlaceholdersWithChildrenTestElement($args_1, $args_2, $args_3);
982
983     $element = $test_element;
984     $output = $this->renderer->renderRoot($element);
985     $expected_output = <<<HTML
986 <details>
987   <summary>Parent</summary>
988   <div class="details-wrapper"><details>
989   <summary>Child</summary>
990   <div class="details-wrapper">Subchild</div>
991 </details></div>
992 </details>
993 HTML;
994     $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
995     $this->assertTrue(isset($element['#printed']), 'No cache hit');
996     $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
997     $expected_js_settings = [
998       'foo' => 'bar',
999       'dynamic_animal' => [$args_1[0] => TRUE, $args_2[0] => TRUE, $args_3[0] => TRUE],
1000     ];
1001     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each placeholder #lazy_builder callback exist.');
1002
1003     // GET request: validate cached data.
1004     $cached_element = $this->memoryCache->get('simpletest:renderer:children_placeholders')->data;
1005     $expected_element = [
1006       '#attached' => [
1007         'drupalSettings' => [
1008           'foo' => 'bar',
1009         ],
1010         'placeholders' => [
1011           'parent-x-parent' => [
1012             '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_1],
1013           ],
1014           'child-x-child' => [
1015             '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_2],
1016           ],
1017           'subchild-x-subchild' => [
1018             '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_3],
1019           ],
1020         ],
1021       ],
1022       '#cache' => [
1023         'contexts' => [],
1024         'tags' => [],
1025         'max-age' => Cache::PERMANENT,
1026       ],
1027     ];
1028
1029     $dom = Html::load($cached_element['#markup']);
1030     $xpath = new \DOMXPath($dom);
1031     $parent = $xpath->query('//details/summary[text()="Parent"]')->length;
1032     $child = $xpath->query('//details/div[@class="details-wrapper"]/details/summary[text()="Child"]')->length;
1033     $subchild = $xpath->query('//details/div[@class="details-wrapper"]/details/div[@class="details-wrapper" and text()="Subchild"]')->length;
1034     $this->assertTrue($parent && $child && $subchild, 'The correct data is cached: the stored #markup is not affected by placeholder #lazy_builder callbacks.');
1035
1036     // Remove markup because it's compared above in the xpath.
1037     unset($cached_element['#markup']);
1038     $this->assertEquals($cached_element, $expected_element, 'The correct data is cached: the stored #attached properties are not affected by placeholder #lazy_builder callbacks.');
1039
1040     // GET request: #cache enabled, cache hit.
1041     $element = $test_element;
1042     $output = $this->renderer->renderRoot($element);
1043     $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
1044     $this->assertFalse(isset($element['#printed']), 'Cache hit');
1045     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each placeholder #lazy_builder callback exist.');
1046
1047     // Use the exact same element, but now unset #cache; ensure we get the same
1048     // result.
1049     unset($test_element['#cache']);
1050     $element = $test_element;
1051     $output = $this->renderer->renderRoot($element);
1052     $this->assertSame($expected_output, (string) $output, 'Output is not overridden.');
1053     $this->assertSame($expected_output, (string) $element['#markup'], '#markup is not overridden.');
1054     $this->assertSame($element['#attached']['drupalSettings'], $expected_js_settings, '#attached is modified; both the original JavaScript setting and the ones added by each #lazy_builder callback exist.');
1055   }
1056
1057   /**
1058    * Generates an element with placeholders at 3 levels.
1059    *
1060    * @param array $args_1
1061    *   The arguments for the placeholder at level 1.
1062    * @param array $args_2
1063    *   The arguments for the placeholder at level 2.
1064    * @param array $args_3
1065    *   The arguments for the placeholder at level 3.
1066    *
1067    * @return array
1068    *   The generated render array for testing.
1069    */
1070   protected function generatePlaceholdersWithChildrenTestElement(array $args_1, array $args_2, array $args_3) {
1071     $test_element = [
1072       '#type' => 'details',
1073       '#cache' => [
1074         'keys' => ['simpletest', 'renderer', 'children_placeholders'],
1075       ],
1076       '#title' => 'Parent',
1077       '#attached' => [
1078         'drupalSettings' => [
1079           'foo' => 'bar',
1080         ],
1081         'placeholders' => [
1082           'parent-x-parent' => [
1083             '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_1],
1084           ],
1085         ],
1086       ],
1087     ];
1088     $test_element['child'] = [
1089       '#type' => 'details',
1090       '#attached' => [
1091         'placeholders' => [
1092           'child-x-child' => [
1093             '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_2],
1094           ],
1095         ],
1096       ],
1097       '#title' => 'Child',
1098     ];
1099     $test_element['child']['subchild'] = [
1100       '#attached' => [
1101         'placeholders' => [
1102           'subchild-x-subchild' => [
1103             '#lazy_builder' => [__NAMESPACE__ . '\\PlaceholdersTest::callback', $args_3],
1104           ],
1105         ],
1106       ],
1107       '#markup' => 'Subchild',
1108     ];
1109     return $test_element;
1110   }
1111
1112   /**
1113    * @return \Drupal\Core\Theme\ThemeManagerInterface|\PHPUnit_Framework_MockObject_Builder_InvocationMocker
1114    */
1115   protected function setupThemeManagerForDetails() {
1116     return $this->themeManager->expects($this->any())
1117       ->method('render')
1118       ->willReturnCallback(function ($theme, array $vars) {
1119         $output = <<<'EOS'
1120 <details>
1121   <summary>{{ title }}</summary>
1122   <div class="details-wrapper">{{ children }}</div>
1123 </details>
1124 EOS;
1125         $output = str_replace([
1126           '{{ title }}',
1127           '{{ children }}'
1128         ], [$vars['#title'], $vars['#children']], $output);
1129         return $output;
1130       });
1131   }
1132
1133 }
1134
1135 /**
1136  * @see \Drupal\Tests\Core\Render\RendererPlaceholdersTest::testRecursivePlaceholder()
1137  */
1138 class RecursivePlaceholdersTest {
1139
1140   /**
1141    * #lazy_builder callback; bubbles another placeholder.
1142    *
1143    * @param string $animal
1144    *   An animal.
1145    *
1146    * @return array
1147    *   A renderable array.
1148    */
1149   public static function callback($animal) {
1150     return [
1151       'another' => [
1152         '#create_placeholder' => TRUE,
1153         '#lazy_builder' => ['Drupal\Tests\Core\Render\PlaceholdersTest::callback', [$animal]],
1154       ],
1155     ];
1156   }
1157
1158 }