diff --git a/vendor/exTpl/Context.php b/vendor/exTpl/Context.php index 222c17f96b1093b982d8887352a64e7fa2d6285d..ea94b2fd25f8b917bf1330814147829ed97d8e75 100644 --- a/vendor/exTpl/Context.php +++ b/vendor/exTpl/Context.php @@ -20,6 +20,7 @@ namespace exTpl; class Context { private $bindings; + private $escape; private $parent; /** @@ -50,4 +51,30 @@ class Context return NULL; } + + /** + * Enables or disables automatic escaping for template values. + * + * @param callable $escape escape callback or NULL + */ + public function autoescape($escape) + { + $this->escape = $escape; + } + + /** + * Escapes the given value using the configured strategy. + * + * @param mixed $value expression value + */ + public function escape($value) + { + if (isset($this->escape)) { + $value = call_user_func($this->escape, $value); + } else if ($this->parent) { + $value = $this->parent->escape($value); + } + + return $value; + } } diff --git a/vendor/exTpl/Expression.php b/vendor/exTpl/Expression.php index 0885f4eca110aadd66111b7ebfbecd33d5a78e3e..ced08b59a6875c562905f4b64216d427496b1e20 100644 --- a/vendor/exTpl/Expression.php +++ b/vendor/exTpl/Expression.php @@ -71,6 +71,14 @@ class SymbolExpression implements Expression $this->name = $name; } + /** + * Returns the name of this symbol. + */ + public function name() + { + return $this->name; + } + /** * Returns the value of this expression. * @@ -132,6 +140,22 @@ class NotExpression extends UnaryExpression } } +/** + * RawExpression represents the "raw" filter function. + */ +class RawExpression extends UnaryExpression +{ + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value($context) + { + return $this->expr->value($context); + } +} + /** * BinaryExpression represents a binary operator. */ @@ -261,3 +285,45 @@ class ConditionExpression implements Expression $this->left->value($context) : $this->right->value($context); } } + +/** + * FunctionExpression represents a function call. + */ +class FunctionExpression implements Expression +{ + protected $name; + protected $arguments; + + /** + * Initializes a new Expression instance. + * + * @param Expression $name function name + * @param array $arguments function arguments + */ + public function __construct(Expression $name, $arguments) + { + $this->name = $name; + $this->arguments = $arguments; + } + + /** + * Returns the value of this expression. + * + * @param Context $context symbol table + */ + public function value($context) + { + $callable = $this->name->value($context); + $arguments = array(); + + foreach ($this->arguments as $expr) { + $arguments[] = $expr->value($context); + } + + if (is_callable($callable)) { + return call_user_func_array($callable, $arguments); + } + + return NULL; + } +} diff --git a/vendor/exTpl/Node.php b/vendor/exTpl/Node.php index 5fdada2619bec819427bb1e98ffe91c2f1a3a222..0dbd9463d0ca8ff308b8077efed076c81018ecc1 100644 --- a/vendor/exTpl/Node.php +++ b/vendor/exTpl/Node.php @@ -81,7 +81,13 @@ class ExpressionNode implements Node */ public function render($context) { - return $this->expr->value($context); + $value = $this->expr->value($context); + + if (!($this->expr instanceof RawExpression)) { + $value = $context->escape($value); + } + + return $value; } } @@ -121,20 +127,26 @@ class ArrayNode implements Node /** * IteratorNode represents a single iterator tag: - * "{foreach ARRAY}...{endforeach}". + * "{foreach ARRAY [as [KEY =>] VALUE]}...{endforeach}". */ class IteratorNode extends ArrayNode { protected $expr; + protected $key_name; + protected $val_name; /** * Initializes a new Node instance with the given expression. * * @param Expression $expr expression object + * @param string $key_name name of variable on each iteration + * @param string $val_name name of variable on each iteration */ - public function __construct(Expression $expr) + public function __construct(Expression $expr, $key_name, $val_name) { $this->expr = $expr; + $this->key_name = $key_name; + $this->val_name = $val_name; } /** @@ -149,7 +161,7 @@ class IteratorNode extends ArrayNode $result = ''; if (is_array($values) && is_int(key($values))) { - $bindings = array('index' => &$key, 'this' => &$value); + $bindings = array($this->key_name => &$key, $this->val_name => &$value); $context = new Context($bindings, $context); foreach ($values as $key => $value) { diff --git a/vendor/exTpl/Scanner.php b/vendor/exTpl/Scanner.php index d6ec2e696f4a2abb0aa8f9b08842a676b0e2ff82..b2464fbb4410a31dc82c68e70fa2d7e34fe7f184 100644 --- a/vendor/exTpl/Scanner.php +++ b/vendor/exTpl/Scanner.php @@ -43,7 +43,9 @@ class Scanner $token = next($this->tokens); $key = key($this->tokens); - while ($token[0] === T_STRING && + // FIXME this workaround should be dropped + while ($token && $token[0] === T_STRING && + isset($this->tokens[$key + 2]) && $this->tokens[++$key] === '-' && $this->tokens[++$key][0] === T_STRING) { $token[1] .= '-' . $this->tokens[$key][1]; diff --git a/vendor/exTpl/Template.php b/vendor/exTpl/Template.php index 28db8fe6fe730ab897fd1feed68fc46ef418f224..0be46bf85fff6eda33c39e91d687ba16fec49ef2 100644 --- a/vendor/exTpl/Template.php +++ b/vendor/exTpl/Template.php @@ -24,6 +24,8 @@ class Template { private static $tag_start = '{'; private static $tag_end = '}'; + private $escape; + private $functions; private $template; /** @@ -47,9 +49,31 @@ class Template public function __construct($string) { $this->template = new ArrayNode(); + $this->functions = array('count' => 'count', 'strlen' => 'mb_strlen'); self::parseTemplate($this->template, $string, 0); } + /** + * Enables or disables automatic escaping for template values. + * Currently supported strategies: NULL, 'html', 'json', 'xml' + * + * @param string|callable $escape escape strategy or callback + */ + public function autoescape($escape) + { + if ($escape === 'html' || $escape === 'xml') { + $this->escape = 'htmlspecialchars'; + } else if ($escape === 'json') { + $this->escape = 'json_encode'; + } else if (is_callable($escape)) { + $this->escape = $escape; + } else if ($escape === NULL) { + $this->escape = NULL; + } else { + throw new InvalidArgumentException("invalid escape strategy: $escape"); + } + } + /** * Renders the template to a string using the given array of * bindings to resolve symbol references inside the template. @@ -60,7 +84,10 @@ class Template */ public function render(array $bindings) { - return $this->template->render(new Context($bindings)); + $context = new Context($bindings + $this->functions); + $context->autoescape($this->escape); + + return $this->template->render($context); } /** @@ -125,7 +152,34 @@ class Template switch ($scanner->nextToken()) { case T_FOREACH: $scanner->nextToken(); - $child = new IteratorNode(self::parseExpr($scanner)); + $expr = self::parseExpr($scanner); + $key_name = 'index'; + $val_name = 'this'; + + if ($scanner->tokenType() === T_AS) { + $scanner->nextToken(); + + if ($scanner->tokenType() !== T_STRING) { + throw new TemplateParserException('symbol expected', $scanner); + } + + $val_name = $scanner->tokenValue(); + $scanner->nextToken(); + + if ($scanner->tokenType() === T_DOUBLE_ARROW) { + $scanner->nextToken(); + + if ($scanner->tokenType() !== T_STRING) { + throw new TemplateParserException('symbol expected', $scanner); + } + + $key_name = $val_name; + $val_name = $scanner->tokenValue(); + $scanner->nextToken(); + } + } + + $child = new IteratorNode($expr, $key_name, $val_name); $pos = self::parseTemplate($child, $string, $pos); $node->addChild($child); break; @@ -193,13 +247,46 @@ class Template } /** - * index: value | index '[' expr ']' | index '.' SYMBOL + * function: value | function '(' ')' | function '(' expr { ',' expr } ')' */ - private static function parseIndex(Scanner $scanner) + private static function parseFunction(Scanner $scanner) { $result = self::parseValue($scanner); $type = $scanner->tokenType(); + while ($type === '(') { + $scanner->nextToken(); + $arguments = array(); + + if ($scanner->tokenType() !== ')') { + $arguments[] = self::parseExpr($scanner); + + while ($scanner->tokenType() === ',') { + $scanner->nextToken(); + $arguments[] = self::parseExpr($scanner); + } + + if ($scanner->tokenType() !== ')') { + throw new TemplateParserException('missing ")"', $scanner); + } + } + + $scanner->nextToken(); + $result = new FunctionExpression($result, $arguments); + $type = $scanner->tokenType(); + } + + return $result; + } + + /** + * index: function | index '[' expr ']' | index '.' SYMBOL + */ + private static function parseIndex(Scanner $scanner) + { + $result = self::parseFunction($scanner); + $type = $scanner->tokenType(); + while ($type === '[' || $type === '.') { $scanner->nextToken(); @@ -224,7 +311,57 @@ class Template } /** - * sign: '!' sign | '+' sign | '-' sign | index + * filter: index | filter '|' SYMBOL | filter '|' SYMBOL '(' expr { ',' expr } ')' + */ + private static function parseFilter(Scanner $scanner) + { + $result = self::parseIndex($scanner); + $type = $scanner->tokenType(); + + while ($type === '|') { + $scanner->nextToken(); + + if ($scanner->tokenType() !== T_STRING) { + throw new TemplateParserException('symbol expected', $scanner); + } + + $arguments = array($result); + $symbol = new SymbolExpression($scanner->tokenValue()); + $scanner->nextToken(); + + if ($scanner->tokenType() === '(') { + $scanner->nextToken(); + + if ($scanner->tokenType() !== ')') { + $arguments[] = self::parseExpr($scanner); + + while ($scanner->tokenType() === ',') { + $scanner->nextToken(); + $arguments[] = self::parseExpr($scanner); + } + + if ($scanner->tokenType() !== ')') { + throw new TemplateParserException('missing ")"', $scanner); + } + } + + $scanner->nextToken(); + } + + if ($symbol->name() === 'raw') { + $result = new RawExpression($result); + } else { + $result = new FunctionExpression($symbol, $arguments); + } + + $type = $scanner->tokenType(); + } + + return $result; + } + + /** + * sign: '!' sign | '+' sign | '-' sign | filter */ private static function parseSign(Scanner $scanner) { @@ -242,7 +379,7 @@ class Template $result = new MinusExpression(self::parseSign($scanner)); break; default: - $result = self::parseIndex($scanner); + $result = self::parseFilter($scanner); } return $result; @@ -353,7 +490,7 @@ class Template } /** - * expr: or | or '?' expr ':' expr + * expr: or | or '?' expr ':' expr | or '?' ':' expr */ private static function parseExpr(Scanner $scanner) { @@ -361,7 +498,12 @@ class Template if ($scanner->tokenType() === '?') { $scanner->nextToken(); - $expr = self::parseExpr($scanner); + + if ($scanner->tokenType() !== ':') { + $expr = self::parseExpr($scanner); + } else { + $expr = $result; + } if ($scanner->tokenType() !== ':') { throw new TemplateParserException('missing ":"', $scanner); diff --git a/vendor/exTpl/template_test.php b/vendor/exTpl/template_test.php index 5adbb136a34233c752e39a8e9d466a4f53f66b01..b794a820a1b032acb8b5a38ba2c43ef5754b60d9 100644 --- a/vendor/exTpl/template_test.php +++ b/vendor/exTpl/template_test.php @@ -14,7 +14,7 @@ require 'Template.php'; use exTpl\Template; -class TemplateTest extends PHPUnit_Framework_TestCase +class template_test extends PHPUnit\Framework\TestCase { public function testSimpleString() { @@ -36,6 +36,16 @@ class TemplateTest extends PHPUnit_Framework_TestCase $this->assertEquals($expected, $tmpl_obj->render($bindings)); } + public function testConditionExpression() + { + $bindings = array('a' => 0, 'b' => 42); + $template = 'answer is {"" ?: a ?: b}'; + $expected = 'answer is 42'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + public function testStringEscapes() { $bindings = array(); @@ -93,7 +103,7 @@ class TemplateTest extends PHPUnit_Framework_TestCase 2 => array('user' => 'mike', 'phone' => '230-28382'), 3 => array('user' => 'john', 'phone' => '911-19212') )); - $template = '<ul>{foreach persons}<li>{index~":"~this.user~":"~phone}</li>{endforeach}</ul>'; + $template = '<ul>{foreach persons as person}<li>{index~":"~person.user~":"~phone}</li>{endforeach}</ul>'; $expected = '<ul>' . '<li>1:jane:555-81281</li>' . '<li>2:mike:230-28382</li>' . @@ -145,4 +155,57 @@ class TemplateTest extends PHPUnit_Framework_TestCase $this->assertEquals($expected, $tmpl_obj->render($bindings)); } + + public function testFunctionCall() + { + $bindings = array('val' => array(0, 1, 2, 3)); + $template = '{strlen("foobar") + count(val)}'; + $expected = '10'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testFilters() + { + $bindings = array('pi' => 3.14159, 'format_number' => 'number_format', 'upper' => 'strtoupper'); + $template = '{pi|format_number(3) ~ ":" ~ "foobar"|upper}'; + $expected = '3.142:FOOBAR'; + $tmpl_obj = new Template($template); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testRawFilter() + { + $bindings = array('foo' => '<img>', 'upper' => 'strtoupper'); + $template = '{foo}:{foo|upper|raw}'; + $expected = '<img>:<IMG>'; + $tmpl_obj = new Template($template); + $tmpl_obj->autoescape('html'); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testHtmlAutoEscape() + { + $bindings = array('foo' => '<img>', 'pi' => 3.14159); + $template = '{foo}:{pi}'; + $expected = '<img>:3.14159'; + $tmpl_obj = new Template($template); + $tmpl_obj->autoescape('html'); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } + + public function testJsonAutoEscape() + { + $bindings = array('foo' => '<img>', 'pi' => 3.14159); + $template = '{foo}:{pi}'; + $expected = '"<img>":3.14159'; + $tmpl_obj = new Template($template); + $tmpl_obj->autoescape('json'); + + $this->assertEquals($expected, $tmpl_obj->render($bindings)); + } }