Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion examples/Resources/Private/Singles/Variables.html
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
xyz: '{
foobar: \'Escaped sub-string\'
}'
}
},
unsafeHTML: unsafeHTML
}"/>

</f:section>
Expand All @@ -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()}
</f:section>
3 changes: 3 additions & 0 deletions examples/example_variables.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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('<strong>Safe HTML String</strong>'),
]);

// Assigning the template path and filename to be rendered. Doing this overrides
Expand Down
5 changes: 5 additions & 0 deletions src/Core/Parser/SyntaxTree/BooleanNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/Core/Parser/SyntaxTree/EscapingNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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'],
Expand Down
23 changes: 23 additions & 0 deletions src/Core/Parser/UnsafeHTML.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

/*
* This file belongs to the package "TYPO3 Fluid".
* See LICENSE.txt that was shipped with this package.
*/

namespace TYPO3Fluid\Fluid\Core\Parser;

use Stringable;

/**
* Interface for values that are considered safe HTML
* and will not be escaped by the Fluid rendering engine.
*
* Use with caution and ensure the HTML has already been sanitized, as it will not be escaped by the Fluid rendering engine.
*
* @method string __toString() returns HTML that has already been sanitized and will not be escaped by the Fluid rendering engine.
* @internal
*/
interface UnsafeHTML extends Stringable {}
36 changes: 36 additions & 0 deletions src/Core/Parser/UnsafeHTMLString.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

/*
* This file belongs to the package "TYPO3 Fluid".
* See LICENSE.txt that was shipped with this package.
*/

namespace TYPO3Fluid\Fluid\Core\Parser;

use Stringable;

/**
* value object for values that are considered safe HTML
* and will not be escaped by the Fluid rendering engine.
*
* Use with caution and ensure the HTML has already been sanitized, as it will not be escaped by the Fluid rendering engine.
*
* @internal
*/
final readonly class UnsafeHTMLString implements UnsafeHTML
{
/**
* @param string|Stringable $html HTML that has already been sanitized and will not be escaped by the Fluid rendering engine.
*/
public function __construct(private string|Stringable $html) {}

/**
* @inheritDoc
*/
public function __toString(): string
{
return (string)$this->html;
}
}
8 changes: 8 additions & 0 deletions tests/Functional/Core/ViewHelper/ConditionViewHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -78,6 +79,7 @@ public static function variableConditionDataProvider(): array
'foo' => 'bar',
];
$emptyCountable = new \SplObjectStorage();
$htmlString = new UnsafeHTMLString('baz');

return [
// simple assignments
Expand All @@ -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]],
Expand Down
3 changes: 3 additions & 0 deletions tests/Functional/ExamplesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 <strong>Safe HTML String</strong>',
'Received $unsafeHTML with format.raw <strong>Safe HTML String</strong>',
'Received $unsafeHTML with format.htmlspecialchars &lt;strong&gt;Safe HTML String&lt;/strong&gt;',
],
],
'example_variableprovider.php' => [
Expand Down
10 changes: 10 additions & 0 deletions tests/Unit/Core/Parser/BooleanParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down