Skip to content

Commit 5814d93

Browse files
committed
Add escapeExpression method and document SafeString usage
1 parent 88ffc4f commit 5814d93

13 files changed

+82
-100
lines changed

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,15 @@ echo $template(['my_var' => 1]); // Not equal
103103
echo $template(['my_var' => null]); // Not equal
104104
```
105105

106+
## String Escaping
107+
108+
If a custom helper is executed in a `{{ }}` expression, the return value will be HTML escaped.
109+
When a helper is executed in a `{{{ }}}` expression, the original return value will be output directly.
110+
111+
Helpers may return a `DevTheorem\Handlebars\SafeString` instance to prevent escaping the return value.
112+
When constructing the string that will be marked as safe, any external content should be properly escaped
113+
using the `Handlebars::escapeExpression()` method to avoid potential security concerns.
114+
106115
## Unsupported Features
107116

108117
* `{{foo/bar}}` style variables (deprecated in official Handlebars.js). Instead use: `{{foo.bar}}`.

src/Compiler.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ final class Compiler extends Validator
1515
*/
1616
public static function compileTemplate(Context $context, string $template): string
1717
{
18+
$template = addcslashes($template, '\\');
1819
array_unshift($context->parsed, []);
1920
Validator::verify($context, $template);
2021
static::$lastParsed = $context->parsed;

src/Encoder.php

Lines changed: 0 additions & 31 deletions
This file was deleted.

src/Handlebars.php

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ public static function precompile(string $template, Options $options = new Optio
2525
$context = new Context($options);
2626
static::handleError($context);
2727

28-
$code = Compiler::compileTemplate($context, SafeString::escapeTemplate($template));
28+
$code = Compiler::compileTemplate($context, $template);
2929
static::$lastParsed = Compiler::$lastParsed;
3030
static::handleError($context);
3131

@@ -41,6 +41,19 @@ public static function template(string $templateSpec): \Closure
4141
return eval($templateSpec);
4242
}
4343

44+
/**
45+
* HTML escapes the passed string, making it safe for rendering as text within HTML content.
46+
* The output of all expressions except for triple-braced expressions are passed through this method.
47+
* Helpers should also use this method when returning HTML content via a SafeString instance,
48+
* to prevent possible code injection.
49+
*/
50+
public static function escapeExpression(string $string): string
51+
{
52+
$search = ['&', '<', '>', '"', "'", '`', '='];
53+
$replace = ['&amp;', '&lt;', '&gt;', '&quot;', '&#x27;', '&#x60;', '&#x3D;'];
54+
return str_replace($search, $replace, $string);
55+
}
56+
4457
/**
4558
* @throws \Exception
4659
*/

src/Parser.php

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
*/
88
final class Parser
99
{
10+
public const IS_SUBEXP_SEARCH = '/^\(.+\)$/s';
11+
1012
// Compile time error handling flags
1113
public const BLOCKPARAM = 9999;
1214
public const PARTIALBLOCK = 9998;
@@ -179,7 +181,7 @@ public static function parse(array $token, Context $context): array
179181
*/
180182
public static function getPartialName(array $vars, int $pos = 0): ?array
181183
{
182-
if (!isset($vars[$pos]) || preg_match(SafeString::IS_SUBEXP_SEARCH, $vars[$pos])) {
184+
if (!isset($vars[$pos]) || preg_match(static::IS_SUBEXP_SEARCH, $vars[$pos])) {
183185
return null;
184186
}
185187
assert(is_string($vars[$pos]));
@@ -229,14 +231,14 @@ protected static function advancedVariable(array $vars, Context $context, string
229231
$i = 0;
230232
foreach ($vars as $idx => $var) {
231233
// handle (...)
232-
if (preg_match(SafeString::IS_SUBEXP_SEARCH, $var)) {
234+
if (preg_match(static::IS_SUBEXP_SEARCH, $var)) {
233235
$ret[$i] = static::subexpression($var, $context);
234236
$i++;
235237
continue;
236238
}
237239

238240
// handle |...|
239-
if (preg_match(SafeString::IS_BLOCKPARAM_SEARCH, $var, $matched)) {
241+
if (preg_match('/^ +\|(.+)\|$/s', $var, $matched)) {
240242
$ret[static::BLOCKPARAM] = preg_split('/\s+/', trim($matched[1]));
241243
continue;
242244
}
@@ -245,7 +247,7 @@ protected static function advancedVariable(array $vars, Context $context, string
245247
$idx = $m[3] ?: $m[4];
246248
$var = $m[5];
247249
// handle foo=(...)
248-
if (preg_match(SafeString::IS_SUBEXP_SEARCH, $var)) {
250+
if (preg_match(static::IS_SUBEXP_SEARCH, $var)) {
249251
$ret[$idx] = static::subexpression($var, $context);
250252
continue;
251253
}

src/Partial.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public static function read(Context $context, string $name): void
3232
$cnt = static::resolve($context, $name);
3333

3434
if ($cnt !== null) {
35-
$context->usedPartial[$name] = SafeString::escapeTemplate($cnt);
35+
$context->usedPartial[$name] = $cnt;
3636
static::compileDynamic($context, $name);
3737
return;
3838
}

src/Runtime.php

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,19 +60,17 @@ public static function isec(mixed $v): bool
6060
}
6161

6262
/**
63-
* For {{var}} , do html encode just like handlebars.js .
63+
* HTML encode {{var}} just like handlebars.js
6464
*
6565
* @param array<array<mixed>|string|int>|string|SafeString|int|null $var value to be htmlencoded
66-
*
67-
* @return string The htmlencoded value of the specified variable
6866
*/
6967
public static function encq($var): string
7068
{
7169
if ($var instanceof SafeString) {
7270
return (string) $var;
7371
}
7472

75-
return Encoder::encq($var);
73+
return Handlebars::escapeExpression(static::raw($var));
7674
}
7775

7876
/**

src/SafeString.php

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,34 +2,22 @@
22

33
namespace DevTheorem\Handlebars;
44

5+
/**
6+
* Can be returned from a custom helper to prevent an HTML string from being escaped
7+
* when the template is rendered. When constructing, any external content should be
8+
* properly escaped using Handlebars::escapeExpression() to avoid potential security concerns.
9+
*/
510
class SafeString implements \Stringable
611
{
7-
public const EXTENDED_COMMENT_SEARCH = '/{{!--.*?--}}/s';
8-
public const IS_SUBEXP_SEARCH = '/^\(.+\)$/s';
9-
public const IS_BLOCKPARAM_SEARCH = '/^ +\|(.+)\|$/s';
10-
1112
private string $string;
1213

1314
public function __construct(string $string)
1415
{
1516
$this->string = $string;
1617
}
1718

18-
public function __toString()
19+
public function __toString(): string
1920
{
2021
return $this->string;
2122
}
22-
23-
/**
24-
* Strip extended comments {{!-- .... --}}
25-
*/
26-
public static function stripExtendedComments(string $template): string
27-
{
28-
return preg_replace(static::EXTENDED_COMMENT_SEARCH, '{{! }}', $template);
29-
}
30-
31-
public static function escapeTemplate(string $template): string
32-
{
33-
return addcslashes($template, '\\');
34-
}
3523
}

src/Validator.php

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,22 @@
77
*/
88
class Validator
99
{
10+
public const EXTENDED_COMMENT_SEARCH = '/{{!--.*?--}}/s';
11+
12+
/**
13+
* Strip extended comments {{!-- .... --}}
14+
*/
15+
public static function stripExtendedComments(string $template): string
16+
{
17+
return preg_replace(static::EXTENDED_COMMENT_SEARCH, '{{! }}', $template);
18+
}
19+
1020
/**
1121
* Verify template
1222
*/
1323
public static function verify(Context $context, string $template): void
1424
{
15-
$template = SafeString::stripExtendedComments($template);
25+
$template = static::stripExtendedComments($template);
1626
$context->level = 0;
1727
Token::setDelimiter($context);
1828

tests/EncoderTest.php

Lines changed: 0 additions & 24 deletions
This file was deleted.

tests/HandlebarsTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace DevTheorem\Handlebars\Test;
4+
5+
use DevTheorem\Handlebars\Handlebars;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class HandlebarsTest extends TestCase
9+
{
10+
public function testEscapeExpression(): void
11+
{
12+
$this->assertSame('a&amp;&#x27;b', Handlebars::escapeExpression("a&'b"));
13+
$this->assertSame('&lt;&gt;&quot;', Handlebars::escapeExpression('<>"'));
14+
$this->assertSame('&#x60;a&#x3D;b', Handlebars::escapeExpression('`a=b'));
15+
}
16+
}

tests/SafeStringTest.php

Lines changed: 0 additions & 16 deletions
This file was deleted.

tests/ValidatorTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
namespace DevTheorem\Handlebars\Test;
4+
5+
use DevTheorem\Handlebars\Validator;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class ValidatorTest extends TestCase
9+
{
10+
public function testStripExtendedComments(): void
11+
{
12+
$this->assertSame('abc', Validator::stripExtendedComments('abc'));
13+
$this->assertSame('abc{{!}}cde', Validator::stripExtendedComments('abc{{!}}cde'));
14+
$this->assertSame('abc{{! }}cde', Validator::stripExtendedComments('abc{{!----}}cde'));
15+
}
16+
}

0 commit comments

Comments
 (0)