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
20642d537e
commit
fe6b686153
|
@ -13,8 +13,11 @@ use Drupal\Component\Utility\Crypt;
|
|||
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 @@ namespace Drupal\Core\Template;
|
|||
|
||||
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 @@ class TwigExtension extends \Twig_Extension {
|
|||
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 @@ class TwigExtension extends \Twig_Extension {
|
|||
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 @@ class TwigExtension extends \Twig_Extension {
|
|||
}
|
||||
|
||||
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 @@ class ThemeRenderAndAutoescapeTest extends KernelTestBase {
|
|||
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 @@ class TwigExtensionTest extends UnitTestCase {
|
|||
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 {
|
||||
|
|
Loading…
Reference in New Issue