mirror of
https://github.com/twigphp/Twig.git
synced 2026-10-29 23:26:44 +00:00
Align runtime safe classes with the compile-time strategy subset rules
This commit is contained in:
@@ -14,6 +14,7 @@
|
||||
* Speed up traversing nodes with node visitors
|
||||
* Speed up loading compiled templates that OPcache already holds
|
||||
* Speed up applying `html_cva()` recipes that resolve to classes already seen
|
||||
* Fix a class marked safe for `html_attr` not being considered safe in `html` and `html_attr_relaxed` contexts
|
||||
|
||||
# 3.30.0 (2026-09-25)
|
||||
|
||||
|
||||
@@ -554,6 +554,10 @@ The escaping rules are implemented as follows:
|
||||
// mark objects of class "HtmlGenerator" as safe for all strategies
|
||||
$escaper->addSafeClass('HtmlGenerator', ['all']);
|
||||
|
||||
A class marked safe for ``html_attr`` is also safe for ``html_attr_relaxed``
|
||||
and ``html``, and a class marked safe for ``html_attr_relaxed`` is also safe
|
||||
for ``html``, as the stricter escaping covers the looser contexts.
|
||||
|
||||
* Escaping is applied before printing, after any other filter is applied:
|
||||
|
||||
.. code-block:: twig
|
||||
|
||||
@@ -17,6 +17,16 @@ use Twig\Markup;
|
||||
|
||||
final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
{
|
||||
/**
|
||||
* Strategies a value is also safe for when it is safe for the declared one.
|
||||
*
|
||||
* Mirrors the compile-time rules of Twig\NodeVisitor\SafeAnalysisNodeVisitor.
|
||||
*/
|
||||
private const IMPLIED_STRATEGIES = [
|
||||
'html_attr' => ['html_attr_relaxed', 'html'],
|
||||
'html_attr_relaxed' => ['html'],
|
||||
];
|
||||
|
||||
/** @var array<string, callable(string, string): string> */
|
||||
private $escapers = [];
|
||||
|
||||
@@ -76,8 +86,20 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
}
|
||||
$this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
|
||||
|
||||
$this->addSafeLookup($class, $strategies);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $strategies
|
||||
*/
|
||||
private function addSafeLookup(string $class, array $strategies): void
|
||||
{
|
||||
foreach ($strategies as $strategy) {
|
||||
$this->safeLookup[$strategy][$class] = true;
|
||||
|
||||
foreach (self::IMPLIED_STRATEGIES[$strategy] ?? [] as $implied) {
|
||||
$this->safeLookup[$implied][$class] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,9 +128,7 @@ final class EscaperRuntime implements RuntimeExtensionInterface
|
||||
foreach (class_parents($string) + class_implements($string) as $class) {
|
||||
if (isset($this->safeClasses[$class])) {
|
||||
$this->safeClasses[$c] = array_unique(array_merge($this->safeClasses[$c], $this->safeClasses[$class]));
|
||||
foreach ($this->safeClasses[$class] as $s) {
|
||||
$this->safeLookup[$s][$c] = true;
|
||||
}
|
||||
$this->addSafeLookup($c, $this->safeClasses[$class]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +426,41 @@ class EscaperRuntimeTest extends TestCase
|
||||
['<br />', '<br />', ['\Twig\Tests\Runtime\Extension_SafeHtmlInterface' => ['all']]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider provideImpliedStrategies
|
||||
*/
|
||||
#[DataProvider('provideImpliedStrategies')]
|
||||
public function testSafeClassesAreSafeForImpliedStrategies(bool $escaped, string $declaredStrategy, string $strategy): void
|
||||
{
|
||||
$escaper = new EscaperRuntime();
|
||||
$escaper->addSafeClass(Extension_TestClass::class, [$declaredStrategy]);
|
||||
|
||||
$output = $escaper->escape(new Extension_TestClass(), $strategy, null, true);
|
||||
|
||||
$this->assertSame($escaped, '<br />' !== $output);
|
||||
}
|
||||
|
||||
public static function provideImpliedStrategies()
|
||||
{
|
||||
return [
|
||||
[false, 'html_attr', 'html'],
|
||||
[false, 'html_attr', 'html_attr_relaxed'],
|
||||
[false, 'html_attr_relaxed', 'html'],
|
||||
[true, 'html', 'html_attr'],
|
||||
[true, 'html', 'html_attr_relaxed'],
|
||||
[true, 'html_attr_relaxed', 'html_attr'],
|
||||
[true, 'html_attr', 'js'],
|
||||
];
|
||||
}
|
||||
|
||||
public function testInheritedSafeClassesAreSafeForImpliedStrategies(): void
|
||||
{
|
||||
$escaper = new EscaperRuntime();
|
||||
$escaper->addSafeClass(Extension_SafeHtmlInterface::class, ['html_attr']);
|
||||
|
||||
$this->assertSame('<br />', $escaper->escape(new Extension_TestClass(), 'html', null, true));
|
||||
}
|
||||
}
|
||||
|
||||
function escaper($string, $charset)
|
||||
|
||||
Reference in New Issue
Block a user