Browse code

added the possibility to register classes/interfaces as being safe for the escaper

Fabien Potencier authored on 21/05/2019 13:07:36
Showing 5 changed files
... ...
@@ -1,5 +1,6 @@
1 1
 * 2.11.0 (2019-XX-XX)
2 2
 
3
+ * added the possibility to register classes/interfaces as being safe for the escaper ("EscaperExtension::addSafeClass()")
3 4
  * deprecated CoreExtension::setEscaper() and CoreExtension::getEscapers() in favor of the same methods on EscaperExtension
4 5
  * macros are now auto-imported in the template they are defined (under the ``_self`` variable)
5 6
  * added support for macros on "is defined" tests
... ...
@@ -415,6 +415,24 @@ The escaping rules are implemented as follows:
415 415
         {% set text = "Twig<br />" %}
416 416
         {{ foo ? text|escape : "<br />Twig" }} {# the result of the expression won't be escaped #}
417 417
 
418
+* Objects with a ``__toString`` method are converted to strings and
419
+  escaped. You can mark some classes and/or interfaces as being safe for some
420
+  strategies via ``EscaperExtension::addSafeClass()``:
421
+
422
+  .. code-block:: twig
423
+
424
+        // mark object of class Foo as safe for the HTML strategy
425
+        $escaper->addSafeClass('Foo', ['html']);
426
+
427
+        // mark object of interface Foo as safe for the HTML strategy
428
+        $escaper->addSafeClass('FooInterface', ['html']);
429
+
430
+        // mark object of class Foo as safe for the HTML and JS strategies
431
+        $escaper->addSafeClass('Foo', ['html', 'js']);
432
+
433
+        // mark object of class Foo as safe for all strategies
434
+        $escaper->addSafeClass('Foo', ['all']);
435
+
418 436
 * Escaping is applied before printing, after any other filter is applied:
419 437
 
420 438
   .. code-block:: twig
... ...
@@ -14,11 +14,17 @@ use Twig\FileExtensionEscapingStrategy;
14 14
 use Twig\NodeVisitor\EscaperNodeVisitor;
15 15
 use Twig\TokenParser\AutoEscapeTokenParser;
16 16
 use Twig\TwigFilter;
17
+use Twig\Environment;
18
+use Twig\Error\RuntimeError;
19
+use Twig\Extension\CoreExtension;
20
+use Twig\Markup;
17 21
 
18 22
 final class EscaperExtension extends AbstractExtension
19 23
 {
20 24
     private $defaultStrategy;
21 25
     private $escapers = [];
26
+    private $safeClasses = [];
27
+    private $safeLookup = [];
22 28
 
23 29
     /**
24 30
      * @param string|false|callable $defaultStrategy An escaping strategy
... ...
@@ -43,8 +49,8 @@ final class EscaperExtension extends AbstractExtension
43 49
     public function getFilters()
44 50
     {
45 51
         return [
46
-            new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
47
-            new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
52
+            new TwigFilter('escape', [$this, 'escape'], ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
53
+            new TwigFilter('e', [$this, 'escape'], ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
48 54
             new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]),
49 55
         ];
50 56
     }
... ...
@@ -104,262 +110,309 @@ final class EscaperExtension extends AbstractExtension
104 110
     {
105 111
         return $this->escapers;
106 112
     }
107
-}
108 113
 
109
-class_alias('Twig\Extension\EscaperExtension', 'Twig_Extension_Escaper');
110
-}
111
-
112
-namespace {
113
-use Twig\Environment;
114
-use Twig\Error\RuntimeError;
115
-use Twig\Extension\CoreExtension;
116
-use Twig\Extension\EscaperExtension;
117
-use Twig\Markup;
118
-use Twig\Node\Expression\ConstantExpression;
119
-use Twig\Node\Node;
114
+    public function setSafeClasses(array $safeClasses = [])
115
+    {
116
+        $this->safeClasses = [];
117
+        $this->safeLookup = [];
118
+        foreach ($safeClasses as $class => $strategies) {
119
+            $this->addSafeClass($class, $strategies);
120
+        }
121
+    }
120 122
 
121
-/**
122
- * Marks a variable as being safe.
123
- *
124
- * @param string $string A PHP variable
125
- *
126
- * @return string
127
- */
128
-function twig_raw_filter($string)
129
-{
130
-    return $string;
131
-}
123
+    public function addSafeClass(string $class, array $strategies)
124
+    {
125
+        $class = ltrim($class, '\\');
126
+        if (!isset($this->safeClasses[$class])) {
127
+            $this->safeClasses[$class] = [];
128
+        }
129
+        $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies);
132 130
 
133
-/**
134
- * Escapes a string.
135
- *
136
- * @param mixed  $string     The value to be escaped
137
- * @param string $strategy   The escaping strategy
138
- * @param string $charset    The charset
139
- * @param bool   $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
140
- *
141
- * @return string
142
- */
143
-function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
144
-{
145
-    if ($autoescape && $string instanceof Markup) {
146
-        return $string;
131
+        foreach ($strategies as $strategy) {
132
+            $this->safeLookup[$strategy][$class] = true;
133
+        }
147 134
     }
148 135
 
149
-    if (!\is_string($string)) {
150
-        if (\is_object($string) && method_exists($string, '__toString')) {
151
-            $string = (string) $string;
152
-        } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
136
+    /**
137
+     * Escapes a string.
138
+     *
139
+     * @param mixed  $string     The value to be escaped
140
+     * @param string $strategy   The escaping strategy
141
+     * @param string $charset    The charset
142
+     * @param bool   $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
143
+     *
144
+     * @return string
145
+     */
146
+    function escape(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
147
+    {
148
+        if ($autoescape && $string instanceof Markup) {
153 149
             return $string;
154 150
         }
155
-    }
156
-
157
-    if ('' === $string) {
158
-        return '';
159
-    }
160 151
 
161
-    if (null === $charset) {
162
-        $charset = $env->getCharset();
163
-    }
152
+        if (!\is_string($string)) {
153
+            if (\is_object($string) && method_exists($string, '__toString')) {
154
+                if ($autoescape) {
155
+                    $c = get_class($string);
156
+                    if (!isset($this->safeClasses[$c])) {
157
+                        $this->safeClasses[$c] = [];
158
+                        foreach (class_parents($string) + class_implements($string) as $class) {
159
+                            if (isset($this->safeClasses[$class])) {
160
+                                $this->safeClasses[$c] = array_unique(array_merge($this->safeClasses[$c], $this->safeClasses[$class]));
161
+                                foreach ($this->safeClasses[$class] as $s) {
162
+                                    $this->safeLookup[$s][$c] = true;
163
+                                }
164
+                            }
165
+                        }
166
+                    }
167
+                    if (isset($this->safeLookup[$strategy][$c]) || isset($this->safeLookup['all'][$c])) {
168
+                        return (string) $string;
169
+                    }
170
+                }
164 171
 
165
-    switch ($strategy) {
166
-        case 'html':
167
-            // see https://secure.php.net/htmlspecialchars
168
-
169
-            // Using a static variable to avoid initializing the array
170
-            // each time the function is called. Moving the declaration on the
171
-            // top of the function slow downs other escaping strategies.
172
-            static $htmlspecialcharsCharsets = [
173
-                'ISO-8859-1' => true, 'ISO8859-1' => true,
174
-                'ISO-8859-15' => true, 'ISO8859-15' => true,
175
-                'utf-8' => true, 'UTF-8' => true,
176
-                'CP866' => true, 'IBM866' => true, '866' => true,
177
-                'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
178
-                '1251' => true,
179
-                'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
180
-                'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
181
-                'BIG5' => true, '950' => true,
182
-                'GB2312' => true, '936' => true,
183
-                'BIG5-HKSCS' => true,
184
-                'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
185
-                'EUC-JP' => true, 'EUCJP' => true,
186
-                'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
187
-            ];
188
-
189
-            if (isset($htmlspecialcharsCharsets[$charset])) {
190
-                return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
172
+                $string = (string) $string;
173
+            } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
174
+                return $string;
191 175
             }
176
+        }
192 177
 
193
-            if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
194
-                // cache the lowercase variant for future iterations
195
-                $htmlspecialcharsCharsets[$charset] = true;
178
+        if ('' === $string) {
179
+            return '';
180
+        }
196 181
 
197
-                return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
198
-            }
182
+        if (null === $charset) {
183
+            $charset = $env->getCharset();
184
+        }
185
+
186
+        switch ($strategy) {
187
+            case 'html':
188
+                // see https://secure.php.net/htmlspecialchars
189
+
190
+                // Using a static variable to avoid initializing the array
191
+                // each time the function is called. Moving the declaration on the
192
+                // top of the function slow downs other escaping strategies.
193
+                static $htmlspecialcharsCharsets = [
194
+                    'ISO-8859-1' => true, 'ISO8859-1' => true,
195
+                    'ISO-8859-15' => true, 'ISO8859-15' => true,
196
+                    'utf-8' => true, 'UTF-8' => true,
197
+                    'CP866' => true, 'IBM866' => true, '866' => true,
198
+                    'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
199
+                    '1251' => true,
200
+                    'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
201
+                    'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
202
+                    'BIG5' => true, '950' => true,
203
+                    'GB2312' => true, '936' => true,
204
+                    'BIG5-HKSCS' => true,
205
+                    'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
206
+                    'EUC-JP' => true, 'EUCJP' => true,
207
+                    'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
208
+                ];
209
+
210
+                if (isset($htmlspecialcharsCharsets[$charset])) {
211
+                    return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
212
+                }
199 213
 
200
-            $string = iconv($charset, 'UTF-8', $string);
201
-            $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
214
+                if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
215
+                    // cache the lowercase variant for future iterations
216
+                    $htmlspecialcharsCharsets[$charset] = true;
202 217
 
203
-            return iconv('UTF-8', $charset, $string);
218
+                    return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
219
+                }
204 220
 
205
-        case 'js':
206
-            // escape all non-alphanumeric characters
207
-            // into their \x or \uHHHH representations
208
-            if ('UTF-8' !== $charset) {
209 221
                 $string = iconv($charset, 'UTF-8', $string);
210
-            }
222
+                $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
211 223
 
212
-            if (!preg_match('//u', $string)) {
213
-                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
214
-            }
224
+                return iconv('UTF-8', $charset, $string);
215 225
 
216
-            $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
217
-                $char = $matches[0];
218
-
219
-                /*
220
-                 * A few characters have short escape sequences in JSON and JavaScript.
221
-                 * Escape sequences supported only by JavaScript, not JSON, are ommitted.
222
-                 * \" is also supported but omitted, because the resulting string is not HTML safe.
223
-                 */
224
-                static $shortMap = [
225
-                    '\\' => '\\\\',
226
-                    '/' => '\\/',
227
-                    "\x08" => '\b',
228
-                    "\x0C" => '\f',
229
-                    "\x0A" => '\n',
230
-                    "\x0D" => '\r',
231
-                    "\x09" => '\t',
232
-                ];
226
+            case 'js':
227
+                // escape all non-alphanumeric characters
228
+                // into their \x or \uHHHH representations
229
+                if ('UTF-8' !== $charset) {
230
+                    $string = iconv($charset, 'UTF-8', $string);
231
+                }
233 232
 
234
-                if (isset($shortMap[$char])) {
235
-                    return $shortMap[$char];
233
+                if (!preg_match('//u', $string)) {
234
+                    throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
236 235
                 }
237 236
 
238
-                // \uHHHH
239
-                $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
240
-                $char = strtoupper(bin2hex($char));
237
+                $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
238
+                    $char = $matches[0];
241 239
 
242
-                if (4 >= \strlen($char)) {
243
-                    return sprintf('\u%04s', $char);
244
-                }
240
+                    /*
241
+                    * A few characters have short escape sequences in JSON and JavaScript.
242
+                    * Escape sequences supported only by JavaScript, not JSON, are ommitted.
243
+                    * \" is also supported but omitted, because the resulting string is not HTML safe.
244
+                    */
245
+                    static $shortMap = [
246
+                        '\\' => '\\\\',
247
+                        '/' => '\\/',
248
+                        "\x08" => '\b',
249
+                        "\x0C" => '\f',
250
+                        "\x0A" => '\n',
251
+                        "\x0D" => '\r',
252
+                        "\x09" => '\t',
253
+                    ];
245 254
 
246
-                return sprintf('\u%04s\u%04s', substr($char, 0, -4), substr($char, -4));
247
-            }, $string);
255
+                    if (isset($shortMap[$char])) {
256
+                        return $shortMap[$char];
257
+                    }
248 258
 
249
-            if ('UTF-8' !== $charset) {
250
-                $string = iconv('UTF-8', $charset, $string);
251
-            }
259
+                    // \uHHHH
260
+                    $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
261
+                    $char = strtoupper(bin2hex($char));
252 262
 
253
-            return $string;
263
+                    if (4 >= \strlen($char)) {
264
+                        return sprintf('\u%04s', $char);
265
+                    }
254 266
 
255
-        case 'css':
256
-            if ('UTF-8' !== $charset) {
257
-                $string = iconv($charset, 'UTF-8', $string);
258
-            }
267
+                    return sprintf('\u%04s\u%04s', substr($char, 0, -4), substr($char, -4));
268
+                }, $string);
259 269
 
260
-            if (!preg_match('//u', $string)) {
261
-                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
262
-            }
270
+                if ('UTF-8' !== $charset) {
271
+                    $string = iconv('UTF-8', $charset, $string);
272
+                }
263 273
 
264
-            $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
265
-                $char = $matches[0];
274
+                return $string;
266 275
 
267
-                return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
268
-            }, $string);
276
+            case 'css':
277
+                if ('UTF-8' !== $charset) {
278
+                    $string = iconv($charset, 'UTF-8', $string);
279
+                }
269 280
 
270
-            if ('UTF-8' !== $charset) {
271
-                $string = iconv('UTF-8', $charset, $string);
272
-            }
281
+                if (!preg_match('//u', $string)) {
282
+                    throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
283
+                }
273 284
 
274
-            return $string;
285
+                $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
286
+                    $char = $matches[0];
275 287
 
276
-        case 'html_attr':
277
-            if ('UTF-8' !== $charset) {
278
-                $string = iconv($charset, 'UTF-8', $string);
279
-            }
288
+                    return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
289
+                }, $string);
280 290
 
281
-            if (!preg_match('//u', $string)) {
282
-                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
283
-            }
291
+                if ('UTF-8' !== $charset) {
292
+                    $string = iconv('UTF-8', $charset, $string);
293
+                }
294
+
295
+                return $string;
284 296
 
285
-            $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
286
-                /**
287
-                 * This function is adapted from code coming from Zend Framework.
288
-                 *
289
-                 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
290
-                 * @license   https://framework.zend.com/license/new-bsd New BSD License
291
-                 */
292
-                $chr = $matches[0];
293
-                $ord = \ord($chr);
294
-
295
-                /*
296
-                 * The following replaces characters undefined in HTML with the
297
-                 * hex entity for the Unicode replacement character.
298
-                 */
299
-                if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) {
300
-                    return '&#xFFFD;';
297
+            case 'html_attr':
298
+                if ('UTF-8' !== $charset) {
299
+                    $string = iconv($charset, 'UTF-8', $string);
301 300
                 }
302 301
 
303
-                /*
304
-                 * Check if the current character to escape has a name entity we should
305
-                 * replace it with while grabbing the hex value of the character.
306
-                 */
307
-                if (1 === \strlen($chr)) {
308
-                    /*
309
-                     * While HTML supports far more named entities, the lowest common denominator
310
-                     * has become HTML5's XML Serialisation which is restricted to the those named
311
-                     * entities that XML supports. Using HTML entities would result in this error:
312
-                     *     XML Parsing Error: undefined entity
302
+                if (!preg_match('//u', $string)) {
303
+                    throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
304
+                }
305
+
306
+                $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
307
+                    /**
308
+                     * This function is adapted from code coming from Zend Framework.
309
+                     *
310
+                     * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
311
+                     * @license   https://framework.zend.com/license/new-bsd New BSD License
313 312
                      */
314
-                    static $entityMap = [
315
-                        34 => '&quot;', /* quotation mark */
316
-                        38 => '&amp;',  /* ampersand */
317
-                        60 => '&lt;',   /* less-than sign */
318
-                        62 => '&gt;',   /* greater-than sign */
319
-                    ];
313
+                    $chr = $matches[0];
314
+                    $ord = \ord($chr);
320 315
 
321
-                    if (isset($entityMap[$ord])) {
322
-                        return $entityMap[$ord];
316
+                    /*
317
+                    * The following replaces characters undefined in HTML with the
318
+                    * hex entity for the Unicode replacement character.
319
+                    */
320
+                    if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) {
321
+                        return '&#xFFFD;';
322
+                    }
323
+
324
+                    /*
325
+                    * Check if the current character to escape has a name entity we should
326
+                    * replace it with while grabbing the hex value of the character.
327
+                    */
328
+                    if (1 === \strlen($chr)) {
329
+                        /*
330
+                        * While HTML supports far more named entities, the lowest common denominator
331
+                        * has become HTML5's XML Serialisation which is restricted to the those named
332
+                        * entities that XML supports. Using HTML entities would result in this error:
333
+                        *     XML Parsing Error: undefined entity
334
+                        */
335
+                        static $entityMap = [
336
+                            34 => '&quot;', /* quotation mark */
337
+                            38 => '&amp;',  /* ampersand */
338
+                            60 => '&lt;',   /* less-than sign */
339
+                            62 => '&gt;',   /* greater-than sign */
340
+                        ];
341
+
342
+                        if (isset($entityMap[$ord])) {
343
+                            return $entityMap[$ord];
344
+                        }
345
+
346
+                        return sprintf('&#x%02X;', $ord);
323 347
                     }
324 348
 
325
-                    return sprintf('&#x%02X;', $ord);
349
+                    /*
350
+                    * Per OWASP recommendations, we'll use hex entities for any other
351
+                    * characters where a named entity does not exist.
352
+                    */
353
+                    return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8'));
354
+                }, $string);
355
+
356
+                if ('UTF-8' !== $charset) {
357
+                    $string = iconv('UTF-8', $charset, $string);
326 358
                 }
327 359
 
328
-                /*
329
-                 * Per OWASP recommendations, we'll use hex entities for any other
330
-                 * characters where a named entity does not exist.
331
-                 */
332
-                return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8'));
333
-            }, $string);
360
+                return $string;
334 361
 
335
-            if ('UTF-8' !== $charset) {
336
-                $string = iconv('UTF-8', $charset, $string);
337
-            }
362
+            case 'url':
363
+                return rawurlencode($string);
338 364
 
339
-            return $string;
365
+            default:
366
+                static $escapers;
367
+
368
+                if (null === $escapers) {
369
+                    // merge the ones set on CoreExtension for BC (to be removed in 3.0)
370
+                    $escapers = array_merge(
371
+                        $env->getExtension(CoreExtension::class)->getEscapers(false),
372
+                        $env->getExtension(EscaperExtension::class)->getEscapers()
373
+                    );
374
+                }
340 375
 
341
-        case 'url':
342
-            return rawurlencode($string);
376
+                if (isset($escapers[$strategy])) {
377
+                    return $escapers[$strategy]($env, $string, $charset);
378
+                }
343 379
 
344
-        default:
345
-            static $escapers;
380
+                $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
346 381
 
347
-            if (null === $escapers) {
348
-                // merge the ones set on CoreExtension for BC (to be removed in 3.0)
349
-                $escapers = array_merge(
350
-                    $env->getExtension(CoreExtension::class)->getEscapers(false),
351
-                    $env->getExtension(EscaperExtension::class)->getEscapers()
352
-                );
353
-            }
382
+                throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
383
+        }
384
+    }
385
+}
354 386
 
355
-            if (isset($escapers[$strategy])) {
356
-                return $escapers[$strategy]($env, $string, $charset);
357
-            }
387
+class_alias('Twig\Extension\EscaperExtension', 'Twig_Extension_Escaper');
388
+}
358 389
 
359
-            $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
390
+namespace {
391
+use Twig\Environment;
392
+use Twig\Extension\EscaperExtension;
393
+use Twig\Node\Expression\ConstantExpression;
394
+use Twig\Node\Node;
360 395
 
361
-            throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
362
-    }
396
+/**
397
+ * Marks a variable as being safe.
398
+ *
399
+ * @param string $string A PHP variable
400
+ *
401
+ * @return string
402
+ */
403
+function twig_raw_filter($string)
404
+{
405
+    return $string;
406
+}
407
+
408
+/**
409
+ * @deprecated since Twig 2.11, to be removed in 3.0; use EscaperExtension::escape instead
410
+ */
411
+function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
412
+{
413
+    @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.11; use "%s::escape" instead.', __METHOD__, EscaperExtension::class), E_USER_DEPRECATED);
414
+
415
+    return $env->getExtension(EscaperExtension::class)->escape($env, $string, $strategy, $charset, $autoescape);
363 416
 }
364 417
 
365 418
 /**
... ...
@@ -158,7 +158,7 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
158 158
     {
159 159
         $twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
160 160
         foreach ($this->htmlSpecialChars as $key => $value) {
161
-            $this->assertEquals($value, twig_escape_filter($twig, $key, 'html'), 'Failed to escape: '.$key);
161
+            $this->assertEquals($value, $twig->getExtension(EscaperExtension::class)->escape($twig, $key, 'html'), 'Failed to escape: '.$key);
162 162
         }
163 163
     }
164 164
 
... ...
@@ -166,7 +166,7 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
166 166
     {
167 167
         $twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
168 168
         foreach ($this->htmlAttrSpecialChars as $key => $value) {
169
-            $this->assertEquals($value, twig_escape_filter($twig, $key, 'html_attr'), 'Failed to escape: '.$key);
169
+            $this->assertEquals($value, $twig->getExtension(EscaperExtension::class)->escape($twig, $key, 'html_attr'), 'Failed to escape: '.$key);
170 170
         }
171 171
     }
172 172
 
... ...
@@ -174,47 +174,47 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
174 174
     {
175 175
         $twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
176 176
         foreach ($this->jsSpecialChars as $key => $value) {
177
-            $this->assertEquals($value, twig_escape_filter($twig, $key, 'js'), 'Failed to escape: '.$key);
177
+            $this->assertEquals($value, $twig->getExtension(EscaperExtension::class)->escape($twig, $key, 'js'), 'Failed to escape: '.$key);
178 178
         }
179 179
     }
180 180
 
181 181
     public function testJavascriptEscapingReturnsStringIfZeroLength()
182 182
     {
183 183
         $twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
184
-        $this->assertEquals('', twig_escape_filter($twig, '', 'js'));
184
+        $this->assertEquals('', $twig->getExtension(EscaperExtension::class)->escape($twig, '', 'js'));
185 185
     }
186 186
 
187 187
     public function testJavascriptEscapingReturnsStringIfContainsOnlyDigits()
188 188
     {
189 189
         $twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
190
-        $this->assertEquals('123', twig_escape_filter($twig, '123', 'js'));
190
+        $this->assertEquals('123', $twig->getExtension(EscaperExtension::class)->escape($twig, '123', 'js'));
191 191
     }
192 192
 
193 193
     public function testCssEscapingConvertsSpecialChars()
194 194
     {
195 195
         $twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
196 196
         foreach ($this->cssSpecialChars as $key => $value) {
197
-            $this->assertEquals($value, twig_escape_filter($twig, $key, 'css'), 'Failed to escape: '.$key);
197
+            $this->assertEquals($value, $twig->getExtension(EscaperExtension::class)->escape($twig, $key, 'css'), 'Failed to escape: '.$key);
198 198
         }
199 199
     }
200 200
 
201 201
     public function testCssEscapingReturnsStringIfZeroLength()
202 202
     {
203 203
         $twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
204
-        $this->assertEquals('', twig_escape_filter($twig, '', 'css'));
204
+        $this->assertEquals('', $twig->getExtension(EscaperExtension::class)->escape($twig, '', 'css'));
205 205
     }
206 206
 
207 207
     public function testCssEscapingReturnsStringIfContainsOnlyDigits()
208 208
     {
209 209
         $twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
210
-        $this->assertEquals('123', twig_escape_filter($twig, '123', 'css'));
210
+        $this->assertEquals('123', $twig->getExtension(EscaperExtension::class)->escape($twig, '123', 'css'));
211 211
     }
212 212
 
213 213
     public function testUrlEscapingConvertsSpecialChars()
214 214
     {
215 215
         $twig = new \Twig\Environment($this->getMockBuilder(\Twig\Loader\LoaderInterface::class)->getMock());
216 216
         foreach ($this->urlSpecialChars as $key => $value) {
217
-            $this->assertEquals($value, twig_escape_filter($twig, $key, 'url'), 'Failed to escape: '.$key);
217
+            $this->assertEquals($value, $twig->getExtension(EscaperExtension::class)->escape($twig, $key, 'url'), 'Failed to escape: '.$key);
218 218
         }
219 219
     }
220 220
 
... ...
@@ -276,15 +276,15 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
276 276
             || $chr >= 0x41 && $chr <= 0x5A
277 277
             || $chr >= 0x61 && $chr <= 0x7A) {
278 278
                 $literal = $this->codepointToUtf8($chr);
279
-                $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'js'));
279
+                $this->assertEquals($literal, $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'js'));
280 280
             } else {
281 281
                 $literal = $this->codepointToUtf8($chr);
282 282
                 if (\in_array($literal, $immune)) {
283
-                    $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'js'));
283
+                    $this->assertEquals($literal, $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'js'));
284 284
                 } else {
285 285
                     $this->assertNotEquals(
286 286
                         $literal,
287
-                        twig_escape_filter($twig, $literal, 'js'),
287
+                        $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'js'),
288 288
                         "$literal should be escaped!");
289 289
                 }
290 290
             }
... ...
@@ -300,15 +300,15 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
300 300
             || $chr >= 0x41 && $chr <= 0x5A
301 301
             || $chr >= 0x61 && $chr <= 0x7A) {
302 302
                 $literal = $this->codepointToUtf8($chr);
303
-                $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'html_attr'));
303
+                $this->assertEquals($literal, $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'html_attr'));
304 304
             } else {
305 305
                 $literal = $this->codepointToUtf8($chr);
306 306
                 if (\in_array($literal, $immune)) {
307
-                    $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'html_attr'));
307
+                    $this->assertEquals($literal, $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'html_attr'));
308 308
                 } else {
309 309
                     $this->assertNotEquals(
310 310
                         $literal,
311
-                        twig_escape_filter($twig, $literal, 'html_attr'),
311
+                        $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'html_attr'),
312 312
                         "$literal should be escaped!");
313 313
                 }
314 314
             }
... ...
@@ -324,12 +324,12 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
324 324
             || $chr >= 0x41 && $chr <= 0x5A
325 325
             || $chr >= 0x61 && $chr <= 0x7A) {
326 326
                 $literal = $this->codepointToUtf8($chr);
327
-                $this->assertEquals($literal, twig_escape_filter($twig, $literal, 'css'));
327
+                $this->assertEquals($literal, $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'css'));
328 328
             } else {
329 329
                 $literal = $this->codepointToUtf8($chr);
330 330
                 $this->assertNotEquals(
331 331
                     $literal,
332
-                    twig_escape_filter($twig, $literal, 'css'),
332
+                    $twig->getExtension(EscaperExtension::class)->escape($twig, $literal, 'css'),
333 333
                     "$literal should be escaped!");
334 334
             }
335 335
         }
... ...
@@ -343,7 +343,7 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
343 343
         $twig = new Environment($this->getMockBuilder(LoaderInterface::class)->getMock());
344 344
         $twig->getExtension(EscaperExtension::class)->setEscaper('foo', 'foo_escaper_for_test');
345 345
 
346
-        $this->assertSame($expected, twig_escape_filter($twig, $string, $strategy));
346
+        $this->assertSame($expected, $twig->getExtension(EscaperExtension::class)->escape($twig, $string, $strategy));
347 347
     }
348 348
 
349 349
     public function provideCustomEscaperCases()
... ...
@@ -360,7 +360,29 @@ class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
360 360
      */
361 361
     public function testUnknownCustomEscaper()
362 362
     {
363
-        twig_escape_filter(new Environment($this->getMockBuilder(LoaderInterface::class)->getMock()), 'foo', 'bar');
363
+        $twig = new Environment($this->getMockBuilder(LoaderInterface::class)->getMock());
364
+        $twig->getExtension(EscaperExtension::class)->escape($twig, 'foo', 'bar');
365
+    }
366
+
367
+    /**
368
+     * @dataProvider provideObjectsForEscaping
369
+     */
370
+    public function testObjectEscaping(string $escapedHtml, string $escapedJs, array $safeClasses)
371
+    {
372
+        $obj = new Twig_Tests_Extension_TestClass();
373
+        $twig = new Environment($this->getMockBuilder('\Twig\Loader\LoaderInterface')->getMock());
374
+        $twig->getExtension('\Twig\Extension\EscaperExtension')->setSafeClasses($safeClasses);
375
+        $this->assertSame($escapedHtml, $twig->getExtension(EscaperExtension::class)->escape($twig, $obj, 'html', null, true));
376
+        $this->assertSame($escapedJs, $twig->getExtension(EscaperExtension::class)->escape($twig, $obj, 'js', null, true));
377
+    }
378
+    public function provideObjectsForEscaping()
379
+    {
380
+        return [
381
+            ['&lt;br /&gt;', '<br />', ['\Twig_Tests_Extension_TestClass' => ['js']]],
382
+            ['<br />', '\u003Cbr\u0020\/\u003E', ['\Twig_Tests_Extension_TestClass' => ['html']]],
383
+            ['&lt;br /&gt;', '<br />', ['\Twig_Tests_Extension_SafeHtmlInterface' => ['js']]],
384
+            ['<br />', '<br />', ['\Twig_Tests_Extension_SafeHtmlInterface' => ['all']]],
385
+        ];
364 386
     }
365 387
 }
366 388
 
... ...
@@ -368,3 +390,14 @@ function foo_escaper_for_test(Environment $twig, $string, $charset)
368 390
 {
369 391
     return $string.$charset;
370 392
 }
393
+
394
+interface Twig_Tests_Extension_SafeHtmlInterface
395
+{
396
+}
397
+class Twig_Tests_Extension_TestClass implements Twig_Tests_Extension_SafeHtmlInterface
398
+{
399
+    public function __toString()
400
+    {
401
+        return '<br />';
402
+    }
403
+}
... ...
@@ -22,6 +22,7 @@ use Twig\TokenParser\AbstractTokenParser;
22 22
 use Twig\TwigFilter;
23 23
 use Twig\TwigFunction;
24 24
 use Twig\TwigTest;
25
+use Twig\Extension\EscaperExtension;
25 26
 
26 27
 // This function is defined to check that escaping strategies
27 28
 // like html works even if a function with the same name is defined.
... ...
@@ -203,7 +204,7 @@ class TwigTestExtension extends AbstractExtension
203 204
      */
204 205
     public function escape_and_nl2br($env, $value, $sep = '<br />')
205 206
     {
206
-        return $this->nl2br(twig_escape_filter($env, $value, 'html'), $sep);
207
+        return $this->nl2br($env->getExtension(EscaperExtension::class)->escape($env, $value, 'html'), $sep);
207 208
     }
208 209
 
209 210
     /**