Commit fe6b6861 authored by catch's avatar catch

Issue #2575519 by Wim Leers, dawehner, alexpott: Twig template variables...

Issue #2575519 by Wim Leers, dawehner, alexpott: Twig template variables containing result of Drupal::url() and Drupal:l:() don't bubble up their cacheability and attachment metadata (e.g. token placeholder)
parent 20642d53
......@@ -13,8 +13,11 @@
use Drupal\Component\Utility\Html;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\StorageException;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\RenderableInterface;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Theme\ThemeSettings;
......@@ -399,6 +402,17 @@ function theme_get_setting($setting_name, $theme = NULL) {
* https://www.drupal.org/node/2575065
*/
function theme_render_and_autoescape($arg) {
// If it's a renderable, then it'll be up to the generated render array it
// returns to contain the necessary cacheability & attachment metadata. If
// it doesn't implement CacheableDependencyInterface or AttachmentsInterface
// then there is nothing to do here.
if (!($arg instanceof RenderableInterface) && ($arg instanceof CacheableDependencyInterface || $arg instanceof AttachmentsInterface)) {
$arg_bubbleable = [];
BubbleableMetadata::createFromObject($arg)
->applyTo($arg_bubbleable);
\Drupal::service('renderer')->render($arg_bubbleable);
}
if ($arg instanceof MarkupInterface) {
return (string) $arg;
}
......
......@@ -4,7 +4,10 @@
use Drupal\Component\Utility\Html;
use Drupal\Component\Render\MarkupInterface;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Render\AttachmentsInterface;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\RenderableInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
......@@ -411,6 +414,8 @@ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $
return NULL;
}
$this->bubbleArgMetadata($arg);
// Keep Twig_Markup objects intact to support autoescaping.
if ($autoescape && ($arg instanceof \Twig_Markup || $arg instanceof MarkupInterface)) {
return $arg;
......@@ -463,6 +468,37 @@ public function escapeFilter(\Twig_Environment $env, $arg, $strategy = 'html', $
return $this->renderer->render($arg);
}
/**
* Bubbles Twig template argument's cacheability & attachment metadata.
*
* For example: a generated link or generated URL object is passed as a Twig
* template argument, and its bubbleable metadata must be bubbled.
*
* @see \Drupal\Core\GeneratedLink
* @see \Drupal\Core\GeneratedUrl
*
* @param mixed $arg
* A Twig template argument that is about to be printed.
*
* @see \Drupal\Core\Theme\ThemeManager::render()
* @see \Drupal\Core\Render\RendererInterface::render()
*/
protected function bubbleArgMetadata($arg) {
// If it's a renderable, then it'll be up to the generated render array it
// returns to contain the necessary cacheability & attachment metadata. If
// it doesn't implement CacheableDependencyInterface or AttachmentsInterface
// then there is nothing to do here.
if ($arg instanceof RenderableInterface || !($arg instanceof CacheableDependencyInterface || $arg instanceof AttachmentsInterface)) {
return;
}
$arg_bubbleable = [];
BubbleableMetadata::createFromObject($arg)
->applyTo($arg_bubbleable);
$this->renderer->render($arg_bubbleable);
}
/**
* Wrapper around render() for twig printed output.
*
......@@ -505,6 +541,7 @@ public function renderVar($arg) {
}
if (is_object($arg)) {
$this->bubbleArgMetadata($arg);
if ($arg instanceof RenderableInterface) {
$arg = $arg->toRenderable();
}
......
......@@ -8,9 +8,11 @@
namespace Drupal\KernelTests\Core\Theme;
use Drupal\Component\Utility\Html;
use Drupal\Core\GeneratedLink;
use Drupal\Core\Link;
use Drupal\Core\Render\RenderContext;
use Drupal\Core\Render\Markup;
use Drupal\Core\Url;
use Drupal\KernelTests\KernelTestBase;
/**
......@@ -87,6 +89,50 @@ public function testThemeEscapeAndRenderNotPrintable() {
theme_render_and_autoescape(new NonPrintable());
}
/**
* Ensure cache metadata is bubbled when using theme_render_and_autoescape().
*/
public function testBubblingMetadata() {
$link = new GeneratedLink();
$link->setGeneratedLink('<a href="http://example.com"></a>');
$link->addCacheTags(['foo']);
$link->addAttachments(['library' => ['system/base']]);
$context = new RenderContext();
// Use a closure here since we need to render with a render context.
$theme_render_and_autoescape = function () use ($link) {
return theme_render_and_autoescape($link);
};
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$output = $renderer->executeInRenderContext($context, $theme_render_and_autoescape);
$this->assertEquals('<a href="http://example.com"></a>', $output);
/** @var \Drupal\Core\Render\BubbleableMetadata $metadata */
$metadata = $context->pop();
$this->assertEquals(['foo'], $metadata->getCacheTags());
$this->assertEquals(['library' => ['system/base']], $metadata->getAttachments());
}
/**
* Ensure cache metadata is bubbled when using theme_render_and_autoescape().
*/
public function testBubblingMetadataWithRenderable() {
$link = new Link('', Url::fromRoute('<current>'));
$context = new RenderContext();
// Use a closure here since we need to render with a render context.
$theme_render_and_autoescape = function () use ($link) {
return theme_render_and_autoescape($link);
};
/** @var \Drupal\Core\Render\RendererInterface $renderer */
$renderer = \Drupal::service('renderer');
$output = $renderer->executeInRenderContext($context, $theme_render_and_autoescape);
$this->assertEquals('<a href="/' . urlencode('<none>') . '"></a>', $output);
/** @var \Drupal\Core\Render\BubbleableMetadata $metadata */
$metadata = $context->pop();
$this->assertEquals(['route'], $metadata->getCacheContexts());
}
}
class NonPrintable { }
......@@ -7,8 +7,10 @@
namespace Drupal\Tests\Core\Template;
use Drupal\Core\GeneratedLink;
use Drupal\Core\Render\RenderableInterface;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Template\Loader\StringLoader;
use Drupal\Core\Template\TwigEnvironment;
......@@ -240,6 +242,63 @@ public function providerTestRenderVar() {
return $data;
}
/**
* @covers ::escapeFilter
* @covers ::bubbleArgMetadata
*/
public function testEscapeWithGeneratedLink() {
$renderer = $this->prophesize(RendererInterface::class);
$twig = new \Twig_Environment(NULL, [
'debug' => TRUE,
'cache' => FALSE,
'autoescape' => 'html',
'optimizations' => 0,
]
);
$twig_extension = new TwigExtension($renderer->reveal());
$twig->addExtension($twig_extension->setUrlGenerator($this->prophesize(UrlGeneratorInterface::class)->reveal()));
$link = new GeneratedLink();
$link->setGeneratedLink('<a href="http://example.com"></a>');
$link->addCacheTags(['foo']);
$link->addAttachments(['library' => ['system/base']]);
$result = $twig_extension->escapeFilter($twig, $link, 'html', NULL, TRUE);
$renderer->render([
"#cache" => [
"contexts" => [],
"tags" => ["foo"],
"max-age" => -1
],
"#attached" => ['library' => ['system/base']],
])->shouldHaveBeenCalled();
$this->assertEquals('<a href="http://example.com"></a>', $result);
}
/**
* @covers ::renderVar
* @covers ::bubbleArgMetadata
*/
public function testRenderVarWithGeneratedLink() {
$renderer = $this->prophesize(RendererInterface::class);
$twig_extension = new TwigExtension($renderer->reveal());
$link = new GeneratedLink();
$link->setGeneratedLink('<a href="http://example.com"></a>');
$link->addCacheTags(['foo']);
$link->addAttachments(['library' => ['system/base']]);
$result = $twig_extension->renderVar($link);
$renderer->render([
"#cache" => [
"contexts" => [],
"tags" => ["foo"],
"max-age" => -1
],
"#attached" => ['library' => ['system/base']],
])->shouldHaveBeenCalled();
$this->assertEquals('<a href="http://example.com"></a>', $result);
}
}
class TwigExtensionTestString {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment