From e5480849061d60e05ba279f4e566d7a6aa53d2d0 Mon Sep 17 00:00:00 2001 From: pine3ree Date: Tue, 13 Jun 2023 17:39:52 +0200 Subject: [PATCH 1/6] Add Escaper extension for classic escaper template functions Signed-off-by: pine3ree --- src/Extension/Escaper.php | 128 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/Extension/Escaper.php diff --git a/src/Extension/Escaper.php b/src/Extension/Escaper.php new file mode 100644 index 00000000..c8bb89b6 --- /dev/null +++ b/src/Extension/Escaper.php @@ -0,0 +1,128 @@ + ENT_HTML401, + ENT_HTML5 => ENT_HTML5, + ENT_XHTML => ENT_XHTML, + ENT_XML1 => ENT_XML1, + ]; + + public function __construct(int $ent_doctype = null, string $encoding = null) + { + if ($ent_doctype !== null) { + $ent_doctype = $this->parseEntDoctype($ent_doctype); + if (isset($ent_doctype)) { + $this->ent_doctype = $ent_doctype; + } + } + + if (isset($encoding)) { + $this->encoding = $encoding; + } + } + + /** + * Register extension functions. + * + * @param Engine $engine + * @return null + */ + public function register(Engine $engine) + { + $engine->registerFunction('escape', array($this, 'escape')); + $engine->registerFunction('e', array($this, 'escape')); + } + + /** + * Escape string + * + * @param string $string + * @param string $functions Function pipe string expression + * @param int|null $ent_doctype Use an alternative ENT_(HTML5, XHTML, XML1) doctype constant + * @param string|null $encoding Use an alternative charset + * @param bool $double_encode + * @return string + */ + public function escape( + $string, + string $functions = null, + int $ent_doctype = null, + string $encoding = null, + bool $double_encode = true + ): string { + + if ($functions) { + $string = $this->template->batch($string, $functions); + } + + // Stop further processing of empty values + if ($string === null || $string === '') { + return ''; + } + + if ($ent_doctype !== null) { + $ent_doctype = $this->parseEntDoctype($ent_doctype); + } + + return htmlspecialchars( + (string)$string, // Perform type-casting + ($ent_doctype ?? $this->ent_doctype) | ENT_QUOTES | ENT_SUBSTITUTE, + $encoding ?? $this->encoding, + $double_encode + ); + } + + /** + * Return an ENT_* doctype constant integer value or null if the input is + * not a valid constant + * + * @param int $ent_doctype + * @return int|null + */ + protected function parseEntDoctype(int $ent_doctype) + { + return self::ENT_DOCTYPES[$ent_doctype] ?? null; + } +} From 2a886cbc6d9347112c9a93bb2d0c4d6342c1ff22 Mon Sep 17 00:00:00 2001 From: pine3ree Date: Tue, 13 Jun 2023 17:40:38 +0200 Subject: [PATCH 2/6] Remove the classic escape methods and register them from the new extesion by default Signed-off-by: pine3ree --- src/Engine.php | 3 +++ src/Template/Template.php | 32 -------------------------------- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/src/Engine.php b/src/Engine.php index c889c5e6..5b603160 100644 --- a/src/Engine.php +++ b/src/Engine.php @@ -3,6 +3,7 @@ namespace League\Plates; use League\Plates\Extension\ExtensionInterface; +use League\Plates\Extension\Escaper; use League\Plates\Template\Data; use League\Plates\Template\Directory; use League\Plates\Template\FileExtension; @@ -65,6 +66,8 @@ public function __construct($directory = null, $fileExtension = 'php') $this->functions = new Functions(); $this->data = new Data(); $this->resolveTemplatePath = new ResolveTemplatePath\NameAndFolderResolveTemplatePath(); + // Register classic escaper template functions by default + $this->loadExtension($e = new Escaper()); } public static function fromTheme(Theme $theme, string $fileExtension = 'php'): self { diff --git a/src/Template/Template.php b/src/Template/Template.php index db11a9f1..17d6ad0e 100644 --- a/src/Template/Template.php +++ b/src/Template/Template.php @@ -349,36 +349,4 @@ public function batch($var, $functions) return $var; } - - /** - * Escape string. - * @param string $string - * @param null|string $functions - * @return string - */ - public function escape($string, $functions = null) - { - static $flags; - - if (!isset($flags)) { - $flags = ENT_QUOTES | (defined('ENT_SUBSTITUTE') ? ENT_SUBSTITUTE : 0); - } - - if ($functions) { - $string = $this->batch($string, $functions); - } - - return htmlspecialchars($string ?? '', $flags, 'UTF-8'); - } - - /** - * Alias to escape function. - * @param string $string - * @param null|string $functions - * @return string - */ - public function e($string, $functions = null) - { - return $this->escape($string, $functions); - } } From 762511b7bfafd752d31f23be0694f81a4170da3f Mon Sep 17 00:00:00 2001 From: pine3ree Date: Tue, 13 Jun 2023 17:48:38 +0200 Subject: [PATCH 3/6] Add option in plates engine ctor to allow bypassing loading the classic escaper functions Signed-off-by: pine3ree --- src/Engine.php | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Engine.php b/src/Engine.php index 5b603160..d4d702ee 100644 --- a/src/Engine.php +++ b/src/Engine.php @@ -57,17 +57,22 @@ class Engine * Create new Engine instance. * @param string $directory * @param string $fileExtension + * @param bool $registerEscapeFunctions Enabled by default to avoid bc-breaks */ - public function __construct($directory = null, $fileExtension = 'php') - { + public function __construct( + $directory = null, + $fileExtension = 'php', + bool $registerEscapeFunctions = true + ) { $this->directory = new Directory($directory); $this->fileExtension = new FileExtension($fileExtension); $this->folders = new Folders(); $this->functions = new Functions(); $this->data = new Data(); $this->resolveTemplatePath = new ResolveTemplatePath\NameAndFolderResolveTemplatePath(); - // Register classic escaper template functions by default - $this->loadExtension($e = new Escaper()); + if ($registerEscapeFunctions) { + $this->loadExtension(new Escaper()); + } } public static function fromTheme(Theme $theme, string $fileExtension = 'php'): self { From d79e94fe9e2eb343b725e93c9bbf094e387b0dbb Mon Sep 17 00:00:00 2001 From: pine3ree Date: Tue, 13 Jun 2023 18:39:06 +0200 Subject: [PATCH 4/6] Add Escaper extension test cases Signed-off-by: pine3ree --- tests/Extension/EscaperTest.php | 147 ++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 tests/Extension/EscaperTest.php diff --git a/tests/Extension/EscaperTest.php b/tests/Extension/EscaperTest.php new file mode 100644 index 00000000..ba845920 --- /dev/null +++ b/tests/Extension/EscaperTest.php @@ -0,0 +1,147 @@ +assertInstanceOf(Escaper::class, new Escaper()); + $this->assertInstanceOf(Escaper::class, new Escaper(ENT_HTML5)); + $this->assertInstanceOf(Escaper::class, new Escaper(ENT_XHTML)); + $this->assertInstanceOf(Escaper::class, new Escaper(ENT_XML1)); + $this->assertInstanceOf(Escaper::class, new Escaper(ENT_HTML401, 'ISO-8859-15')); + } + + public function testThatEscaperIsAutoRegisteredByDefault() + { + $engine = new Engine(); + $this->assertTrue($engine->doesFunctionExist('escape')); + $this->assertTrue($engine->doesFunctionExist('e')); + } + + public function testThatEscaperAutoRegistrationCanBeBypassed() + { + $engine = new Engine(null, 'php', false); + $this->assertFalse($engine->doesFunctionExist('escape')); + $this->assertFalse($engine->doesFunctionExist('e')); + } + + public function testDefaultHtml401EscapeFunction() + { + $string = '<&>"\''; + $expected = '<&>"''; + + $escaper = new Escaper(); + $this->assertSame($expected, $escaper->escape($string)); + } + + public function testHtml5EscapeFunction() + { + $string = '<&>"\''; + $expected = '<&>"''; + + $escaper = new Escaper(); + $this->assertSame($expected, $escaper->escape($string, null, ENT_HTML5)); + + $escaper = new Escaper(ENT_HTML5); + $this->assertSame($expected, $escaper->escape($string)); + } + + public function testXhtmlEscapeFunction() + { + $string = '<&>"\''; + $expected = '<&>"''; + + $escaper = new Escaper(); + $this->assertSame($expected, $escaper->escape($string, null, ENT_XHTML)); + + $escaper = new Escaper(ENT_XHTML); + $this->assertSame($expected, $escaper->escape($string)); + } + + public function testXml1EscapeFunction() + { + $string = '<&>"\''; + $expected = '<&>"''; + + $escaper = new Escaper(); + $this->assertSame($expected, $escaper->escape($string, null, ENT_XML1)); + + $escaper = new Escaper(ENT_XML1); + $this->assertSame($expected, $escaper->escape($string)); + } + + public function testThatDoubleEncodeIsEnabledByDefault() + { + $string = '<&>"''; + $expected = '&lt;&amp;&gt;&quot;&apos;'; + + $escaper = new Escaper(ENT_HTML5); + $this->assertSame($expected, $escaper->escape($string)); + } + + public function testThatDoubleEncodeCanBeDisabled() + { + $escaper = new Escaper(); + + $string = '<&>"''; + + $expected = '<&>"&apos;'; + $this->assertSame($expected, $escaper->escape($string, null, null, null, false)); + + $expected = '<&>"''; + $this->assertSame($expected, $escaper->escape($string, null, ENT_HTML5, null, false)); + $this->assertSame($expected, $escaper->escape($string, null, ENT_XHTML, null, false)); + $this->assertSame($expected, $escaper->escape($string, null, ENT_XML1, null, false)); + } + + public function testThatBatchFunctionsAreCalledFromWithinTemplate() + { + vfsStream::setup('templates'); + + $engine = new Engine(vfsStream::url('templates')); + $engine->registerFunction('tr', 'trim'); + $engine->registerFunction('uc', 'strtoupper'); + + $template = new Template($engine, 'template'); + + vfsStream::create( + array( + 'template.php' => 'batch(" abc ", "tr|uc") ?>', + ) + ); + + $this->assertSame('ABC', $template->render()); + } + + public function testThatBatchFunctionsAreSkippedIfOutsideTemplate() + { + $escaper = new Escaper(ENT_HTML5); + + $engine = new Engine(null, 'php', false); + $engine->loadExtension($escaper); + + $engine->registerFunction('tr', 'trim'); + $engine->registerFunction('uc', 'strtoupper'); + + $this->assertSame('abc', $escaper->escape('abc', 'tr|uc')); + } +} From e418761cc8033d7afdd4beda6e1f332a3ee4d0d5 Mon Sep 17 00:00:00 2001 From: pine3ree Date: Tue, 13 Jun 2023 18:40:01 +0200 Subject: [PATCH 5/6] Check for template, skip batch functions if not set Signed-off-by: pine3ree --- src/Extension/Escaper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Extension/Escaper.php b/src/Extension/Escaper.php index c8bb89b6..b4d6663a 100644 --- a/src/Extension/Escaper.php +++ b/src/Extension/Escaper.php @@ -93,7 +93,7 @@ public function escape( bool $double_encode = true ): string { - if ($functions) { + if ($functions && $this->template instanceof Template) { $string = $this->template->batch($string, $functions); } From 76ebc55d3e3012554ec2b6c884032242abf6fe1b Mon Sep 17 00:00:00 2001 From: pine3ree Date: Wed, 14 Jun 2023 21:57:50 +0200 Subject: [PATCH 6/6] Add new ctor parameter to static factory method Signed-off-by: pine3ree --- src/Engine.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Engine.php b/src/Engine.php index d4d702ee..d6112572 100644 --- a/src/Engine.php +++ b/src/Engine.php @@ -75,8 +75,12 @@ public function __construct( } } - public static function fromTheme(Theme $theme, string $fileExtension = 'php'): self { - $engine = new self(null, $fileExtension); + public static function fromTheme( + Theme $theme, + string $fileExtension = 'php', + bool $registerEscapeFunctions = true + ): self { + $engine = new self(null, $fileExtension, $registerEscapeFunctions); $engine->setResolveTemplatePath(new ResolveTemplatePath\ThemeResolveTemplatePath($theme)); return $engine; }