Skip to content

Commit 4e97e11

Browse files
liulka-oxidAshrafOxid
authored andcommitted
OXDEV-5017 Add HTML sanitize extension
1 parent 0f4df4d commit 4e97e11

6 files changed

Lines changed: 133 additions & 7 deletions

File tree

CHANGELOG-2.x.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
# Change Log for OXID Twig engine component
22

3+
## v2.8.0 - unreleased
4+
5+
### Added
6+
- Template Filter to sanitize HTML content
7+
8+
### Changed
9+
- Extension `include_content` uses HTML sanitizer
10+
311
## v2.7.0 - unreleased
412

513
### Added
614
- Improved template rendering performance
15+
- Template Filter to sanitize HTML content
716

817
## v2.6.0 - 2025-04-09
918

services.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,9 @@ services:
259259
OxidEsales\Twig\Extensions\IncludeContentExtension:
260260
tags: ['twig.extension']
261261

262+
OxidEsales\Twig\Extensions\Filters\SanitizeHtmlExtension:
263+
tags: ['twig.extension']
264+
262265
OxidEsales\EshopCommunity\Internal\Framework\Templating\TemplateEngineInterface:
263266
class: OxidEsales\Twig\TwigEngine
264267
arguments:
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/**
4+
* Copyright © OXID eSales AG. All rights reserved.
5+
* See LICENSE file for license details.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OxidEsales\Twig\Extensions\Filters;
11+
12+
use OxidEsales\EshopCommunity\Internal\Framework\Html\HtmlSanitizerInterface;
13+
use Twig\Extension\AbstractExtension;
14+
use Twig\TwigFilter;
15+
16+
class SanitizeHtmlExtension extends AbstractExtension
17+
{
18+
public function __construct(
19+
private readonly HtmlSanitizerInterface $sanitizer,
20+
) {
21+
}
22+
23+
public function getFilters(): array
24+
{
25+
return [
26+
new TwigFilter(
27+
'sanitize_html',
28+
$this->sanitize(...),
29+
['is_safe' => ['html']]
30+
),
31+
];
32+
}
33+
34+
public function sanitize(string $html): string
35+
{
36+
return $this->sanitizer->sanitize($html);
37+
}
38+
}

src/Extensions/IncludeContentExtension.php

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace OxidEsales\Twig\Extensions;
1111

12+
use OxidEsales\EshopCommunity\Internal\Framework\Html\HtmlSanitizerInterface;
1213
use OxidEsales\EshopCommunity\Internal\Transition\Adapter\TemplateLogic\ContentFactory;
1314
use OxidEsales\Twig\TokenParser\IncludeContentTokenParser;
1415
use Twig\Error\LoaderError;
@@ -18,8 +19,10 @@
1819

1920
class IncludeContentExtension extends AbstractExtension
2021
{
21-
public function __construct(private ContentFactory $contentFactory)
22-
{
22+
public function __construct(
23+
private ContentFactory $contentFactory,
24+
private readonly HtmlSanitizerInterface $sanitizer
25+
) {
2326
}
2427

2528
/**
@@ -50,6 +53,6 @@ public function content(string $name): string
5053
throw new LoaderError("Template is not active.");
5154
}
5255

53-
return $content->oxcontents__oxcontent->value;
56+
return $this->sanitizer->sanitize($content->oxcontents__oxcontent->value);
5457
}
5558
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
/**
4+
* Copyright © OXID eSales AG. All rights reserved.
5+
* See LICENSE file for license details.
6+
*/
7+
8+
declare(strict_types=1);
9+
10+
namespace OxidEsales\Twig\Extensions\Filters;
11+
12+
use OxidEsales\EshopCommunity\Core\Di\ContainerFacade;
13+
use OxidEsales\EshopCommunity\Internal\Framework\Html\HtmlSanitizerInterface;
14+
use OxidEsales\EshopCommunity\Tests\ContainerTrait;
15+
use OxidEsales\Twig\Tests\Integration\Extensions\AbstractExtensionTestCase;
16+
17+
final class SanitizeHtmlExtensionTest extends AbstractExtensionTestCase
18+
{
19+
use ContainerTrait;
20+
21+
private string $unsafeHtml = '<div><script> alert("SPAM MESSAGE") </script></div>';
22+
private string $safeHtml = '<div></div>';
23+
24+
protected function setUp(): void
25+
{
26+
parent::setUp();
27+
$this->createContainer();
28+
}
29+
30+
public function testSanitizerShouldEliminateUnsafeTags(): void
31+
{
32+
$this->setParameter('oxid_esales.html_sanitizer_enabled', true);
33+
$this->attachContainerToContainerFactory();
34+
$this->extension = new SanitizeHtmlExtension(ContainerFacade::get(HtmlSanitizerInterface::class));
35+
$template = "{{ '" . $this->unsafeHtml . "' | sanitize_html }}";
36+
37+
$result = $this->getTemplate($template)->render([]);
38+
39+
$this->assertEquals($this->safeHtml, $result);
40+
}
41+
42+
public function testSanitizerShouldPassEverythingWhenDisabled(): void
43+
{
44+
$this->setParameter('oxid_esales.html_sanitizer_enabled', false);
45+
$this->attachContainerToContainerFactory();
46+
$this->extension = new SanitizeHtmlExtension(ContainerFacade::get(HtmlSanitizerInterface::class));
47+
$template = "{{ '" . $this->unsafeHtml . "' | sanitize_html }}";
48+
49+
$result = $this->getTemplate($template)->render([]);
50+
51+
$this->assertEquals($this->unsafeHtml, $result);
52+
}
53+
}

tests/Integration/Extensions/IncludeContentExtensionTest.php

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
namespace OxidEsales\Twig\Tests\Integration\Extensions;
1111

1212
use OxidEsales\EshopCommunity\Application\Model\Content;
13+
use OxidEsales\EshopCommunity\Core\Di\ContainerFacade;
14+
use OxidEsales\EshopCommunity\Internal\Framework\Html\HtmlSanitizerInterface;
1315
use OxidEsales\EshopCommunity\Internal\Transition\Adapter\TemplateLogic\ContentFactory;
16+
use OxidEsales\EshopCommunity\Tests\ContainerTrait;
1417
use OxidEsales\Twig\Extensions\IncludeContentExtension;
1518
use PHPUnit\Framework\Attributes\DataProvider;
1619
use PHPUnit\Framework\MockObject\MockBuilder;
@@ -23,9 +26,11 @@
2326

2427
final class IncludeContentExtensionTest extends AbstractExtensionTestCase
2528
{
26-
private MockBuilder $contentMockBuilder;
29+
use ContainerTrait;
2730

28-
protected function setUp(): void
31+
private MockBuilder $contentMockBuilder;
32+
33+
protected function setUp(): void
2934
{
3035
parent::setUp();
3136

@@ -54,6 +59,10 @@ protected function setUp(): void
5459
'oxactive' => false,
5560
'oxcontent' => 'Not active content'
5661
]);
62+
$SpamContentMock = $this->prepareContentMock(0, [
63+
'oxactive' => true,
64+
'oxcontent' => 'not spam<script>alert("spam")</script>'
65+
]);
5766

5867
/** @var MockObject|ContentFactory $contentFactoryMock */
5968
$contentFactoryMock = $this
@@ -68,10 +77,17 @@ protected function setUp(): void
6877
['ident', 'english', $enContentMock],
6978
['ident', 'twig_code', $twigContentMock],
7079
['ident', 'dynamic_content', $dynamicContentMock],
71-
['ident', 'not_active', $notActiveContentMock]
80+
['ident', 'not_active', $notActiveContentMock],
81+
['ident', 'spam', $SpamContentMock]
7282
]);
7383

74-
$this->extension = new IncludeContentExtension($contentFactoryMock);
84+
$this->setParameter('oxid_esales.html_sanitizer_enabled', true);
85+
$this->attachContainerToContainerFactory();
86+
87+
$this->extension = new IncludeContentExtension(
88+
$contentFactoryMock,
89+
ContainerFacade::get(HtmlSanitizerInterface::class)
90+
);
7591
}
7692

7793
#[DataProvider('contentProvider')]
@@ -99,6 +115,10 @@ public static function contentProvider(): array
99115
"{% set content_name = 'dynamic_content' %}{% include_content content_name %}",
100116
"Dynamic content"
101117
],
118+
[
119+
"{% set content_name = 'spam' %}{% include_content content_name %}",
120+
"not spam"
121+
],
102122
];
103123
}
104124

0 commit comments

Comments
 (0)