View source
<?php
declare (strict_types=1);
namespace Drupal\Tests\Core\Render;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Core\Template\Attribute;
class RendererTest extends RendererTestBase {
protected $defaultThemeVars = [
'#cache' => [
'contexts' => [
'languages:language_interface',
'theme',
],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
'#attached' => [],
'#children' => '',
];
public function testRenderBasic($build, $expected, callable $setup_code = NULL) {
if (isset($setup_code)) {
$setup_code = $setup_code
->bindTo($this);
$setup_code();
}
if (isset($build['#markup'])) {
$this
->assertNotInstanceOf(MarkupInterface::class, $build['#markup']);
}
$render_output = $this->renderer
->renderRoot($build);
$this
->assertSame($expected, (string) $render_output);
if ($render_output !== '') {
$this
->assertInstanceOf(MarkupInterface::class, $render_output);
$this
->assertInstanceOf(MarkupInterface::class, $build['#markup']);
}
}
public function providerTestRenderBasic() {
$data = [];
$data[] = [
NULL,
'',
];
$data[] = [
'',
'',
];
$data[] = [
[
'#markup' => 'foo',
'#printed' => TRUE,
],
'',
];
$data[] = [
[
'#markup' => 'foo',
'#pre_render' => [
[
new TestCallables(),
'preRenderPrinted',
],
],
],
'',
];
$data[] = [
[
'#markup' => 'foo',
],
'foo',
];
$data[] = [
[
'#markup' => '0',
],
'0',
];
$data[] = [
[
'#markup' => 0,
],
'0',
];
$data[] = [
[
'#markup' => '',
],
'',
];
$data[] = [
[
'#markup' => NULL,
],
'',
];
$data[] = [
[
'#plain_text' => 'foo',
],
'foo',
];
$data[] = [
[
'#plain_text' => '<em>foo</em>',
'#markup' => 'bar',
],
'<em>foo</em>',
];
$data[] = [
[
'#plain_text' => Markup::create('<em>foo</em>'),
],
'<em>foo</em>',
];
$data[] = [
[
'#plain_text' => '0',
],
'0',
];
$data[] = [
[
'#plain_text' => 0,
],
'0',
];
$data[] = [
[
'#plain_text' => '',
],
'',
];
$data[] = [
[
'#plain_text' => NULL,
],
'',
];
$data[] = [
[
'child' => [
'#markup' => 'bar',
],
],
'bar',
];
$data[] = [
[
'child' => [
'#markup' => "This is <script>alert('XSS')</script> test",
],
],
"This is alert('XSS') test",
];
$data[] = [
[
'child' => [
'#markup' => "This is <script>alert('XSS')</script> test",
'#allowed_tags' => [
'script',
],
],
],
"This is <script>alert('XSS')</script> test",
];
$data[] = [
[
'child' => [
'#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>",
'#allowed_tags' => [
'em',
'strong',
],
],
],
"This is <em>alert('XSS')</em> <strong>test</strong>",
];
$data[] = [
[
'child' => [
'#plain_text' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>",
],
],
"This is <script><em>alert('XSS')</em></script> <strong>test</strong>",
];
$data[] = [
[
'child' => [
'#markup' => "This is <script><em>alert('XSS')</em></script> <strong>test</strong>",
],
],
"This is <em>alert('XSS')</em> <strong>test</strong>",
];
$data[] = [
[
'child' => [
'#markup' => "This is <strong><script>alert('not a giraffe')</script></strong> test",
],
],
"This is <strong>alert('not a giraffe')</strong> test",
];
$data[] = [
[
'#children' => '',
'child' => [
'#markup' => 'bar',
],
],
'bar',
];
$data[] = [
[
'#children' => 'foo',
'child' => [
'#markup' => 'bar',
],
],
'foo',
];
$data[] = [
[
'#markup' => 'foo',
'#pre_render' => [
function ($elements) {
$elements['#markup'] .= '<script>alert("bar");</script>';
return $elements;
},
],
],
'fooalert("bar");',
];
$data[] = [
[
'#markup' => 'foo',
'#allowed_tags' => [
'script',
],
'#pre_render' => [
function ($elements) {
$elements['#markup'] .= '<script>alert("bar");</script>';
return $elements;
},
],
],
'foo<script>alert("bar");</script>',
];
$data[] = [
[
'#plain_text' => 'foo',
'#pre_render' => [
function ($elements) {
$elements['#plain_text'] .= '<script>alert("bar");</script>';
return $elements;
},
],
],
'foo<script>alert("bar");</script>',
];
$build = [
'#theme' => 'common_test_foo',
'#foo' => 'foo',
'#bar' => 'bar',
'#theme_wrappers' => [
'container',
],
'#attributes' => [
'class' => [
'baz',
],
],
];
$setup_code_type_link = function () {
$this->themeManager
->expects($this
->exactly(2))
->method('render')
->with($this
->logicalOr('common_test_foo', 'container'))
->willReturnCallback(function ($theme, $vars) {
if ($theme == 'container') {
return '<div' . (string) new Attribute($vars['#attributes']) . '>' . $vars['#children'] . "</div>\n";
}
return $vars['#foo'] . $vars['#bar'];
});
};
$data[] = [
$build,
'<div class="baz">foobar</div>' . "\n",
$setup_code_type_link,
];
$build = [
'#type' => 'link',
'#theme_wrappers' => [
'container' => [
'#attributes' => [
'class' => [
'baz',
],
],
],
],
'#attributes' => [
'id' => 'foo',
],
'#url' => 'https://www.drupal.org',
'#title' => 'bar',
];
$setup_code_type_link = function () {
$this->themeManager
->expects($this
->exactly(2))
->method('render')
->with($this
->logicalOr('link', 'container'))
->willReturnCallback(function ($theme, $vars) {
if ($theme == 'container') {
return '<div' . (string) new Attribute($vars['#attributes']) . '>' . $vars['#children'] . "</div>\n";
}
$attributes = new Attribute([
'href' => $vars['#url'],
] + ($vars['#attributes'] ?? []));
return '<a' . (string) $attributes . '>' . $vars['#title'] . '</a>';
});
};
$data[] = [
$build,
'<div class="baz"><a href="https://www.drupal.org" id="foo">bar</a></div>' . "\n",
$setup_code_type_link,
];
$build = [
'#type' => 'link',
'#url' => 'https://www.drupal.org',
'#title' => 'foo',
'#theme_wrappers' => [
'container' => [
'#attributes' => [
'class' => [
'baz',
],
],
],
],
];
$data[] = [
$build,
'<div class="baz"><a href="https://www.drupal.org">foo</a></div>' . "\n",
$setup_code_type_link,
];
$build = [
'#attributes' => [
'class' => [
'foo',
],
],
'#theme_wrappers' => [
'container' => [
'#attributes' => [
'class' => [
'bar',
],
],
],
'container',
],
];
$setup_code = function () {
$this->themeManager
->expects($this
->exactly(2))
->method('render')
->with('container')
->willReturnCallback(function ($theme, $vars) {
return '<div' . (string) new Attribute($vars['#attributes']) . '>' . $vars['#children'] . "</div>\n";
});
};
$data[] = [
$build,
'<div class="foo"><div class="bar"></div>' . "\n" . '</div>' . "\n",
$setup_code,
];
$build = [
'#theme_wrappers' => [
[
'container',
],
],
'#attributes' => [
'class' => [
'foo',
],
],
];
$setup_code = function () {
$this->themeManager
->expects($this
->once())
->method('render')
->with([
'container',
])
->willReturnCallback(function ($theme, $vars) {
return '<div' . (string) new Attribute($vars['#attributes']) . '>' . $vars['#children'] . "</div>\n";
});
};
$data[] = [
$build,
'<div class="foo"></div>' . "\n",
$setup_code,
];
$build = [
'#theme' => [
'suggestion_not_implemented',
],
'#markup' => 'foo',
];
$setup_code = function () {
$this->themeManager
->expects($this
->once())
->method('render')
->with([
'suggestion_not_implemented',
], $this
->anything())
->willReturn(FALSE);
};
$data[] = [
$build,
'foo',
$setup_code,
];
$build = [
'#theme' => [
'suggestion_not_implemented',
],
'child' => [
'#markup' => 'foo',
],
];
$setup_code = function () {
$this->themeManager
->expects($this
->once())
->method('render')
->with([
'suggestion_not_implemented',
], $this
->anything())
->willReturn(FALSE);
};
$data[] = [
$build,
'foo',
$setup_code,
];
$build = [
'#theme' => [
'common_test_empty',
],
'#markup' => 'foo',
];
$theme_function_output = $this
->randomContextValue();
$setup_code = function () use ($theme_function_output) {
$this->themeManager
->expects($this
->once())
->method('render')
->with([
'common_test_empty',
], $this
->anything())
->willReturn($theme_function_output);
};
$data[] = [
$build,
$theme_function_output,
$setup_code,
];
$build = [
'#theme' => [
'common_test_empty',
],
'child' => [
'#markup' => 'foo',
],
];
$data[] = [
$build,
$theme_function_output,
$setup_code,
];
$build = [
'#theme' => 'common_test_foo',
'#children' => 'baz',
'child' => [
'#markup' => 'boo',
],
];
$setup_code = function () {
$this->themeManager
->expects($this
->once())
->method('render')
->with('common_test_foo', $this
->anything())
->willReturn('foobar');
};
$data[] = [
$build,
'foobar',
$setup_code,
];
$build = [
'#theme' => 'common_test_foo',
'#children' => '',
'#render_children' => TRUE,
'child' => [
'#markup' => 'boo',
],
];
$setup_code = function () {
$this->themeManager
->expects($this
->never())
->method('render');
};
$data[] = [
$build,
'boo',
$setup_code,
];
$build = [
'#theme' => 'common_test_foo',
'#children' => 'baz',
'#render_children' => TRUE,
'child' => [
'#markup' => 'boo',
],
];
$setup_code = function () {
$this->themeManager
->expects($this
->never())
->method('render');
};
$data[] = [
$build,
'baz',
$setup_code,
];
$build = [
'#theme' => 'common_test_foo',
'#children' => '',
'#prefix' => 'kangaroo',
'#suffix' => 'unicorn',
'#render_children' => TRUE,
'child' => [
'#markup' => 'kitten',
],
];
$setup_code = function () {
$this->themeManager
->expects($this
->never())
->method('render');
};
$data[] = [
$build,
'kitten',
$setup_code,
];
return $data;
}
public function testRenderSorting() {
$first = $this
->randomMachineName();
$second = $this
->randomMachineName();
$elements = [
'second' => [
'#weight' => 10,
'#markup' => $second,
],
'first' => [
'#weight' => 0,
'#markup' => $first,
],
];
$output = (string) $this->renderer
->renderRoot($elements);
$this
->assertGreaterThan(strpos($output, $first), strpos($output, $second));
$this
->assertTrue($elements['#sorted'], "'#sorted' => TRUE was added to the array");
$children = Element::children($elements);
$this
->assertSame('first', array_shift($children), 'Child found in the correct order.');
$this
->assertSame('second', array_shift($children), 'Child found in the correct order.');
}
public function testRenderSortingWithSetHashSorted() {
$first = $this
->randomMachineName();
$second = $this
->randomMachineName();
$elements = [
'second' => [
'#weight' => 10,
'#markup' => $second,
],
'first' => [
'#weight' => 0,
'#markup' => $first,
],
'#sorted' => TRUE,
];
$output = (string) $this->renderer
->renderRoot($elements);
$this
->assertLessThan(strpos($output, $first), strpos($output, $second));
}
public function testRenderWithPresetAccess($access) {
$build = [
'#access' => $access,
];
$this
->assertAccess($build, $access);
}
public function testRenderWithAccessCallbackCallable($access) {
$build = [
'#access_callback' => function () use ($access) {
return $access;
},
];
$this
->assertAccess($build, $access);
}
public function testRenderWithAccessPropertyAndCallback($access) {
$build = [
'#access' => $access,
'#access_callback' => function () {
return TRUE;
},
];
$this
->assertAccess($build, $access);
}
public function testRenderWithAccessControllerResolved($access) {
switch ($access) {
case AccessResult::allowed():
$method = 'accessResultAllowed';
break;
case AccessResult::forbidden():
$method = 'accessResultForbidden';
break;
case FALSE:
$method = 'accessFalse';
break;
case TRUE:
$method = 'accessTrue';
break;
}
$build = [
'#access_callback' => 'Drupal\\Tests\\Core\\Render\\TestAccessClass::' . $method,
];
$this
->assertAccess($build, $access);
}
public function testRenderAccessCacheabilityDependencyInheritance() {
$build = [
'#access' => AccessResult::allowed()
->addCacheContexts([
'user',
]),
];
$this->renderer
->renderInIsolation($build);
$this
->assertEqualsCanonicalizing([
'languages:language_interface',
'theme',
'user',
], $build['#cache']['contexts']);
}
public function testRenderTwice($build) {
$this
->assertEquals('kittens', $this->renderer
->renderRoot($build));
$this
->assertEquals('kittens', $build['#markup']);
$this
->assertEquals([
'kittens-147',
], $build['#cache']['tags']);
$this
->assertTrue($build['#printed']);
$this
->assertEquals('', $this->renderer
->renderRoot($build));
}
public static function providerRenderTwice() {
return [
[
[
'#markup' => 'kittens',
'#cache' => [
'tags' => [
'kittens-147',
],
],
],
],
[
[
'child' => [
'#markup' => 'kittens',
'#cache' => [
'tags' => [
'kittens-147',
],
],
],
],
],
[
[
'#render_children' => TRUE,
'child' => [
'#markup' => 'kittens',
'#cache' => [
'tags' => [
'kittens-147',
],
],
],
],
],
];
}
public function testRenderChildrenAccess() {
$build = [
'#access' => FALSE,
'#render_children' => TRUE,
'child' => [
'#markup' => 'kittens',
],
];
$this
->assertEquals('', $this->renderer
->renderRoot($build));
}
public static function providerAccessValues() {
return [
[
FALSE,
],
[
TRUE,
],
[
AccessResult::forbidden(),
],
[
AccessResult::allowed(),
],
];
}
protected function assertAccess(array $build, $access) : void {
$sensitive_content = $this
->randomContextValue();
$build['#markup'] = $sensitive_content;
if ($access instanceof AccessResultInterface && $access
->isAllowed() || $access === TRUE) {
$this
->assertSame($sensitive_content, (string) $this->renderer
->renderRoot($build));
}
else {
$this
->assertSame('', (string) $this->renderer
->renderRoot($build));
}
}
public function testRenderWithoutThemeArguments() {
$element = [
'#theme' => 'common_test_foo',
];
$this->themeManager
->expects($this
->once())
->method('render')
->with('common_test_foo', $this->defaultThemeVars + $element)
->willReturn('foobar');
$this
->assertEquals('foobar', $this->renderer
->renderRoot($element), 'Defaults work');
}
public function testRenderWithThemeArguments() {
$element = [
'#theme' => 'common_test_foo',
'#foo' => $this
->randomMachineName(),
'#bar' => $this
->randomMachineName(),
];
$this->themeManager
->expects($this
->once())
->method('render')
->with('common_test_foo', $this->defaultThemeVars + $element)
->willReturnCallback(function ($hook, $vars) {
return $vars['#foo'] . $vars['#bar'];
});
$this
->assertEquals($this->renderer
->renderRoot($element), $element['#foo'] . $element['#bar'], 'Passing arguments to theme functions works');
}
public static function providerRenderCache() {
return [
'full access' => [
NULL,
[
'render_cache_tag',
'render_cache_tag_child:1',
'render_cache_tag_child:2',
],
],
'no child access' => [
AccessResult::forbidden()
->addCacheTags([
'render_cache_tag_child_access:1',
'render_cache_tag_child_access:2',
]),
[
'render_cache_tag',
'render_cache_tag_child:1',
'render_cache_tag_child:2',
'render_cache_tag_child_access:1',
'render_cache_tag_child_access:2',
],
],
];
}
public function testRenderCache($child_access, $expected_tags) {
$this
->setUpRequest();
$this
->setUpMemoryCache();
$test_element = [
'#cache' => [
'keys' => [
'render_cache_test',
],
'tags' => [
'render_cache_tag',
],
],
'#markup' => '',
'child' => [
'#access' => $child_access,
'#cache' => [
'keys' => [
'render_cache_test_child',
],
'tags' => [
'render_cache_tag_child:1',
'render_cache_tag_child:2',
],
],
'#markup' => '',
],
];
$element = $test_element;
$this->renderer
->renderRoot($element);
$this
->assertTrue(isset($element['#printed']), 'No cache hit');
$element = $test_element;
$this->renderer
->renderRoot($element);
$this
->assertFalse(isset($element['#printed']), 'Cache hit');
$this
->assertEquals($expected_tags, $element['#cache']['tags'], 'Cache tags were collected from the element and its subchild.');
$cache_item = $this->cacheFactory
->get('render')
->get([
'render_cache_test',
], CacheableMetadata::createFromRenderArray($element));
$this
->assertSame(Cache::mergeTags($expected_tags, [
'rendered',
]), $cache_item->tags);
}
public function testRenderCacheMaxAge($max_age, $is_render_cached, $render_cache_item_expire) {
$this
->setUpRequest();
$this
->setUpMemoryCache();
$element = [
'#cache' => [
'keys' => [
'render_cache_test',
],
'max-age' => $max_age,
],
'#markup' => '',
];
$this->renderer
->renderRoot($element);
$cache_item = $this->cacheFactory
->get('render')
->get([
'render_cache_test',
], CacheableMetadata::createFromRenderArray($element));
if (!$is_render_cached) {
$this
->assertFalse($cache_item);
}
else {
$this
->assertNotFalse($cache_item);
$this
->assertSame($render_cache_item_expire, $cache_item->expire);
}
}
public static function providerTestRenderCacheMaxAge() {
return [
[
0,
FALSE,
NULL,
],
[
60,
TRUE,
(int) $_SERVER['REQUEST_TIME'] + 60,
],
[
Cache::PERMANENT,
TRUE,
-1,
],
];
}
public function testRenderCacheProperties(array $expected_results) {
$this
->setUpRequest();
$this
->setUpMemoryCache();
$element = $original = [
'#cache' => [
'keys' => [
'render_cache_test',
],
],
'#cache_properties' => array_keys(array_filter($expected_results)),
'child1' => [
'#markup' => Markup::create('1'),
],
'child2' => [
'#markup' => Markup::create('2'),
],
'#custom_property' => Markup::create('custom_value'),
'#custom_property_array' => [
'custom value',
],
];
$this->renderer
->renderRoot($element);
$cache = $this->cacheFactory
->get('render');
$data = $cache
->get([
'render_cache_test',
], CacheableMetadata::createFromRenderArray($element))->data;
$this
->assertEquals($data['#markup'] === '', (bool) Element::children($data));
foreach ($expected_results as $property => $expected) {
$cached = !empty($data[$property]);
$this
->assertEquals($cached, (bool) $expected);
if ($cached) {
$this
->assertEquals($data[$property], $original[$property]);
}
}
$safe_cache_properties = array_diff(Element::properties(array_filter($expected_results)), [
'#custom_property_array',
]);
foreach ($safe_cache_properties as $cache_property) {
$this
->assertInstanceOf(MarkupInterface::class, $data[$cache_property]);
}
}
public static function providerTestRenderCacheProperties() {
return [
[
[],
],
[
[
'child1' => 0,
'child2' => 0,
'#custom_property' => 0,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 0,
'child2' => 0,
'#custom_property' => 1,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 0,
'child2' => 1,
'#custom_property' => 0,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 0,
'child2' => 1,
'#custom_property' => 1,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 1,
'child2' => 0,
'#custom_property' => 0,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 1,
'child2' => 0,
'#custom_property' => 1,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 1,
'child2' => 1,
'#custom_property' => 0,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 1,
'child2' => 1,
'#custom_property' => 1,
'#custom_property_array' => 0,
],
],
[
[
'child1' => 1,
'child2' => 1,
'#custom_property' => 1,
'#custom_property_array' => 1,
],
],
];
}
public function testAddCacheableDependency(array $build, $object, array $expected) {
$this->renderer
->addCacheableDependency($build, $object);
$this
->assertEquals($build, $expected);
}
public static function providerTestAddCacheableDependency() {
return [
[
[],
new TestCacheableDependency([], [], Cache::PERMANENT),
[
'#cache' => [
'contexts' => [],
'tags' => [],
'max-age' => Cache::PERMANENT,
],
],
],
[
[],
new TestCacheableDependency([
'user.roles',
], [
'foo',
], Cache::PERMANENT),
[
'#cache' => [
'contexts' => [
'user.roles',
],
'tags' => [
'foo',
],
'max-age' => Cache::PERMANENT,
],
],
],
[
[
'#cache' => [
'contexts' => [
'theme',
],
'tags' => [
'bar',
],
'max-age' => 600,
],
],
new TestCacheableDependency([
'user.roles',
], [
'foo',
], Cache::PERMANENT),
[
'#cache' => [
'contexts' => [
'theme',
'user.roles',
],
'tags' => [
'bar',
'foo',
],
'max-age' => 600,
],
],
],
[
[
'#cache' => [
'contexts' => [
'theme',
],
'tags' => [
'bar',
],
'max-age' => 600,
],
],
new \stdClass(),
[
'#cache' => [
'contexts' => [
'theme',
],
'tags' => [
'bar',
],
'max-age' => 0,
],
],
],
];
}
}
class TestAccessClass implements TrustedCallbackInterface {
public static function accessTrue() {
return TRUE;
}
public static function accessFalse() {
return FALSE;
}
public static function accessResultAllowed() {
return AccessResult::allowed();
}
public static function accessResultForbidden() {
return AccessResult::forbidden();
}
public static function trustedCallbacks() {
return [
'accessTrue',
'accessFalse',
'accessResultAllowed',
'accessResultForbidden',
];
}
}
class TestCallables implements TrustedCallbackInterface {
public function preRenderPrinted($elements) {
$elements['#printed'] = TRUE;
return $elements;
}
public static function trustedCallbacks() {
return [
'preRenderPrinted',
];
}
}