diff --git a/src/Engine.php b/src/Engine.php index c889c5e6..d6112572 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; @@ -56,19 +57,30 @@ 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(); + if ($registerEscapeFunctions) { + $this->loadExtension(new Escaper()); + } } - 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; } diff --git a/src/Extension/Escaper.php b/src/Extension/Escaper.php new file mode 100644 index 00000000..b4d6663a --- /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 && $this->template instanceof Template) { + $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; + } +} 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); - } } 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')); + } +}