diff --git a/examples/Resources/Private/Singles/Variables.html b/examples/Resources/Private/Singles/Variables.html index 3c443c6f5..8e849ea88 100644 --- a/examples/Resources/Private/Singles/Variables.html +++ b/examples/Resources/Private/Singles/Variables.html @@ -38,7 +38,8 @@ xyz: '{ foobar: \'Escaped sub-string\' }' - } + }, + unsafeHTML: unsafeHTML }"/> @@ -51,4 +52,7 @@ Received $array.baz with value {array.baz} Received $array.xyz.foobar with value {array.xyz.foobar} Received $myVariable with value {myVariable} +Received $unsafeHTML with unescaped value {unsafeHTML} +Received $unsafeHTML with format.raw {unsafeHTML -> f:format.raw()} +Received $unsafeHTML with format.htmlspecialchars {unsafeHTML -> f:format.htmlspecialchars()} diff --git a/examples/example_variables.php b/examples/example_variables.php index 21df591b4..e1cefa5ec 100644 --- a/examples/example_variables.php +++ b/examples/example_variables.php @@ -13,6 +13,7 @@ * how dynamic variable access works. */ +use TYPO3Fluid\Fluid\Core\Parser\UnsafeHTMLString; use TYPO3Fluid\FluidExamples\Helper\ExampleHelper; require_once __DIR__ . '/../vendor/autoload.php'; @@ -56,6 +57,8 @@ '123numericprefix' => 'Numeric prefixed variable', // A variable whose value refers to another variable name 'dynamicVariableName' => 'foobar', + // An UnsafeHTML variable that will not be escaped when rendered + 'unsafeHTML' => new UnsafeHTMLString('Safe HTML String'), ]); // Assigning the template path and filename to be rendered. Doing this overrides diff --git a/src/Core/Parser/SyntaxTree/BooleanNode.php b/src/Core/Parser/SyntaxTree/BooleanNode.php index ef7630ee0..f46e993f2 100644 --- a/src/Core/Parser/SyntaxTree/BooleanNode.php +++ b/src/Core/Parser/SyntaxTree/BooleanNode.php @@ -11,6 +11,7 @@ use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler; use TYPO3Fluid\Fluid\Core\Parser\BooleanParser; +use TYPO3Fluid\Fluid\Core\Parser\UnsafeHTML; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; /** @@ -128,6 +129,10 @@ public static function convertToBoolean(mixed $value, RenderingContextInterface if (is_numeric($value)) { return (bool)((float)$value); } + if ($value instanceof UnsafeHTML) { + // unpack UnsafeHTML to string, as it may be empty + $value = (string)$value; + } if (is_string($value)) { if (strlen($value) === 0) { return false; diff --git a/src/Core/Parser/SyntaxTree/EscapingNode.php b/src/Core/Parser/SyntaxTree/EscapingNode.php index 2fb00ab7b..0f75ebb4a 100644 --- a/src/Core/Parser/SyntaxTree/EscapingNode.php +++ b/src/Core/Parser/SyntaxTree/EscapingNode.php @@ -10,6 +10,7 @@ namespace TYPO3Fluid\Fluid\Core\Parser\SyntaxTree; use TYPO3Fluid\Fluid\Core\Compiler\TemplateCompiler; +use TYPO3Fluid\Fluid\Core\Parser\UnsafeHTML; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface; /** @@ -39,6 +40,9 @@ public function __construct(NodeInterface $node) public function evaluate(RenderingContextInterface $renderingContext): mixed { $evaluated = $this->node->evaluate($renderingContext); + if ($evaluated instanceof UnsafeHTML) { + return (string)$evaluated; + } if (is_string($evaluated) || (is_object($evaluated) && method_exists($evaluated, '__toString'))) { return htmlspecialchars((string)$evaluated, ENT_QUOTES); } @@ -66,6 +70,7 @@ public function convert(TemplateCompiler $templateCompiler): array if ($configuration['execution'] !== '\'\'') { $configuration['execution'] = sprintf( 'call_user_func_array( function ($var) { ' + . 'if ($var instanceof ' . UnsafeHTML::class . ') { return (string)$var; }' . 'return (is_string($var) || (is_object($var) && method_exists($var, \'__toString\')) ' . '? htmlspecialchars((string) $var, ENT_QUOTES) : $var); }, [%s])', $configuration['execution'], diff --git a/src/Core/Parser/UnsafeHTML.php b/src/Core/Parser/UnsafeHTML.php new file mode 100644 index 000000000..40694b90f --- /dev/null +++ b/src/Core/Parser/UnsafeHTML.php @@ -0,0 +1,23 @@ +html; + } +} diff --git a/tests/Functional/Core/ViewHelper/ConditionViewHelperTest.php b/tests/Functional/Core/ViewHelper/ConditionViewHelperTest.php index f1f685d7f..38538ed16 100644 --- a/tests/Functional/Core/ViewHelper/ConditionViewHelperTest.php +++ b/tests/Functional/Core/ViewHelper/ConditionViewHelperTest.php @@ -11,6 +11,7 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; +use TYPO3Fluid\Fluid\Core\Parser\UnsafeHTMLString; use TYPO3Fluid\Fluid\Tests\Functional\AbstractFunctionalTestCase; use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\UserWithToString; use TYPO3Fluid\Fluid\View\TemplateView; @@ -78,6 +79,7 @@ public static function variableConditionDataProvider(): array 'foo' => 'bar', ]; $emptyCountable = new \SplObjectStorage(); + $htmlString = new UnsafeHTMLString('baz'); return [ // simple assignments @@ -94,6 +96,12 @@ public static function variableConditionDataProvider(): array ['{test1} === {test2}', false, ['test1' => 1, 'test2' => true]], ['{test1} == {test2}', true, ['test1' => 1, 'test2' => true]], + // conditions with UnsafeHTMLString + ['{test}', true, ['test' => $htmlString]], + ['{test} == \'baz\'', true, ['test' => $htmlString]], + ['{test1} === {test2}', false, ['test1' => 'baz', 'test2' => $htmlString]], + ['{test1} == {test2}', true, ['test1' => 'baz', 'test2' => $htmlString]], + // conditions with objects ['{user1} == {user1}', true, ['user1' => $user1]], ['{user1} === {user1}', true, ['user1' => $user1]], diff --git a/tests/Functional/ExamplesTest.php b/tests/Functional/ExamplesTest.php index b472ef6d1..b39bd289c 100644 --- a/tests/Functional/ExamplesTest.php +++ b/tests/Functional/ExamplesTest.php @@ -167,6 +167,9 @@ public static function exampleScriptValuesDataProvider(): array 'Received $array.baz with value 42', 'Received $array.xyz.foobar with value Escaped sub-string', 'Received $myVariable with value Nice string', + 'Received $unsafeHTML with unescaped value Safe HTML String', + 'Received $unsafeHTML with format.raw Safe HTML String', + 'Received $unsafeHTML with format.htmlspecialchars <strong>Safe HTML String</strong>', ], ], 'example_variableprovider.php' => [ diff --git a/tests/Unit/Core/Parser/BooleanParserTest.php b/tests/Unit/Core/Parser/BooleanParserTest.php index 9b497bfc3..d4c2fc15c 100644 --- a/tests/Unit/Core/Parser/BooleanParserTest.php +++ b/tests/Unit/Core/Parser/BooleanParserTest.php @@ -15,6 +15,7 @@ use TYPO3Fluid\Fluid\Core\Parser\BooleanParser; use TYPO3Fluid\Fluid\Core\Parser\Exception; use TYPO3Fluid\Fluid\Core\Parser\SyntaxTree\BooleanNode; +use TYPO3Fluid\Fluid\Core\Parser\UnsafeHTMLString; use TYPO3Fluid\Fluid\Core\Rendering\RenderingContext; final class BooleanParserTest extends TestCase @@ -105,6 +106,15 @@ public static function getSomeEvaluationTestValues(): array ['{foo} == FALSE', true, ['foo' => false]], ['!{foo}', true, ['foo' => false]], + ['{foo}', false, ['foo' => new UnsafeHTMLString('')]], + ["{foo} == ''", true, ['foo' => new UnsafeHTMLString('')]], + ['{foo}', true, ['foo' => new UnsafeHTMLString('test')]], + ['{foo} == FALSE', false, ['foo' => new UnsafeHTMLString('test')]], + ['{foo} == TRUE', true, ['foo' => new UnsafeHTMLString('test')]], + ["{foo} == 'test'", true, ['foo' => new UnsafeHTMLString('test')]], + ['{foo} === TRUE', false, ['foo' => new UnsafeHTMLString('0')]], + ['{foo} === \'0\'', false, ['foo' => new UnsafeHTMLString('0')]], + /* * @todo This should work but doesn't at the moment. This is probably related to the boolean * parser not converting variable nodes correctly. There is a related todo in the IfThenElseViewHelperTest.