Skip to content

Commit

Permalink
[FEATURE] StripTagsViewHelper for StandaloneFluid (#840)
Browse files Browse the repository at this point in the history
* [FEATURE] StripTagsViewHelper for StandaloneFluid

StripTagsViewHelper doesn't have any dependencies to TYPO3,
so it can be moved to Fluid Standalone.

* [BUGFIX] Throw exceptions for invalid input
  • Loading branch information
s2b authored Nov 26, 2023
1 parent fb92329 commit 1e258a8
Show file tree
Hide file tree
Showing 2 changed files with 237 additions and 0 deletions.
118 changes: 118 additions & 0 deletions src/ViewHelpers/Format/StripTagsViewHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?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\ViewHelpers\Format;

use Stringable;
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithContentArgumentAndRenderStatic;

/**
* Removes tags from the given string (applying PHPs :php:`strip_tags()` function)
* See https://www.php.net/manual/function.strip-tags.php.
*
* Examples
* ========
*
* Default notation
* ----------------
*
* ::
*
* <f:format.stripTags>Some Text with <b>Tags</b> and an &Uuml;mlaut.</f:format.stripTags>
*
* Some Text with Tags and an &Uuml;mlaut. :php:`strip_tags()` applied.
*
* .. note::
* Encoded entities are not decoded.
*
* Default notation with allowedTags
* ---------------------------------
*
* ::
*
* <f:format.stripTags allowedTags="<p><span><div><script>">
* <p>paragraph</p><span>span</span><div>divider</div><iframe>iframe</iframe><script>script</script>
* </f:format.stripTags>
*
* Output::
*
* <p>paragraph</p><span>span</span><div>divider</div>iframe<script>script</script>
*
* Inline notation
* ---------------
*
* ::
*
* {text -> f:format.stripTags()}
*
* Text without tags :php:`strip_tags()` applied.
*
* Inline notation with allowedTags
* --------------------------------
*
* ::
*
* {text -> f:format.stripTags(allowedTags: "<p><span><div><script>")}
*
* Text with p, span, div and script Tags inside, all other tags are removed.
*/
final class StripTagsViewHelper extends AbstractViewHelper
{
use CompileWithContentArgumentAndRenderStatic;

/**
* No output escaping as some tags may be allowed
*
* @var bool
*/
protected $escapeOutput = false;

public function initializeArguments(): void
{
$this->registerArgument('value', 'string', 'string to format');
$this->registerArgument('allowedTags', 'string', 'Optional string of allowed tags as required by PHPs strip_tags() function');
}

/**
* To ensure all tags are removed, child node's output must not be escaped
*
* @var bool
*/
protected $escapeChildren = false;

/**
* Applies strip_tags() on the specified value if it's string-able.
*
* @see https://www.php.net/manual/function.strip-tags.php
* @return string
*/
public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): string
{
$value = $renderChildrenClosure();
$allowedTags = $arguments['allowedTags'];

if (is_array($value)) {
throw new \InvalidArgumentException('Specified array cannot be converted to string.', 1700819707);
}
if (is_object($value) && !($value instanceof Stringable)) {
throw new \InvalidArgumentException('Specified object cannot be converted to string.', 1700819706);
}
return strip_tags((string)$value, $allowedTags);
}

/**
* Explicitly set argument name to be used as content.
*/
public function resolveContentArgumentName(): string
{
return 'value';
}
}
119 changes: 119 additions & 0 deletions tests/Functional/ViewHelpers/Format/StripTagsViewHelperTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?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\Tests\Functional\ViewHelpers\Format;

use stdClass;
use TYPO3Fluid\Fluid\Tests\Functional\AbstractFunctionalTestCase;
use TYPO3Fluid\Fluid\View\TemplateView;

final class StripTagsViewHelperTest extends AbstractFunctionalTestCase
{
public static function renderDataProvider(): array
{
return [
'renderUsesValueAsSourceIfSpecified' => [
'<f:format.stripTags value="Some string" />',
'Some string',
],
'int as input' => [
'<f:format.stripTags value="123" />',
'123',
],
'renderUsesChildnodesAsSourceIfSpecified' => [
'<f:format.stripTags>Some string</f:format.stripTags>',
'Some string',
],
'no special chars' => [
'<f:format.stripTags>This is a sample text without special characters.</f:format.stripTags>',
'This is a sample text without special characters.',
],
'some tags' => [
'<f:format.stripTags>This is a sample text <b>with <i>some</i> tags</b>.</f:format.stripTags>',
'This is a sample text with some tags.',
],
'some umlauts' => [
'<f:format.stripTags>This text contains some &quot;&Uuml;mlaut&quot;.</f:format.stripTags>',
'This text contains some &quot;&Uuml;mlaut&quot;.',
],
'allowed tags' => [
'<f:format.stripTags allowedTags="<strong>">This text <i>contains</i> some <strong>allowed</strong> tags.</f:format.stripTags>',
'This text contains some <strong>allowed</strong> tags.',
],
];
}

/**
* @test
* @dataProvider renderDataProvider
*/
public function render(string $template, string $expected): void
{
$view = new TemplateView();
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($template);
self::assertSame($expected, $view->render());

$view = new TemplateView();
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($template);
self::assertSame($expected, $view->render());
}

/**
* Ensures that objects are handled properly:
* + class having __toString() method gets tags stripped off
*
* @test
*/
public function renderEscapesObjectIfPossible(): void
{
$toStringClass = new class () {
public function __toString(): string
{
return '<script>alert(\'"xss"\')</script>';
}
};
$view = new TemplateView();
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource('<f:format.stripTags>{value}</f:format.stripTags>');
$view->assign('value', $toStringClass);
self::assertEquals('alert(\'"xss"\')', $view->render());
}

public static function throwsExceptionForInvalidInputDataProvider(): array
{
return [
'array input' => [
[1, 2, 3],
1700819707,
'Specified array cannot be converted to string.',
],
'object input' => [
new stdClass(),
1700819706,
'Specified object cannot be converted to string.',
],
];
}

/**
* @test
* @dataProvider throwsExceptionForInvalidInputDataProvider
*/
public function throwsExceptionForInvalidInput(mixed $value, int $expectedExceptionCode, string $expectedExceptionMessage): void
{
self::expectExceptionCode($expectedExceptionCode);
self::expectExceptionMessage($expectedExceptionMessage);
$view = new TemplateView();
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource('<f:format.stripTags>{value}</f:format.stripTags>');
$view->assign('value', $value);
$view->render();
}
}

0 comments on commit 1e258a8

Please sign in to comment.