support for specifying the escaping type of a filter
authorArnaud Le Blanc <arnaud.lb@gmail.com>
Sat, 6 Nov 2010 02:36:14 +0000 (03:36 +0100)
committerFabien Potencier <fabien.potencier@gmail.com>
Sun, 7 Nov 2010 07:52:06 +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/TokenParser/AutoEscape.php
test/Twig/Tests/Fixtures/tags/autoescape/type.test [new file with mode: 0644]
test/Twig/Tests/integrationTest.php

index ee3afc1..8acf214 100644 (file)
@@ -46,7 +46,7 @@ class Twig_Extension_Core extends Twig_Extension
             'replace' => new Twig_Filter_Function('twig_strtr'),
 
             // encoding
-            'url_encode'  => new Twig_Filter_Function('twig_urlencode_filter', array('is_escaper' => true)),
+            'url_encode'  => new Twig_Filter_Function('twig_urlencode_filter', array('is_safe' => array('html'))),
             'json_encode' => new Twig_Filter_Function('json_encode'),
 
             // string filters
@@ -72,8 +72,8 @@ class Twig_Extension_Core extends Twig_Extension
             'items'   => new Twig_Filter_Function('twig_get_array_items_filter'),
 
             // escaping
-            'escape' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_escaper' => true)),
-            'e'      => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_escaper' => true)),
+            'escape' => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
+            'e'      => new Twig_Filter_Function('twig_escape_filter', array('needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe')),
         );
 
         if (function_exists('mb_get_info')) {
@@ -245,9 +245,24 @@ function twig_escape_filter(Twig_Environment $env, $string, $type = 'html')
             return $string;
 
         case 'html':
-        default:
             return htmlspecialchars($string, ENT_QUOTES, $env->getCharset());
+        default:
+            throw new Exception("Invalid escape type $type");
+    }
+}
+
+function twig_escape_filter_is_safe($for, Twig_Node $filterArgs)
+{
+    $type = 'html';
+    foreach($filterArgs as $arg) {
+        if ($arg instanceof Twig_Node_Expression_Constant) {
+            $type = $arg->getAttribute('value');
+        } else {
+            $type = null;
+        }
+        break;
     }
+    return $for == $type;
 }
 
 if (function_exists('iconv')) {
index 5476800..7fcb17a 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_escaper' => true)),
+            'raw' => new Twig_Filter_Function('twig_raw_filter', array('is_safe_callback' => 'twig_raw_filter_is_safe')),
         );
     }
 
@@ -70,3 +70,10 @@ 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 05557dd..529e500 100644 (file)
@@ -24,8 +24,12 @@ abstract class Twig_Filter implements Twig_FilterInterface
     {
         $this->options = array_merge(array(
             'needs_environment' => false,
-            'is_escaper'        => false,
         ), $options);
+
+        if (isset($this->options['is_escaper'])) {
+            $this->options['is_safe'] = array('html');
+            unset($this->options['is_escaper']);
+        }
     }
 
     public function needsEnvironment()
@@ -33,8 +37,14 @@ abstract class Twig_Filter implements Twig_FilterInterface
         return $this->options['needs_environment'];
     }
 
-    public function isEscaper()
+    public function isSafe($for, Twig_Node $filterArgs)
     {
-        return $this->options['is_escaper'];
+        if (isset($this->options['is_safe'])) {
+            return in_array($for, $this->options['is_safe']);
+        }
+        if (isset($this->options['is_safe_callback'])) {
+            return call_user_func($this->options['is_safe_callback'], $for, $filterArgs);
+        }
+        return false;
     }
 }
index 7190955..e06aaba 100644 (file)
@@ -76,11 +76,12 @@ class Twig_NodeVisitor_Escaper implements Twig_NodeVisitorInterface
 
         if ($expression instanceof Twig_Node_Expression_Filter) {
 
-            // don't escape if the last filter in the chain is an "escaper"
+            // 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');
-            if (isset($filterMap[$name]) && $filterMap[$name]->isEscaper()) {
+            $args = $expression->getNode('filters')->getNode($i+1);
+            if (isset($filterMap[$name]) && $filterMap[$name]->isSafe($type, $args)) {
                 return $node;
             }
         }
@@ -108,7 +109,11 @@ class Twig_NodeVisitor_Escaper implements Twig_NodeVisitorInterface
         if (count($this->statusStack)) {
             return $this->statusStack[count($this->statusStack) - 1];
         } else {
-            return $env->hasExtension('escaper') ? $env->getExtension('escaper')->isGlobal() : false;
+            if ($env->hasExtension('escaper') && $env->getExtension('escaper')->isGlobal()) {
+                return 'html';
+            } else {
+                return false;
+            }
         }
     }
 
index 08f047b..ac3791b 100644 (file)
@@ -24,7 +24,7 @@ class Twig_TokenParser_AutoEscape extends Twig_TokenParser
         if (!in_array($value, array('on', 'off'))) {
             throw new Twig_SyntaxError("Autoescape value must be 'on' or 'off'", $lineno);
         }
-        $value = 'on' === $value ? true : false;
+        $value = 'on' === $value ? 'html' : false;
 
         if ($this->parser->getStream()->test(Twig_Token::NAME_TYPE)) {
             if (false === $value) {
diff --git a/test/Twig/Tests/Fixtures/tags/autoescape/type.test b/test/Twig/Tests/Fixtures/tags/autoescape/type.test
new file mode 100644 (file)
index 0000000..de3f6c3
--- /dev/null
@@ -0,0 +1,69 @@
+--TEST--
+escape types
+--TEMPLATE--
+
+1. autoescape on |escape('js')
+
+{% autoescape on %}
+<a onclick="alert(&quot;{{ msg|escape('js') }}&quot;)"></a>
+{% endautoescape %}
+
+2. autoescape on html |escape('js')
+
+{% autoescape on html %}
+<a onclick="alert(&quot;{{ msg|escape('js') }}&quot;)"></a>
+{% endautoescape %}
+
+3. autoescape on js |escape('js')
+
+{% autoescape on js %}
+<a onclick="alert(&quot;{{ msg|escape('js') }}&quot;)"></a>
+{% endautoescape %}
+
+4. no escape
+
+{% autoescape off %}
+<a onclick="alert(&quot;{{ msg }}&quot;)"></a>
+{% endautoescape %}
+
+5. |escape('js')|escape('html')
+
+{% autoescape off %}
+<a onclick="alert(&quot;{{ msg|escape('js')|escape('html') }}&quot;)"></a>
+{% endautoescape %}
+
+6. autoescape on html |escape('js')|escape('html')
+
+{% autoescape on html %}
+<a onclick="alert(&quot;{{ msg|escape('js')|escape('html') }}&quot;)"></a>
+{% endautoescape %}
+
+--DATA--
+return array('msg' => "<>\n'\"")
+--EXPECT--
+
+1. autoescape on |escape('js')
+
+<a onclick="alert(&quot;\x3c\x3e\x0a\x27\x22&quot;)"></a>
+
+2. autoescape on html |escape('js')
+
+<a onclick="alert(&quot;\x3c\x3e\x0a\x27\x22&quot;)"></a>
+
+3. autoescape on js |escape('js')
+
+<a onclick="alert(&quot;\x3c\x3e\x0a\x27\x22&quot;)"></a>
+
+4. no escape
+
+<a onclick="alert(&quot;<>
+'"&quot;)"></a>
+
+5. |escape('js')|escape('html')
+
+<a onclick="alert(&quot;\x3c\x3e\x0a\x27\x22&quot;)"></a>
+
+6. autoescape on html |escape('js')|escape('html')
+
+<a onclick="alert(&quot;\x3c\x3e\x0a\x27\x22&quot;)"></a>
+
index ca6ae7d..b0405a3 100644 (file)
@@ -103,7 +103,7 @@ class TestExtension extends Twig_Extension
 {
     public function getFilters()
     {
-        return array('nl2br' => new Twig_Filter_Method($this, 'nl2br', array('needs_environment' => true, 'is_escaper' => true)));
+        return array('nl2br' => new Twig_Filter_Method($this, 'nl2br', array('needs_environment' => true, 'is_safe' => array('html'))));
     }
 
     public function nl2br($env, $value, $sep = '<br />')