diff --git a/src/Mustache/Compiler.php b/src/Mustache/Compiler.php index 2b0d1f93..a214e4e9 100644 --- a/src/Mustache/Compiler.php +++ b/src/Mustache/Compiler.php @@ -26,6 +26,7 @@ class Mustache_Compiler private $entityFlags; private $charset; private $strictCallables; + private $strictVariables; /** * Compile a Mustache token parse tree into PHP source code. @@ -36,11 +37,12 @@ class Mustache_Compiler * @param bool $customEscape (default: false) * @param string $charset (default: 'UTF-8') * @param bool $strictCallables (default: false) + * @param bool $strictVariables (default: false) * @param int $entityFlags (default: ENT_COMPAT) * * @return string Generated PHP source code */ - public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT) + public function compile($source, array $tree, $name, $customEscape = false, $charset = 'UTF-8', $strictCallables = false, $entityFlags = ENT_COMPAT, $strictVariables = false) { $this->pragmas = $this->defaultPragmas; $this->sections = array(); @@ -51,6 +53,7 @@ public function compile($source, array $tree, $name, $customEscape = false, $cha $this->entityFlags = $entityFlags; $this->charset = $charset; $this->strictCallables = $strictCallables; + $this->strictVariables = $strictVariables; return $this->writeCode($tree, $name); } @@ -187,7 +190,7 @@ private function walk(array $tree, $level = 0) class %s extends Mustache_Template { - private $lambdaHelper;%s + private $lambdaHelper;%s%s public function renderInternal(Mustache_Context $context, $indent = \'\') { @@ -204,7 +207,7 @@ public function renderInternal(Mustache_Context $context, $indent = \'\') const KLASS_NO_LAMBDAS = 'sections) && empty($this->blocks) ? self::KLASS_NO_LAMBDAS : self::KLASS; $callable = $this->strictCallables ? $this->prepare(self::STRICT_CALLABLE) : ''; + $variable = $this->strictVariables ? $this->prepare(self::STRICT_VARIABLE) : ''; - return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $code, $sections, $blocks); + return sprintf($this->prepare($klass, 0, false, true), $name, $callable, $variable, $code, $sections, $blocks); } const BLOCK_VAR = ' @@ -322,7 +328,11 @@ private function block($nodes) } const SECTION_CALL = ' - $value = $context->%s(%s);%s + try { + $value = $context->%s(%s);%s + } catch (Mustache_Exception_UnknownVariableException $ex) { + $value = ""; + } $buffer .= $this->section%s($context, $indent, $value); '; @@ -396,7 +406,11 @@ private function section($nodes, $id, $filters, $start, $end, $otag, $ctag, $lev } const INVERTED_SECTION = ' - $value = $context->%s(%s);%s + try { + $value = $context->%s(%s);%s + } catch (Mustache_Exception_UnknownVariableException $ex) { + $value = ""; + } if (empty($value)) { %s } diff --git a/src/Mustache/Context.php b/src/Mustache/Context.php index 69c02e01..a19ec34a 100644 --- a/src/Mustache/Context.php +++ b/src/Mustache/Context.php @@ -14,19 +14,22 @@ */ class Mustache_Context { - private $stack = array(); - private $blockStack = array(); + private $stack = array(); + private $blockStack = array(); + private $strictVariables; /** * Mustache rendering Context constructor. * * @param mixed $context Default rendering context (default: null) + * @param bool $strictVariables */ - public function __construct($context = null) + public function __construct($context = null, $strictVariables = false) { if ($context !== null) { $this->stack = array($context); } + $this->strictVariables = $strictVariables; } /** @@ -200,10 +203,11 @@ public function findInBlock($id) * * @see Mustache_Context::find * - * @param string $id Variable name - * @param array $stack Context stack + * @param string $id Variable name + * @param array $stack Context stack * * @return mixed Variable value, or '' if not found + * @throws \Mustache_Exception_UnknownVariableException */ private function findVariableInStack($id, array $stack) { @@ -237,6 +241,9 @@ private function findVariableInStack($id, array $stack) } } + if ($this->strictVariables) { + throw new Mustache_Exception_UnknownVariableException($id); + } return ''; } } diff --git a/src/Mustache/Engine.php b/src/Mustache/Engine.php index 7a31ac03..05ec1193 100644 --- a/src/Mustache/Engine.php +++ b/src/Mustache/Engine.php @@ -55,6 +55,7 @@ class Mustache_Engine private $charset = 'UTF-8'; private $logger; private $strictCallables = false; + private $strictVariables = false; private $pragmas = array(); private $delimiters; @@ -132,6 +133,9 @@ class Mustache_Engine * // This currently defaults to false, but will default to true in v3.0. * 'strict_callables' => true, * + * // Treat unknown variables as a failure and throw an exception instead of silently ignoring them. + * 'strict_variables' => true, + * * // Enable pragmas across all templates, regardless of the presence of pragma tags in the individual * // templates. * 'pragmas' => [Mustache_Engine::PRAGMA_FILTERS], @@ -206,6 +210,10 @@ public function __construct(array $options = array()) $this->strictCallables = $options['strict_callables']; } + if (isset($options['strict_variables'])) { + $this->strictVariables = $options['strict_variables']; + } + if (isset($options['delimiters'])) { $this->delimiters = $options['delimiters']; } @@ -812,7 +820,7 @@ private function compile($source) $compiler = $this->getCompiler(); $compiler->setPragmas($this->getPragmas()); - return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags); + return $compiler->compile($source, $tree, $name, isset($this->escape), $this->charset, $this->strictCallables, $this->entityFlags, $this->strictVariables); } /** diff --git a/src/Mustache/Exception/UnknownVariableException.php b/src/Mustache/Exception/UnknownVariableException.php new file mode 100644 index 00000000..7851ec62 --- /dev/null +++ b/src/Mustache/Exception/UnknownVariableException.php @@ -0,0 +1,41 @@ +variableName = $variableName; + $message = sprintf('Unknown variable: %s', $variableName); + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + parent::__construct($message, 0, $previous); + } else { + parent::__construct($message); // @codeCoverageIgnore + } + } + + /** + * @return string + */ + public function getVariableName() + { + return $this->variableName; + } +} diff --git a/src/Mustache/Template.php b/src/Mustache/Template.php index 4de82393..569cdca2 100644 --- a/src/Mustache/Template.php +++ b/src/Mustache/Template.php @@ -26,6 +26,11 @@ abstract class Mustache_Template */ protected $strictCallables = false; + /** + * @var bool + */ + protected $strictVariables = false; + /** * Mustache Template constructor. * @@ -143,7 +148,7 @@ protected function isIterable($value) */ protected function prepareContextStack($context = null) { - $stack = new Mustache_Context(); + $stack = new Mustache_Context(null, $this->strictVariables); $helpers = $this->mustache->getHelpers(); if (!$helpers->isEmpty()) { diff --git a/test/Mustache/Test/CompilerTest.php b/test/Mustache/Test/CompilerTest.php index c07a0d45..2ef374d5 100644 --- a/test/Mustache/Test/CompilerTest.php +++ b/test/Mustache/Test/CompilerTest.php @@ -21,7 +21,7 @@ public function testCompile($source, array $tree, $name, $customEscaper, $entity { $compiler = new Mustache_Compiler(); - $compiled = $compiler->compile($source, $tree, $name, $customEscaper, $charset, false, $entityFlags); + $compiled = $compiler->compile($source, $tree, $name, $customEscaper, $charset, false, $entityFlags, false); foreach ($expected as $contains) { $this->assertContains($contains, $compiled); } diff --git a/test/Mustache/Test/ContextTest.php b/test/Mustache/Test/ContextTest.php index 447ea161..f0afea11 100644 --- a/test/Mustache/Test/ContextTest.php +++ b/test/Mustache/Test/ContextTest.php @@ -180,6 +180,29 @@ public function testAnchoredDotNotationThrowsExceptions() $context->push(array('a' => 1)); $context->findAnchoredDot('a'); } + + /** + * @expectedException Mustache_Exception_UnknownVariableException + */ + public function testUnknownVariableThrowsException() + { + $context = new Mustache_Context(null, true); + $context->push(array('a' => 1)); + $context->find('b'); + } + + /** + * @expectedException Mustache_Exception_UnknownVariableException + */ + public function testAnchoredDotNotationUnknownVariableThrowsException() + { + $context = new Mustache_Context(null, true); + $a = array( + 'a' => array('b' => 1), + ); + $context->push($a); + $context->find('a.c'); + } } class Mustache_Test_TestDummy diff --git a/test/Mustache/Test/Exception/UnknownVariableExceptionTest.php b/test/Mustache/Test/Exception/UnknownVariableExceptionTest.php new file mode 100644 index 00000000..246a4ff6 --- /dev/null +++ b/test/Mustache/Test/Exception/UnknownVariableExceptionTest.php @@ -0,0 +1,44 @@ +assertTrue($e instanceof UnexpectedValueException); + $this->assertTrue($e instanceof Mustache_Exception); + } + + public function testMessage() + { + $e = new Mustache_Exception_UnknownVariableException('beta'); + $this->assertEquals('Unknown variable: beta', $e->getMessage()); + } + + public function testGetHelperName() + { + $e = new Mustache_Exception_UnknownVariableException('gamma'); + $this->assertEquals('gamma', $e->getVariableName()); + } + + public function testPrevious() + { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + $this->markTestSkipped('Exception chaining requires at least PHP 5.3'); + } + + $previous = new Exception(); + $e = new Mustache_Exception_UnknownVariableException('foo', $previous); + $this->assertSame($previous, $e->getPrevious()); + } +} diff --git a/test/Mustache/Test/Functional/StrictVariablesTest.php b/test/Mustache/Test/Functional/StrictVariablesTest.php new file mode 100644 index 00000000..a1d463ab --- /dev/null +++ b/test/Mustache/Test/Functional/StrictVariablesTest.php @@ -0,0 +1,84 @@ +mustache = new Mustache_Engine(array('strict_variables' => true)); + } + + /** + * @@dataProvider strictVariablesProvider + */ + public function testStrictVariables($template, $expected) + { + $context = array( + 'a' => array('b' => 'ab'), + 'c' => 'c', + 'd' => 'd', + ); + + $tpl = $this->mustache->loadTemplate($template); + $this->assertEquals($expected, $tpl->render($context)); + } + + public function strictVariablesProvider() + { + return array( + array('{{ c }}', 'c'), + array('{{# a }}{{ b }}{{/ a }}', 'ab'), + array('{{ a.b }}', 'ab'), + array('{{# c }}{{ d }}{{/ c }}', 'd'), + array('{{# x }}{{/ x }}', ''), + array('{{^ x }}{{/ x }}', ''), + array('{{# a }}{{# x }}{{/ x }}{{/ a }}', ''), + array('{{# a.x }}{{/ a.x }}', ''), + array('{{# d.x }}{{/ d.x }}', ''), + array('{{^ a }}{{ x }}{{/ a }}', ''), + array('{{# f }}{{ x }}{{/ f }}', ''), + ); + } + + + + /** + * @dataProvider unknownVariableThrowsExceptionProvider + * @expectedException Mustache_Exception_UnknownVariableException + */ + public function testUnknownVariableThrowsException($template) + { + $context = array( + 'a' => array('b' => 1), + 'c' => 1, + 'd' => 0, + ); + + $tpl = $this->mustache->loadTemplate($template); + $tpl->render($context); + } + + public function unknownVariableThrowsExceptionProvider() + { + return array( + array('{{ e }}'), + array('{{ .a }}'), + array('{{ .a.b }}'), + array('{{ a.c }}') + ); + } +}