From 42b71ed644a15a59ffcc97dab65bf7fdcbf214b9 Mon Sep 17 00:00:00 2001 From: bzick Date: Sun, 7 Jul 2013 01:29:33 +0400 Subject: [PATCH] Add {autoescape} block tag and {raw} inline pseudo tag. Improve auto escaping --- src/Fenom.php | 15 +++++++++++-- src/Fenom/Compiler.php | 50 +++++++++++++++++++++++++++++++++++++++--- src/Fenom/Scope.php | 7 ++++++ src/Fenom/Template.php | 46 +++++++++++++++++++++++++++----------- 4 files changed, 100 insertions(+), 18 deletions(-) diff --git a/src/Fenom.php b/src/Fenom.php index 22e61ab..55f09ed 100644 --- a/src/Fenom.php +++ b/src/Fenom.php @@ -33,6 +33,7 @@ class Fenom { const FORCE_COMPILE = 0xF0; const DISABLE_CACHE = 0x1F0; const AUTO_ESCAPE = 0x200; + const FORCE_VALIDATE = 0x400; /* Default parsers */ const DEFAULT_CLOSE_COMPILER = 'Fenom\Compiler::stdClose'; @@ -43,7 +44,7 @@ class Fenom { /** * @var int[] of possible options, as associative array - * @see setOptions, addOptions, delOptions + * @see setOptions */ private static $_option_list = array( "disable_methods" => self::DENY_METHODS, @@ -52,6 +53,8 @@ class Fenom { "force_compile" => self::FORCE_COMPILE, "auto_reload" => self::AUTO_RELOAD, "force_include" => self::FORCE_INCLUDE, + "auto_escape" => self::AUTO_ESCAPE, + "force_validate" => self::FORCE_VALIDATE ); /** @@ -213,6 +216,15 @@ class Fenom { 'cycle' => array( 'type' => self::INLINE_COMPILER, 'parser' => 'Fenom\Compiler::tagCycle' + ), + 'raw' => array( + 'type' => self::INLINE_COMPILER, + 'parser' => 'Fenom\Compiler::tagRaw' + ), + 'autoescape' => array( + 'type' => self::BLOCK_COMPILER, + 'open' => 'Fenom\Compiler::autoescapeOpen', + 'close' => 'Fenom\Compiler::autoescapeClose' ) ); @@ -234,7 +246,6 @@ class Fenom { throw new InvalidArgumentException("Source must be a valid path or provider object"); } $fenom = new static($provider); - /* @var Fenom $fytro */ $fenom->setCompileDir($compile_dir); if($options) { $fenom->setOptions($options); diff --git a/src/Fenom/Compiler.php b/src/Fenom/Compiler.php index 7918761..27431a9 100644 --- a/src/Fenom/Compiler.php +++ b/src/Fenom/Compiler.php @@ -588,7 +588,7 @@ class Compiler { * @return string */ public static function stdFuncParser($function, Tokenizer $tokens, Template $tpl) { - return "echo $function(".self::toArray($tpl->parseParams($tokens)).', $tpl);'; + return "$function(".self::toArray($tpl->parseParams($tokens)).', $tpl)'; } /** @@ -618,7 +618,7 @@ class Compiler { $args[] = $param->getDefaultValue(); } } - return "echo $function(".implode(", ", $args).');'; + return "$function(".implode(", ", $args).')'; } /** @@ -643,7 +643,7 @@ class Compiler { * @return string */ public static function stdFuncClose($tokens, Scope $scope) { - return "echo ".$scope["function"].'('.$scope["params"].', ob_get_clean(), $tpl);'; + return $scope["function"].'('.$scope["params"].', ob_get_clean(), $tpl)'; } /** @@ -866,4 +866,48 @@ class Compiler { $scope->tpl->_body = substr($scope->tpl->_body, 0, strlen($scope->tpl->_body) - strlen($content)); } + /** + * Output value as is, without escaping + * + * @param Tokenizer $tokens + * @param Template $tpl + * @throws InvalidUsageException + * @return string + */ + public static function tagRaw(Tokenizer $tokens, Template $tpl) { + $tpl->escape = false; + if($tokens->is(':')) { + $func = $tokens->getNext(Tokenizer::MACRO_STRING); + $tag = $tpl->getStorage()->getFunction($func); + if($tag["type"] == \Fenom::INLINE_FUNCTION) { + return $tpl->parseAct($tokens); + } elseif ($tag["type"] == \Fenom::BLOCK_FUNCTION) { + $code = $tpl->parseAct($tokens); + $tpl->getLastScope()->escape = false; + return $code; + } + throw new InvalidUsageException("Raw mode allow for expressions or functions"); + } else { + return $tpl->out($tpl->parseExp($tokens, true)); + } + } + + /** + * @param Tokenizer $tokens + * @param Scope $scope + */ + public static function autoescapeOpen(Tokenizer $tokens, Scope $scope) { + $boolean = ($tokens->get(T_STRING) == "true" ? true : false); + $scope["escape"] = $scope->tpl->escape; + $scope->tpl->escape = $boolean; + $tokens->next(); + } + + /** + * @param Tokenizer $tokens + * @param Scope $scope + */ + public static function autoescapeClose(Tokenizer $tokens, Scope $scope) { + $scope->tpl->escape = $scope["escape"]; + } } diff --git a/src/Fenom/Scope.php b/src/Fenom/Scope.php index 8480f81..592cdf8 100644 --- a/src/Fenom/Scope.php +++ b/src/Fenom/Scope.php @@ -25,9 +25,11 @@ class Scope extends \ArrayObject { public $tpl; public $is_compiler = true; public $is_closed = false; + public $escape = false; private $_action; private $_body; private $_offset; + private $_global_escape = false; /** * Creating cope @@ -56,6 +58,8 @@ class Scope extends \ArrayObject { public function setFuncName($function) { $this["function"] = $function; $this->is_compiler = false; + $this->_global_escape = $this->tpl->escape; + $this->tpl->escape = false; } /** @@ -104,6 +108,9 @@ class Scope extends \ArrayObject { * @return string */ public function close($tokenizer) { + if(!$this->is_compiler) { + $this->tpl->escape = $this->_global_escape; + } return call_user_func($this->_action["close"], $tokenizer, $this); } diff --git a/src/Fenom/Template.php b/src/Fenom/Template.php index 24126f3..1f7faa0 100644 --- a/src/Fenom/Template.php +++ b/src/Fenom/Template.php @@ -58,6 +58,11 @@ class Template extends Render { public $parents = array(); + /** + * Escape output value + * @var bool + */ + public $escape = false; public $_extends; public $_extended = false; public $_compatible; @@ -159,6 +164,7 @@ class Template extends Render { */ public function compile() { $end = $pos = 0; + $this->escape = $this->_options & Fenom::AUTO_ESCAPE; while(($start = strpos($this->_src, '{', $pos)) !== false) { // search open-symbol of tags switch($this->_src[$start + 1]) { // check next character case "\n": case "\r": case "\t": case " ": case "}": // ignore the tag @@ -397,11 +403,11 @@ class Template extends Render { * @param $data * @return string */ - private function _print($data) { - if($this->_options & Fenom::AUTO_ESCAPE) { - return "echo htmlspecialchars($data, ENT_COMPAT, 'UTF-8')"; + public function out($data) { + if($this->escape) { + return "echo htmlspecialchars($data, ENT_COMPAT, 'UTF-8');"; } else { - return "echo $data"; + return "echo $data;"; } } /** @@ -420,14 +426,14 @@ class Template extends Render { $tokens->next(); return ''; } else { - return $this->_parseAct($tokens); + return $this->parseAct($tokens); } } elseif ($tokens->is('/')) { return $this->_end($tokens); } elseif ($tokens->is('#')) { - return $this->_print($this->parseConst($tokens), $tokens).';'; + return $this->out($this->parseConst($tokens), $tokens).';'; } else { - return $code = $this->_print($this->parseExp($tokens), $tokens).";"; + return $code = $this->out($this->parseExp($tokens), $tokens).";"; } } catch (InvalidUsageException $e) { throw new CompileException($e->getMessage()." in {$this} line {$this->_line}", 0, E_ERROR, $this->_name, $this->_line, $e); @@ -442,7 +448,7 @@ class Template extends Render { * Close tag handler * * @param Tokenizer $tokens - * @return mixed + * @return string * @throws TokenizeException */ private function _end(Tokenizer $tokens) { @@ -457,7 +463,20 @@ class Template extends Render { if($scope->name !== $name) { throw new TokenizeException("Unexpected closing of the tag '$name' (expecting closing of the tag {$scope->name}, opened on line {$scope->line})"); } - return $scope->close($tokens); + if($scope->is_compiler) { + return $scope->close($tokens); + } else { + $scope->tpl->escape = $scope->escape; + return $this->out($scope->close($tokens)); + } + } + + /** + * Get current scope + * @return Scope + */ + public function getLastScope() { + return end($this->_stack); } /** @@ -469,16 +488,16 @@ class Template extends Render { * @throws TokenizeException * @return string */ - private function _parseAct(Tokenizer $tokens) { + public function parseAct(Tokenizer $tokens) { if($tokens->is(Tokenizer::MACRO_STRING)) { $action = $tokens->getAndNext(); } else { - return $this->_print($this->parseExp($tokens), $tokens).';'; // may be math and/or boolean expression + return $this->out($this->parseExp($tokens), $tokens).';'; // may be math and/or boolean expression } if($tokens->is("(", T_NAMESPACE, T_DOUBLE_COLON)) { // just invoke function or static method $tokens->back(); - return $this->_print($this->parseExp($tokens), $tokens).";"; + return $this->out($this->parseExp($tokens), $tokens).";"; } elseif($tokens->is('.')) { $name = $tokens->skip()->get(Tokenizer::MACRO_STRING); if($action !== "macro") { @@ -499,11 +518,12 @@ class Template extends Render { case Fenom::INLINE_COMPILER: return call_user_func($act["parser"], $tokens, $this); case Fenom::INLINE_FUNCTION: - return call_user_func($act["parser"], $act["function"], $tokens, $this); + return $this->out(call_user_func($act["parser"], $act["function"], $tokens, $this)); case Fenom::BLOCK_FUNCTION: $scope = new Scope($action, $this, $this->_line, $act, count($this->_stack), $this->_body); $scope->setFuncName($act["function"]); array_push($this->_stack, $scope); + $scope->escape = $this->_options & Fenom::AUTO_ESCAPE; return $scope->open($tokens); default: throw new \LogicException("Unknown function type");