Add custom variable escapers.
cf. #86 * Add `escape` Mustache constructor option. Expects something callable. * Update Compiler to either inline `htmlspecialchars` (old way) or a call to the custom escaper callback. * Update Compiler to statically compile charset. * Update template classname to account for charset and custom compilers.
This commit is contained in:
@@ -21,6 +21,8 @@ class Compiler {
|
|||||||
private $sections;
|
private $sections;
|
||||||
private $source;
|
private $source;
|
||||||
private $indentNextLine;
|
private $indentNextLine;
|
||||||
|
private $customEscape;
|
||||||
|
private $charset;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compile a Mustache token parse tree into PHP source code.
|
* Compile a Mustache token parse tree into PHP source code.
|
||||||
@@ -31,10 +33,12 @@ class Compiler {
|
|||||||
*
|
*
|
||||||
* @return string Generated PHP source code
|
* @return string Generated PHP source code
|
||||||
*/
|
*/
|
||||||
public function compile($source, array $tree, $name) {
|
public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8') {
|
||||||
$this->sections = array();
|
$this->sections = array();
|
||||||
$this->source = $source;
|
$this->source = $source;
|
||||||
$this->indentNextLine = true;
|
$this->indentNextLine = true;
|
||||||
|
$this->customEscape = $customEscape;
|
||||||
|
$this->charset = $charset;
|
||||||
|
|
||||||
return $this->writeCode($tree, $name);
|
return $this->writeCode($tree, $name);
|
||||||
}
|
}
|
||||||
@@ -116,10 +120,10 @@ class Compiler {
|
|||||||
%s
|
%s
|
||||||
|
|
||||||
if ($escape) {
|
if ($escape) {
|
||||||
return htmlspecialchars($buffer, ENT_COMPAT, $this->mustache->getCharset());
|
return %s;
|
||||||
|
} else {
|
||||||
|
return $buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $buffer;
|
|
||||||
}
|
}
|
||||||
%s
|
%s
|
||||||
}';
|
}';
|
||||||
@@ -136,7 +140,7 @@ class Compiler {
|
|||||||
$code = $this->walk($tree);
|
$code = $this->walk($tree);
|
||||||
$sections = implode("\n", $this->sections);
|
$sections = implode("\n", $this->sections);
|
||||||
|
|
||||||
return sprintf($this->prepare(self::KLASS, 0, false), $name, $code, $sections);
|
return sprintf($this->prepare(self::KLASS, 0, false), $name, $code, $this->getEscape('$buffer'), $sections);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SECTION_CALL = '
|
const SECTION_CALL = '
|
||||||
@@ -247,7 +251,6 @@ class Compiler {
|
|||||||
}
|
}
|
||||||
$buffer .= %s%s;
|
$buffer .= %s%s;
|
||||||
';
|
';
|
||||||
const VARIABLE_ESCAPED = 'htmlspecialchars($value, ENT_COMPAT, $this->mustache->getCharset())';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate Mustache Template variable interpolation PHP source.
|
* Generate Mustache Template variable interpolation PHP source.
|
||||||
@@ -261,7 +264,7 @@ class Compiler {
|
|||||||
private function variable($id, $escape, $level) {
|
private function variable($id, $escape, $level) {
|
||||||
$method = $this->getFindMethod($id);
|
$method = $this->getFindMethod($id);
|
||||||
$id = ($method !== 'last') ? var_export($id, true) : '';
|
$id = ($method !== 'last') ? var_export($id, true) : '';
|
||||||
$value = $escape ? self::VARIABLE_ESCAPED : '$value';
|
$value = $escape ? $this->getEscape() : '$value';
|
||||||
|
|
||||||
return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $this->flushIndent(), $value);
|
return sprintf($this->prepare(self::VARIABLE, $level), $method, $id, $this->flushIndent(), $value);
|
||||||
}
|
}
|
||||||
@@ -305,6 +308,22 @@ class Compiler {
|
|||||||
return preg_replace("/\n(\t\t)?/", "\n".str_repeat("\t", $bonus), $text);
|
return preg_replace("/\n(\t\t)?/", "\n".str_repeat("\t", $bonus), $text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_ESCAPE = 'htmlspecialchars(%s, ENT_COMPAT, %s)';
|
||||||
|
const CUSTOM_ESCAPE = 'call_user_func($this->mustache->getEscape(), %s)';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current escaper.
|
||||||
|
*
|
||||||
|
* @return string Either a custom callback, or an inline call to `htmlspecialchars`
|
||||||
|
*/
|
||||||
|
private function getEscape($value = '$value') {
|
||||||
|
if ($this->customEscape) {
|
||||||
|
return sprintf(self::CUSTOM_ESCAPE, $value);
|
||||||
|
} else {
|
||||||
|
return sprintf(self::DEFAULT_ESCAPE, $value, var_export($this->charset, true));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Select the appropriate Context `find` method for a given $id.
|
* Select the appropriate Context `find` method for a given $id.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ class Mustache {
|
|||||||
private $loader;
|
private $loader;
|
||||||
private $partialsLoader;
|
private $partialsLoader;
|
||||||
private $helpers;
|
private $helpers;
|
||||||
|
private $escape;
|
||||||
private $charset = 'UTF-8';
|
private $charset = 'UTF-8';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +72,11 @@ class Mustache {
|
|||||||
* // do something translatey here...
|
* // do something translatey here...
|
||||||
* }),
|
* }),
|
||||||
*
|
*
|
||||||
|
* // An 'escape' callback, responsible for escaping double-mustache variables.
|
||||||
|
* 'escape' => function($value) {
|
||||||
|
* return htmlspecialchars($buffer, ENT_COMPAT, 'UTF-8');
|
||||||
|
* },
|
||||||
|
*
|
||||||
* // character set for `htmlspecialchars`. Defaults to 'UTF-8'
|
* // character set for `htmlspecialchars`. Defaults to 'UTF-8'
|
||||||
* 'charset' => 'ISO-8859-1',
|
* 'charset' => 'ISO-8859-1',
|
||||||
* );
|
* );
|
||||||
@@ -102,6 +108,14 @@ class Mustache {
|
|||||||
$this->setHelpers($options['helpers']);
|
$this->setHelpers($options['helpers']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isset($options['escape'])) {
|
||||||
|
if (!is_callable($options['escape'])) {
|
||||||
|
throw new \InvalidArgumentException('Mustache Constructor "escape" option must be callable');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->escape = $options['escape'];
|
||||||
|
}
|
||||||
|
|
||||||
if (isset($options['charset'])) {
|
if (isset($options['charset'])) {
|
||||||
$this->charset = $options['charset'];
|
$this->charset = $options['charset'];
|
||||||
}
|
}
|
||||||
@@ -124,6 +138,15 @@ class Mustache {
|
|||||||
return $this->loadTemplate($template)->render($data);
|
return $this->loadTemplate($template)->render($data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current Mustache escape callback.
|
||||||
|
*
|
||||||
|
* @return mixed Callable or null
|
||||||
|
*/
|
||||||
|
public function getEscape() {
|
||||||
|
return $this->escape;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current Mustache character set.
|
* Get the current Mustache character set.
|
||||||
*
|
*
|
||||||
@@ -366,7 +389,13 @@ class Mustache {
|
|||||||
* @return string Mustache Template class name
|
* @return string Mustache Template class name
|
||||||
*/
|
*/
|
||||||
public function getTemplateClassName($source) {
|
public function getTemplateClassName($source) {
|
||||||
return $this->templateClassPrefix . md5(self::VERSION . ':' . $source);
|
return $this->templateClassPrefix . md5(sprintf(
|
||||||
|
'version:%s,escape:%s,charset:%s,source:%s',
|
||||||
|
self::VERSION,
|
||||||
|
isset($this->escape) ? 'custom' : 'default',
|
||||||
|
$this->charset,
|
||||||
|
$source
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -482,7 +511,10 @@ class Mustache {
|
|||||||
* @return string generated Mustache template class code
|
* @return string generated Mustache template class code
|
||||||
*/
|
*/
|
||||||
private function compile($source) {
|
private function compile($source) {
|
||||||
return $this->getCompiler()->compile($source, $this->parse($source), $this->getTemplateClassName($source));
|
$tree = $this->parse($source);
|
||||||
|
$name = $this->getTemplateClassName($source);
|
||||||
|
|
||||||
|
return $this->getCompiler()->compile($source, $tree, $name, isset($this->escape), $this->charset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ class CompilerTest extends \PHPUnit_Framework_TestCase {
|
|||||||
/**
|
/**
|
||||||
* @dataProvider getCompileValues
|
* @dataProvider getCompileValues
|
||||||
*/
|
*/
|
||||||
public function testCompile($source, array $tree, $name, $expected) {
|
public function testCompile($source, array $tree, $name, $customEscaper, $charset, $expected) {
|
||||||
$compiler = new Compiler;
|
$compiler = new Compiler;
|
||||||
|
|
||||||
$compiled = $compiler->compile($source, $tree, $name);
|
$compiled = $compiler->compile($source, $tree, $name, $customEscaper, $charset);
|
||||||
foreach ($expected as $contains) {
|
foreach ($expected as $contains) {
|
||||||
$this->assertContains($contains, $compiled);
|
$this->assertContains($contains, $compiled);
|
||||||
}
|
}
|
||||||
@@ -33,14 +33,23 @@ class CompilerTest extends \PHPUnit_Framework_TestCase {
|
|||||||
|
|
||||||
public function getCompileValues() {
|
public function getCompileValues() {
|
||||||
return array(
|
return array(
|
||||||
array('', array(), 'Banana', array(
|
array('', array(), 'Banana', false, 'ISO-8859-1', array(
|
||||||
"\nclass Banana extends \Mustache\Template",
|
"\nclass Banana extends \Mustache\Template",
|
||||||
|
'return htmlspecialchars($buffer, ENT_COMPAT, \'ISO-8859-1\');',
|
||||||
'return $buffer;',
|
'return $buffer;',
|
||||||
)),
|
)),
|
||||||
|
|
||||||
array('', array('TEXT'), 'Monkey', array(
|
array('', array('TEXT'), 'Monkey', false, 'UTF-8', array(
|
||||||
|
"\nclass Monkey extends \Mustache\Template",
|
||||||
|
'return htmlspecialchars($buffer, ENT_COMPAT, \'UTF-8\');',
|
||||||
|
'$buffer .= $indent . \'TEXT\';',
|
||||||
|
'return $buffer;',
|
||||||
|
)),
|
||||||
|
|
||||||
|
array('', array('TEXT'), 'Monkey', true, 'ISO-8859-1', array(
|
||||||
"\nclass Monkey extends \Mustache\Template",
|
"\nclass Monkey extends \Mustache\Template",
|
||||||
'$buffer .= $indent . \'TEXT\';',
|
'$buffer .= $indent . \'TEXT\';',
|
||||||
|
'return call_user_func($this->mustache->getEscape(), $buffer);',
|
||||||
'return $buffer;',
|
'return $buffer;',
|
||||||
)),
|
)),
|
||||||
|
|
||||||
@@ -60,14 +69,17 @@ class CompilerTest extends \PHPUnit_Framework_TestCase {
|
|||||||
"'bar'",
|
"'bar'",
|
||||||
),
|
),
|
||||||
'Monkey',
|
'Monkey',
|
||||||
|
false,
|
||||||
|
'UTF-8',
|
||||||
array(
|
array(
|
||||||
"\nclass Monkey extends \Mustache\Template",
|
"\nclass Monkey extends \Mustache\Template",
|
||||||
'$buffer .= $indent . \'foo\'',
|
'$buffer .= $indent . \'foo\'',
|
||||||
'$buffer .= "\n"',
|
'$buffer .= "\n"',
|
||||||
'$value = $context->find(\'name\');',
|
'$value = $context->find(\'name\');',
|
||||||
'$buffer .= htmlspecialchars($value, ENT_COMPAT, $this->mustache->getCharset());',
|
'$buffer .= htmlspecialchars($value, ENT_COMPAT, \'UTF-8\');',
|
||||||
'$value = $context->last();',
|
'$value = $context->last();',
|
||||||
'$buffer .= \'\\\'bar\\\'\';',
|
'$buffer .= \'\\\'bar\\\'\';',
|
||||||
|
'return htmlspecialchars($buffer, ENT_COMPAT, \'UTF-8\');',
|
||||||
'return $buffer;',
|
'return $buffer;',
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ class MustacheTest extends \PHPUnit_Framework_TestCase {
|
|||||||
'foo' => function() { return 'foo'; },
|
'foo' => function() { return 'foo'; },
|
||||||
'bar' => 'BAR',
|
'bar' => 'BAR',
|
||||||
),
|
),
|
||||||
|
'escape' => 'strtoupper',
|
||||||
'charset' => 'ISO-8859-1',
|
'charset' => 'ISO-8859-1',
|
||||||
));
|
));
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ class MustacheTest extends \PHPUnit_Framework_TestCase {
|
|||||||
$this->assertSame($partialsLoader, $mustache->getPartialsLoader());
|
$this->assertSame($partialsLoader, $mustache->getPartialsLoader());
|
||||||
$this->assertEquals('{{ foo }}', $partialsLoader->load('foo'));
|
$this->assertEquals('{{ foo }}', $partialsLoader->load('foo'));
|
||||||
$this->assertContains('__whot__', $mustache->getTemplateClassName('{{ foo }}'));
|
$this->assertContains('__whot__', $mustache->getTemplateClassName('{{ foo }}'));
|
||||||
|
$this->assertEquals('strtoupper', $mustache->getEscape());
|
||||||
$this->assertEquals('ISO-8859-1', $mustache->getCharset());
|
$this->assertEquals('ISO-8859-1', $mustache->getCharset());
|
||||||
$this->assertTrue($mustache->hasHelper('foo'));
|
$this->assertTrue($mustache->hasHelper('foo'));
|
||||||
$this->assertTrue($mustache->hasHelper('bar'));
|
$this->assertTrue($mustache->hasHelper('bar'));
|
||||||
@@ -127,6 +129,21 @@ class MustacheTest extends \PHPUnit_Framework_TestCase {
|
|||||||
$this->assertContains("\nclass $className extends \Mustache\Template", file_get_contents($fileName));
|
$this->assertContains("\nclass $className extends \Mustache\Template", file_get_contents($fileName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @expectedException \InvalidArgumentException
|
||||||
|
* @dataProvider getBadEscapers
|
||||||
|
*/
|
||||||
|
public function testNonCallableEscapeThrowsException($escape) {
|
||||||
|
new Mustache(array('escape' => $escape));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBadEscapers() {
|
||||||
|
return array(
|
||||||
|
array('nothing'),
|
||||||
|
array('foo', 'bar'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @group functional
|
* @group functional
|
||||||
* @expectedException \RuntimeException
|
* @expectedException \RuntimeException
|
||||||
|
|||||||
Reference in New Issue
Block a user