deeply mark expressions as safe (e.g. a conditonal expression is safe if both operand...
authorArnaud Le Blanc <arnaud.lb@gmail.com>
Sat, 6 Nov 2010 02:49:37 +0000 (03:49 +0100)
committerFabien Potencier <fabien.potencier@gmail.com>
Sun, 7 Nov 2010 07:52:12 +0000 (08:52 +0100)
lib/Twig/Extension/Core.php
lib/Twig/Extension/Escaper.php
lib/Twig/Filter.php
lib/Twig/NodeVisitor/Escaper.php
lib/Twig/NodeVisitor/SafeAnalysis.php [new file with mode: 0644]
test/Twig/Tests/Fixtures/tags/autoescape/literal.test

index 8acf214..7805dec 100644 (file)
@@ -251,18 +251,17 @@ function twig_escape_filter(Twig_Environment $env, $string, $type = 'html')
     }
 }
 
-function twig_escape_filter_is_safe($for, Twig_Node $filterArgs)
+function twig_escape_filter_is_safe(Twig_Node $filterArgs)
 {
-    $type = 'html';
     foreach($filterArgs as $arg) {
         if ($arg instanceof Twig_Node_Expression_Constant) {
-            $type = $arg->getAttribute('value');
+            return array($arg->getAttribute('value'));
         } else {
-            $type = null;
+            return array();
         }
         break;
     }
-    return $for == $type;
+    return array('html');
 }
 
 if (function_exists('iconv')) {
index 7fcb17a..62c5a1a 100644 (file)
@@ -45,7 +45,7 @@ class Twig_Extension_Escaper extends Twig_Extension
     public function getFilters()
     {
         return array(
-            'raw' => new Twig_Filter_Function('twig_raw_filter', array('is_safe_callback' => 'twig_raw_filter_is_safe')),
+            'raw' => new Twig_Filter_Function('twig_raw_filter', array('is_safe' => array('all'))),
         );
     }
 
@@ -71,9 +71,3 @@ function twig_raw_filter($string)
     return $string;
 }
 
-// |raw is safe for everything
-function twig_raw_filter_is_safe($for, Twig_Node $filterArgs)
-{
-    return true;
-}
-
index 529e500..d9991d5 100644 (file)
@@ -37,14 +37,14 @@ abstract class Twig_Filter implements Twig_FilterInterface
         return $this->options['needs_environment'];
     }
 
-    public function isSafe($for, Twig_Node $filterArgs)
+    public function getSafe(Twig_Node $filterArgs)
     {
         if (isset($this->options['is_safe'])) {
-            return in_array($for, $this->options['is_safe']);
+            return $this->options['is_safe'];
         }
         if (isset($this->options['is_safe_callback'])) {
-            return call_user_func($this->options['is_safe_callback'], $for, $filterArgs);
+            return call_user_func($this->options['is_safe_callback'], $filterArgs);
         }
-        return false;
+        return array();
     }
 }
index e06aaba..6f0feb7 100644 (file)
@@ -21,6 +21,14 @@ class Twig_NodeVisitor_Escaper implements Twig_NodeVisitorInterface
     protected $statusStack = array();
     protected $blocks = array();
 
+    protected $safeAnalysis;
+    protected $traverser;
+
+    function __construct()
+    {
+        $this->safeAnalysis = new Twig_NodeVisitor_SafeAnalysis;
+    }
+
     /**
      * Called before child nodes are visited.
      *
@@ -69,21 +77,18 @@ class Twig_NodeVisitor_Escaper implements Twig_NodeVisitorInterface
 
         $expression = $node instanceof Twig_Node_Print ? $node->getNode('expr') : $node;
 
-        if ($expression instanceof Twig_Node_Expression_Constant) {
+        $safe = $this->safeAnalysis->getSafe($expression);
 
-            return $node;
+        if ($safe === null) {
+            if ($this->traverser === null) {
+                $this->traverser = new Twig_NodeTraverser($env, array($this->safeAnalysis));
+            }
+            $this->traverser->traverse($expression);
+            $safe = $this->safeAnalysis->getSafe($expression);
         }
 
-        if ($expression instanceof Twig_Node_Expression_Filter) {
-
-            // don't escape if the last filter in the chain is safe for $type
-            $filterMap = $env->getFilters();
-            $i = count($expression->getNode('filters')) - 2;
-            $name = $expression->getNode('filters')->getNode($i)->getAttribute('value');
-            $args = $expression->getNode('filters')->getNode($i+1);
-            if (isset($filterMap[$name]) && $filterMap[$name]->isSafe($type, $args)) {
-                return $node;
-            }
+        if (in_array($type, $safe) !== false || in_array('all', $safe) !== false) {
+            return $node;
         }
 
         // escape
diff --git a/lib/Twig/NodeVisitor/SafeAnalysis.php b/lib/Twig/NodeVisitor/SafeAnalysis.php
new file mode 100644 (file)
index 0000000..d587d1f
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+
+class Twig_NodeVisitor_SafeAnalysis implements Twig_NodeVisitorInterface
+{
+    protected $data = array();
+
+    public function getSafe(Twig_NodeInterface $node)
+    {
+        $hash = spl_object_hash($node);
+        return isset($this->data[$hash]) ? $this->data[$hash] : null;
+    }
+
+    protected function setSafe(Twig_NodeInterface $node, array $safe)
+    {
+        $hash = spl_object_hash($node);
+        $this->data[$hash] = $safe;
+    }
+
+    protected function intersectSafe(array $a = null, array $b = null)
+    {
+        if ($a === null || $b === null) {
+            return array();
+        }
+        if (in_array('all', $a)) {
+            return $b;
+        }
+        if (in_array('all', $b)) {
+            return $a;
+        }
+        return array_intersect($a, $b);
+    }
+
+    public function enterNode(Twig_NodeInterface $node, Twig_Environment $env)
+    {
+        return $node;
+    }
+    
+    public function leaveNode(Twig_NodeInterface $node, Twig_Environment $env)
+    {
+
+        // constants are marked safe for all
+
+        if ($node instanceof Twig_Node_Expression_Constant) {
+
+            $this->setSafe($node, array('all'));
+
+        // instersect safeness of both operands
+
+        } else if ($node instanceof Twig_Node_Expression_Conditional) {
+
+            $safe = $this->intersectSafe($this->getSafe($node->getNode('expr2')), $this->getSafe($node->getNode('expr3')));
+            $this->setSafe($node, $safe);
+
+        // filter expression is safe when the last filter is safe
+
+        } else if ($node instanceof Twig_Node_Expression_Filter) {
+
+            $filterMap = $env->getFilters();
+            $filters = $node->getNode('filters');
+            $i = count($filters) - 2;
+            $name = $filters->getNode($i)->getAttribute('value');
+            $args = $filters->getNode($i+1);
+            if (isset($filterMap[$name])) {
+                $this->setSafe($node, $filterMap[$name]->getSafe($args));
+            } else {
+                $this->setSafe($node, array());
+            }
+
+        } else {
+
+            $this->setSafe($node, array());
+        }
+
+        return $node;
+    }
+}
index 4c25706..eb0a987 100644 (file)
@@ -2,9 +2,44 @@
 "autoescape" tag does not apply escaping on literals
 --TEMPLATE--
 {% autoescape on %}
+
+1. Simple literal
 {{ "<br />" }}
+
+2. Conditional expression with only literals
+{{ true ? "<br />" : "<br>" }}
+
+3. Conditonal expression with a variable
+{{ true ? "<br />" : someVar }}
+
+4. Nested conditionals with only literals
+{{ true ? (true ? "<br />" : "<br>") : "\n" }}
+
+5. Nested conditionals with a variable
+{{ true ? (true ? "<br />" : someVar) : "\n" }}
+
+6. Nested conditionals with a variable marked safe
+{{ true ? (true ? "<br />" : someVar|raw) : "\n" }}
+
 {% endautoescape %}
 --DATA--
 return array()
 --EXPECT--
+
+1. Simple literal
+<br />
+
+2. Conditional expression with only literals
+<br />
+
+3. Conditonal expression with a variable
+&lt;br /&gt;
+
+4. Nested conditionals with only literals
+<br />
+
+5. Nested conditionals with a variable
+&lt;br /&gt;
+
+6. Nested conditionals with a variable marked safe
 <br />