ソースを参照

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.
Justin Hileman 13 年 前
コミット
23a4bb04f2

+ 26 - 7
src/Mustache/Compiler.php

@@ -21,6 +21,8 @@ class Compiler {
 	private $sections;
 	private $source;
 	private $indentNextLine;
+	private $customEscape;
+	private $charset;
 
 	/**
 	 * Compile a Mustache token parse tree into PHP source code.
@@ -31,10 +33,12 @@ class Compiler {
 	 *
 	 * @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->source         = $source;
 		$this->indentNextLine = true;
+		$this->customEscape   = $customEscape;
+		$this->charset        = $charset;
 
 		return $this->writeCode($tree, $name);
 	}
@@ -116,10 +120,10 @@ class Compiler {
 		%s
 
 				if ($escape) {
-					return htmlspecialchars($buffer, ENT_COMPAT, $this->mustache->getCharset());
+					return %s;
+				} else {
+					return $buffer;
 				}
-
-				return $buffer;
 			}
 		%s
 		}';
@@ -136,7 +140,7 @@ class Compiler {
 		$code     = $this->walk($tree);
 		$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 = '
@@ -247,7 +251,6 @@ class Compiler {
 		}
 		$buffer .= %s%s;
 	';
-	const VARIABLE_ESCAPED = 'htmlspecialchars($value, ENT_COMPAT, $this->mustache->getCharset())';
 
 	/**
 	 * Generate Mustache Template variable interpolation PHP source.
@@ -261,7 +264,7 @@ class Compiler {
 	private function variable($id, $escape, $level) {
 		$method = $this->getFindMethod($id);
 		$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);
 	}
@@ -305,6 +308,22 @@ class Compiler {
 		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.
 	 *

+ 34 - 2
src/Mustache/Mustache.php

@@ -40,6 +40,7 @@ class Mustache {
 	private $loader;
 	private $partialsLoader;
 	private $helpers;
+	private $escape;
 	private $charset = 'UTF-8';
 
 	/**
@@ -71,6 +72,11 @@ class Mustache {
 	 *              // 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'
 	 *         'charset' => 'ISO-8859-1',
 	 *     );
@@ -102,6 +108,14 @@ class Mustache {
 			$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'])) {
 			$this->charset = $options['charset'];
 		}
@@ -124,6 +138,15 @@ class Mustache {
 		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.
 	 *
@@ -366,7 +389,13 @@ class Mustache {
 	 * @return string Mustache Template class name
 	 */
 	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
 	 */
 	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
test/Mustache/Test/CompilerTest.php

@@ -22,10 +22,10 @@ class CompilerTest extends \PHPUnit_Framework_TestCase {
 	/**
 	 * @dataProvider getCompileValues
 	 */
-	public function testCompile($source, array $tree, $name, $expected) {
+	public function testCompile($source, array $tree, $name, $customEscaper, $charset, $expected) {
 		$compiler = new Compiler;
 
-		$compiled = $compiler->compile($source, $tree, $name);
+		$compiled = $compiler->compile($source, $tree, $name, $customEscaper, $charset);
 		foreach ($expected as $contains) {
 			$this->assertContains($contains, $compiled);
 		}
@@ -33,17 +33,26 @@ class CompilerTest extends \PHPUnit_Framework_TestCase {
 
 	public function getCompileValues() {
 		return array(
-			array('', array(), 'Banana', array(
+			array('', array(), 'Banana', false, 'ISO-8859-1', array(
 				"\nclass Banana extends \Mustache\Template",
+				'return htmlspecialchars($buffer, ENT_COMPAT, \'ISO-8859-1\');',
 				'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",
+				'$buffer .= $indent . \'TEXT\';',
+				'return call_user_func($this->mustache->getEscape(), $buffer);',
+				'return $buffer;',
+			)),
+
 			array(
 				'',
 				array(
@@ -60,14 +69,17 @@ class CompilerTest extends \PHPUnit_Framework_TestCase {
 					"'bar'",
 				),
 				'Monkey',
+				false,
+				'UTF-8',
 				array(
 					"\nclass Monkey extends \Mustache\Template",
 					'$buffer .= $indent . \'foo\'',
 					'$buffer .= "\n"',
 					'$value = $context->find(\'name\');',
-					'$buffer .= htmlspecialchars($value, ENT_COMPAT, $this->mustache->getCharset());',
+					'$buffer .= htmlspecialchars($value, ENT_COMPAT, \'UTF-8\');',
 					'$value = $context->last();',
 					'$buffer .= \'\\\'bar\\\'\';',
+					'return htmlspecialchars($buffer, ENT_COMPAT, \'UTF-8\');',
 					'return $buffer;',
 				)
 			),

+ 17 - 0
test/Mustache/Test/MustacheTest.php

@@ -47,6 +47,7 @@ class MustacheTest extends \PHPUnit_Framework_TestCase {
 				'foo' => function() { return 'foo'; },
 				'bar' => 'BAR',
 			),
+			'escape'  => 'strtoupper',
 			'charset' => 'ISO-8859-1',
 		));
 
@@ -54,6 +55,7 @@ class MustacheTest extends \PHPUnit_Framework_TestCase {
 		$this->assertSame($partialsLoader, $mustache->getPartialsLoader());
 		$this->assertEquals('{{ foo }}', $partialsLoader->load('foo'));
 		$this->assertContains('__whot__', $mustache->getTemplateClassName('{{ foo }}'));
+		$this->assertEquals('strtoupper', $mustache->getEscape());
 		$this->assertEquals('ISO-8859-1', $mustache->getCharset());
 		$this->assertTrue($mustache->hasHelper('foo'));
 		$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));
 	}
 
+	/**
+	 * @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
 	 * @expectedException \RuntimeException