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:
Justin Hileman
2012-03-14 20:07:16 -07:00
parent 2bd3804b1c
commit 23a4bb04f2
4 changed files with 94 additions and 14 deletions
+26 -7
View File
@@ -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.
* *
+34 -2
View File
@@ -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);
} }
/** /**
+17 -5
View File
@@ -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;',
) )
), ),
+17
View File
@@ -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