Browse code

feature #3026 Deprecate CoreExtension::setEscaper() and CoreExtension::getEscaper() in favor of the same methods on EscaperExtension (fabpot)

This PR was merged into the 2.x branch.

Discussion
----------

Deprecate CoreExtension::setEscaper() and CoreExtension::getEscaper() in favor of the same methods on EscaperExtension

This is some preliminary work to ease #3025. Everything related to escaping is now part of the `EscaperExtension` instead of `CoreExtension`. This PR is submitted on 2.x because both extensions are always available in 2.x (which is not the case on 1.x).

Commits
-------

59d1d5d4 deprecated CoreExtension::setEscaper() and CoreExtension::getEscaper() in favor of the same methods on EscaperExtension

Fabien Potencier authored on 21/05/2019 13:00:59
Showing 6 changed files
... ...
@@ -1,5 +1,6 @@
1 1
 * 2.11.0 (2019-XX-XX)
2 2
 
3
+ * deprecated CoreExtension::setEscaper() and CoreExtension::getEscapers() in favor of the same methods on EscaperExtension
3 4
  * macros are now auto-imported in the template they are defined (under the ``_self`` variable)
4 5
  * added support for macros on "is defined" tests
5 6
  * fixed macros "import" when using the same name in the parent and child templates
... ...
@@ -93,6 +93,13 @@ Interfaces
93 93
 * As of Twig 2.7, the ``Twig\Extension\InitRuntimeInterface`` interface is
94 94
   deprecated and will be removed in Twig 3.0.
95 95
 
96
+Extensions
97
+----------
98
+
99
+* As of Twig 2.11, the ``Twig\Extension\CoreExtension::setEscaper()`` and
100
+  ``Twig\Extension\CoreExtension::getEscapers()`` are deprecated. Use the same
101
+  methods on ``Twig\Extension\EscaperExtension`` instead.
102
+
96 103
 Miscellaneous
97 104
 -------------
98 105
 
... ...
@@ -83,9 +83,13 @@ final class CoreExtension extends AbstractExtension
83 83
      *
84 84
      * @param string   $strategy The strategy name that should be used as a strategy in the escape call
85 85
      * @param callable $callable A valid PHP callable
86
+     *
87
+     * @deprecated since Twig 2.11, to be removed in 3.0; use the same method on EscaperExtension instead
86 88
      */
87 89
     public function setEscaper($strategy, callable $callable)
88 90
     {
91
+        @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.11; use "%s::setEscaper" instead.', __METHOD__, EscaperExtension::class), E_USER_DEPRECATED);
92
+
89 93
         $this->escapers[$strategy] = $callable;
90 94
     }
91 95
 
... ...
@@ -93,9 +97,15 @@ final class CoreExtension extends AbstractExtension
93 97
      * Gets all defined escapers.
94 98
      *
95 99
      * @return callable[] An array of escapers
100
+     *
101
+     * @deprecated since Twig 2.11, to be removed in 3.0; use the same method on EscaperExtension instead
96 102
      */
97
-    public function getEscapers()
103
+    public function getEscapers(/* $triggerDeprecation = true */)
98 104
     {
105
+        if (0 === \func_num_args() || func_get_arg(0)) {
106
+            @trigger_error(sprintf('The "%s" method is deprecated since Twig 2.11; use "%s::getEscapers" instead.', __METHOD__, EscaperExtension::class), E_USER_DEPRECATED);
107
+        }
108
+
99 109
         return $this->escapers;
100 110
     }
101 111
 
... ...
@@ -244,10 +254,6 @@ final class CoreExtension extends AbstractExtension
244 254
             // iteration and runtime
245 255
             new TwigFilter('default', '_twig_default_filter', ['node_class' => DefaultFilter::class]),
246 256
             new TwigFilter('keys', 'twig_get_array_keys_filter'),
247
-
248
-            // escaping
249
-            new TwigFilter('escape', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
250
-            new TwigFilter('e', 'twig_escape_filter', ['needs_environment' => true, 'is_safe_callback' => 'twig_escape_filter_is_safe']),
251 257
         ];
252 258
     }
253 259
 
... ...
@@ -339,8 +345,6 @@ namespace {
339 345
     use Twig\Extension\CoreExtension;
340 346
     use Twig\Extension\SandboxExtension;
341 347
     use Twig\Markup;
342
-    use Twig\Node\Expression\ConstantExpression;
343
-    use Twig\Node\Node;
344 348
     use Twig\Source;
345 349
     use Twig\Template;
346 350
 
... ...
@@ -982,250 +986,6 @@ function twig_spaceless($content)
982 986
     return trim(preg_replace('/>\s+</', '><', $content));
983 987
 }
984 988
 
985
-/**
986
- * Escapes a string.
987
- *
988
- * @param mixed  $string     The value to be escaped
989
- * @param string $strategy   The escaping strategy
990
- * @param string $charset    The charset
991
- * @param bool   $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false)
992
- *
993
- * @return string
994
- */
995
-function twig_escape_filter(Environment $env, $string, $strategy = 'html', $charset = null, $autoescape = false)
996
-{
997
-    if ($autoescape && $string instanceof Markup) {
998
-        return $string;
999
-    }
1000
-
1001
-    if (!\is_string($string)) {
1002
-        if (\is_object($string) && method_exists($string, '__toString')) {
1003
-            $string = (string) $string;
1004
-        } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'url'])) {
1005
-            return $string;
1006
-        }
1007
-    }
1008
-
1009
-    if ('' === $string) {
1010
-        return '';
1011
-    }
1012
-
1013
-    if (null === $charset) {
1014
-        $charset = $env->getCharset();
1015
-    }
1016
-
1017
-    switch ($strategy) {
1018
-        case 'html':
1019
-            // see https://secure.php.net/htmlspecialchars
1020
-
1021
-            // Using a static variable to avoid initializing the array
1022
-            // each time the function is called. Moving the declaration on the
1023
-            // top of the function slow downs other escaping strategies.
1024
-            static $htmlspecialcharsCharsets = [
1025
-                'ISO-8859-1' => true, 'ISO8859-1' => true,
1026
-                'ISO-8859-15' => true, 'ISO8859-15' => true,
1027
-                'utf-8' => true, 'UTF-8' => true,
1028
-                'CP866' => true, 'IBM866' => true, '866' => true,
1029
-                'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true,
1030
-                '1251' => true,
1031
-                'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true,
1032
-                'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true,
1033
-                'BIG5' => true, '950' => true,
1034
-                'GB2312' => true, '936' => true,
1035
-                'BIG5-HKSCS' => true,
1036
-                'SHIFT_JIS' => true, 'SJIS' => true, '932' => true,
1037
-                'EUC-JP' => true, 'EUCJP' => true,
1038
-                'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true,
1039
-            ];
1040
-
1041
-            if (isset($htmlspecialcharsCharsets[$charset])) {
1042
-                return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
1043
-            }
1044
-
1045
-            if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
1046
-                // cache the lowercase variant for future iterations
1047
-                $htmlspecialcharsCharsets[$charset] = true;
1048
-
1049
-                return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
1050
-            }
1051
-
1052
-            $string = iconv($charset, 'UTF-8', $string);
1053
-            $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
1054
-
1055
-            return iconv('UTF-8', $charset, $string);
1056
-
1057
-        case 'js':
1058
-            // escape all non-alphanumeric characters
1059
-            // into their \x or \uHHHH representations
1060
-            if ('UTF-8' !== $charset) {
1061
-                $string = iconv($charset, 'UTF-8', $string);
1062
-            }
1063
-
1064
-            if (!preg_match('//u', $string)) {
1065
-                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
1066
-            }
1067
-
1068
-            $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', function ($matches) {
1069
-                $char = $matches[0];
1070
-
1071
-                /*
1072
-                 * A few characters have short escape sequences in JSON and JavaScript.
1073
-                 * Escape sequences supported only by JavaScript, not JSON, are ommitted.
1074
-                 * \" is also supported but omitted, because the resulting string is not HTML safe.
1075
-                 */
1076
-                static $shortMap = [
1077
-                    '\\' => '\\\\',
1078
-                    '/' => '\\/',
1079
-                    "\x08" => '\b',
1080
-                    "\x0C" => '\f',
1081
-                    "\x0A" => '\n',
1082
-                    "\x0D" => '\r',
1083
-                    "\x09" => '\t',
1084
-                ];
1085
-
1086
-                if (isset($shortMap[$char])) {
1087
-                    return $shortMap[$char];
1088
-                }
1089
-
1090
-                // \uHHHH
1091
-                $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
1092
-                $char = strtoupper(bin2hex($char));
1093
-
1094
-                if (4 >= \strlen($char)) {
1095
-                    return sprintf('\u%04s', $char);
1096
-                }
1097
-
1098
-                return sprintf('\u%04s\u%04s', substr($char, 0, -4), substr($char, -4));
1099
-            }, $string);
1100
-
1101
-            if ('UTF-8' !== $charset) {
1102
-                $string = iconv('UTF-8', $charset, $string);
1103
-            }
1104
-
1105
-            return $string;
1106
-
1107
-        case 'css':
1108
-            if ('UTF-8' !== $charset) {
1109
-                $string = iconv($charset, 'UTF-8', $string);
1110
-            }
1111
-
1112
-            if (!preg_match('//u', $string)) {
1113
-                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
1114
-            }
1115
-
1116
-            $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
1117
-                $char = $matches[0];
1118
-
1119
-                return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
1120
-            }, $string);
1121
-
1122
-            if ('UTF-8' !== $charset) {
1123
-                $string = iconv('UTF-8', $charset, $string);
1124
-            }
1125
-
1126
-            return $string;
1127
-
1128
-        case 'html_attr':
1129
-            if ('UTF-8' !== $charset) {
1130
-                $string = iconv($charset, 'UTF-8', $string);
1131
-            }
1132
-
1133
-            if (!preg_match('//u', $string)) {
1134
-                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
1135
-            }
1136
-
1137
-            $string = preg_replace_callback('#[^a-zA-Z0-9,\.\-_]#Su', function ($matches) {
1138
-                /**
1139
-                 * This function is adapted from code coming from Zend Framework.
1140
-                 *
1141
-                 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com)
1142
-                 * @license   https://framework.zend.com/license/new-bsd New BSD License
1143
-                 */
1144
-                $chr = $matches[0];
1145
-                $ord = \ord($chr);
1146
-
1147
-                /*
1148
-                 * The following replaces characters undefined in HTML with the
1149
-                 * hex entity for the Unicode replacement character.
1150
-                 */
1151
-                if (($ord <= 0x1f && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7f && $ord <= 0x9f)) {
1152
-                    return '&#xFFFD;';
1153
-                }
1154
-
1155
-                /*
1156
-                 * Check if the current character to escape has a name entity we should
1157
-                 * replace it with while grabbing the hex value of the character.
1158
-                 */
1159
-                if (1 === \strlen($chr)) {
1160
-                    /*
1161
-                     * While HTML supports far more named entities, the lowest common denominator
1162
-                     * has become HTML5's XML Serialisation which is restricted to the those named
1163
-                     * entities that XML supports. Using HTML entities would result in this error:
1164
-                     *     XML Parsing Error: undefined entity
1165
-                     */
1166
-                    static $entityMap = [
1167
-                        34 => '&quot;', /* quotation mark */
1168
-                        38 => '&amp;',  /* ampersand */
1169
-                        60 => '&lt;',   /* less-than sign */
1170
-                        62 => '&gt;',   /* greater-than sign */
1171
-                    ];
1172
-
1173
-                    if (isset($entityMap[$ord])) {
1174
-                        return $entityMap[$ord];
1175
-                    }
1176
-
1177
-                    return sprintf('&#x%02X;', $ord);
1178
-                }
1179
-
1180
-                /*
1181
-                 * Per OWASP recommendations, we'll use hex entities for any other
1182
-                 * characters where a named entity does not exist.
1183
-                 */
1184
-                return sprintf('&#x%04X;', mb_ord($chr, 'UTF-8'));
1185
-            }, $string);
1186
-
1187
-            if ('UTF-8' !== $charset) {
1188
-                $string = iconv('UTF-8', $charset, $string);
1189
-            }
1190
-
1191
-            return $string;
1192
-
1193
-        case 'url':
1194
-            return rawurlencode($string);
1195
-
1196
-        default:
1197
-            static $escapers;
1198
-
1199
-            if (null === $escapers) {
1200
-                $escapers = $env->getExtension(CoreExtension::class)->getEscapers();
1201
-            }
1202
-
1203
-            if (isset($escapers[$strategy])) {
1204
-                return $escapers[$strategy]($env, $string, $charset);
1205
-            }
1206
-
1207
-            $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
1208
-
1209
-            throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
1210
-    }
1211
-}
1212
-
1213
-/**
1214
- * @internal
1215
- */
1216
-function twig_escape_filter_is_safe(Node $filterArgs)
1217
-{
1218
-    foreach ($filterArgs as $arg) {
1219
-        if ($arg instanceof ConstantExpression) {
1220
-            return [$arg->getAttribute('value')];
1221
-        }
1222
-
1223
-        return [];
1224
-    }
1225
-
1226
-    return ['html'];
1227
-}
1228
-
1229 989
 function twig_convert_encoding($string, $to, $from)
1230 990
 {
1231 991
     return iconv($from, $to, $string);
... ...
@@ -18,6 +18,7 @@ use Twig\TwigFilter;
18 18
 final class EscaperExtension extends AbstractExtension
19 19
 {
20 20
     private $defaultStrategy;
21
+    private $escapers = [];
21 22
 
22 23
     /**
23 24
      * @param string|false|callable $defaultStrategy An escaping strategy
... ...
@@ -42,6 +43,8 @@ final class EscaperExtension extends AbstractExtension
42 43
     public function getFilters()
43 44
     {
44 45
         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']),
45 48
             new TwigFilter('raw', 'twig_raw_filter', ['is_safe' => ['all']]),
46 49
         ];
47 50
     }
... ...
@@ -80,12 +83,41 @@ final class EscaperExtension extends AbstractExtension
80 83
 
81 84
         return $this->defaultStrategy;
82 85
     }
86
+
87
+    /**
88
+     * Defines a new escaper to be used via the escape filter.
89
+     *
90
+     * @param string   $strategy The strategy name that should be used as a strategy in the escape call
91
+     * @param callable $callable A valid PHP callable
92
+     */
93
+    public function setEscaper($strategy, callable $callable)
94
+    {
95
+        $this->escapers[$strategy] = $callable;
96
+    }
97
+
98
+    /**
99
+     * Gets all defined escapers.
100
+     *
101
+     * @return callable[] An array of escapers
102
+     */
103
+    public function getEscapers()
104
+    {
105
+        return $this->escapers;
106
+    }
83 107
 }
84 108
 
85 109
 class_alias('Twig\Extension\EscaperExtension', 'Twig_Extension_Escaper');
86 110
 }
87 111
 
88 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;
120
+
89 121
 /**
90 122
  * Marks a variable as being safe.
91 123
  *
... ...
@@ -97,4 +129,252 @@ function twig_raw_filter($string)
97 129
 {
98 130
     return $string;
99 131
 }
132
+
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;
147
+    }
148
+
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'])) {
153
+            return $string;
154
+        }
155
+    }
156
+
157
+    if ('' === $string) {
158
+        return '';
159
+    }
160
+
161
+    if (null === $charset) {
162
+        $charset = $env->getCharset();
163
+    }
164
+
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);
191
+            }
192
+
193
+            if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) {
194
+                // cache the lowercase variant for future iterations
195
+                $htmlspecialcharsCharsets[$charset] = true;
196
+
197
+                return htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, $charset);
198
+            }
199
+
200
+            $string = iconv($charset, 'UTF-8', $string);
201
+            $string = htmlspecialchars($string, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
202
+
203
+            return iconv('UTF-8', $charset, $string);
204
+
205
+        case 'js':
206
+            // escape all non-alphanumeric characters
207
+            // into their \x or \uHHHH representations
208
+            if ('UTF-8' !== $charset) {
209
+                $string = iconv($charset, 'UTF-8', $string);
210
+            }
211
+
212
+            if (!preg_match('//u', $string)) {
213
+                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
214
+            }
215
+
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
+                ];
233
+
234
+                if (isset($shortMap[$char])) {
235
+                    return $shortMap[$char];
236
+                }
237
+
238
+                // \uHHHH
239
+                $char = twig_convert_encoding($char, 'UTF-16BE', 'UTF-8');
240
+                $char = strtoupper(bin2hex($char));
241
+
242
+                if (4 >= \strlen($char)) {
243
+                    return sprintf('\u%04s', $char);
244
+                }
245
+
246
+                return sprintf('\u%04s\u%04s', substr($char, 0, -4), substr($char, -4));
247
+            }, $string);
248
+
249
+            if ('UTF-8' !== $charset) {
250
+                $string = iconv('UTF-8', $charset, $string);
251
+            }
252
+
253
+            return $string;
254
+
255
+        case 'css':
256
+            if ('UTF-8' !== $charset) {
257
+                $string = iconv($charset, 'UTF-8', $string);
258
+            }
259
+
260
+            if (!preg_match('//u', $string)) {
261
+                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
262
+            }
263
+
264
+            $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', function ($matches) {
265
+                $char = $matches[0];
266
+
267
+                return sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8'));
268
+            }, $string);
269
+
270
+            if ('UTF-8' !== $charset) {
271
+                $string = iconv('UTF-8', $charset, $string);
272
+            }
273
+
274
+            return $string;
275
+
276
+        case 'html_attr':
277
+            if ('UTF-8' !== $charset) {
278
+                $string = iconv($charset, 'UTF-8', $string);
279
+            }
280
+
281
+            if (!preg_match('//u', $string)) {
282
+                throw new RuntimeError('The string to escape is not a valid UTF-8 string.');
283
+            }
284
+
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;';
301
+                }
302
+
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
313
+                     */
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
+                    ];
320
+
321
+                    if (isset($entityMap[$ord])) {
322
+                        return $entityMap[$ord];
323
+                    }
324
+
325
+                    return sprintf('&#x%02X;', $ord);
326
+                }
327
+
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);
334
+
335
+            if ('UTF-8' !== $charset) {
336
+                $string = iconv('UTF-8', $charset, $string);
337
+            }
338
+
339
+            return $string;
340
+
341
+        case 'url':
342
+            return rawurlencode($string);
343
+
344
+        default:
345
+            static $escapers;
346
+
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
+            }
354
+
355
+            if (isset($escapers[$strategy])) {
356
+                return $escapers[$strategy]($env, $string, $charset);
357
+            }
358
+
359
+            $validStrategies = implode(', ', array_merge(['html', 'js', 'url', 'css', 'html_attr'], array_keys($escapers)));
360
+
361
+            throw new RuntimeError(sprintf('Invalid escaping strategy "%s" (valid ones: %s).', $strategy, $validStrategies));
362
+    }
363
+}
364
+
365
+/**
366
+ * @internal
367
+ */
368
+function twig_escape_filter_is_safe(Node $filterArgs)
369
+{
370
+    foreach ($filterArgs as $arg) {
371
+        if ($arg instanceof ConstantExpression) {
372
+            return [$arg->getAttribute('value')];
373
+        }
374
+
375
+        return [];
376
+    }
377
+
378
+    return ['html'];
379
+}
100 380
 }
... ...
@@ -10,7 +10,6 @@
10 10
  */
11 11
 
12 12
 use Twig\Environment;
13
-use Twig\Extension\CoreExtension;
14 13
 use Twig\Loader\LoaderInterface;
15 14
 
16 15
 class Twig_Tests_Extension_CoreTest extends \PHPUnit\Framework\TestCase
... ...
@@ -127,34 +126,6 @@ class Twig_Tests_Extension_CoreTest extends \PHPUnit\Framework\TestCase
127 126
     }
128 127
 
129 128
     /**
130
-     * @dataProvider provideCustomEscaperCases
131
-     */
132
-    public function testCustomEscaper($expected, $string, $strategy)
133
-    {
134
-        $twig = new Environment($this->getMockBuilder(LoaderInterface::class)->getMock());
135
-        $twig->getExtension(CoreExtension::class)->setEscaper('foo', 'foo_escaper_for_test');
136
-
137
-        $this->assertSame($expected, twig_escape_filter($twig, $string, $strategy));
138
-    }
139
-
140
-    public function provideCustomEscaperCases()
141
-    {
142
-        return [
143
-            ['fooUTF-8', 'foo', 'foo'],
144
-            ['UTF-8', null, 'foo'],
145
-            ['42UTF-8', 42, 'foo'],
146
-        ];
147
-    }
148
-
149
-    /**
150
-     * @expectedException \Twig\Error\RuntimeError
151
-     */
152
-    public function testUnknownCustomEscaper()
153
-    {
154
-        twig_escape_filter(new Environment($this->getMockBuilder(LoaderInterface::class)->getMock()), 'foo', 'bar');
155
-    }
156
-
157
-    /**
158 129
      * @dataProvider provideTwigFirstCases
159 130
      */
160 131
     public function testTwigFirst($expected, $input)
... ...
@@ -280,11 +251,6 @@ class Twig_Tests_Extension_CoreTest extends \PHPUnit\Framework\TestCase
280 251
     }
281 252
 }
282 253
 
283
-function foo_escaper_for_test(Environment $env, $string, $charset)
284
-{
285
-    return $string.$charset;
286
-}
287
-
288 254
 final class CoreTestIteratorAggregate implements \IteratorAggregate
289 255
 {
290 256
     private $iterator;
291 257
new file mode 100644
... ...
@@ -0,0 +1,50 @@
1
+<?php
2
+
3
+/*
4
+ * This file is part of Twig.
5
+ *
6
+ * (c) Fabien Potencier
7
+ *
8
+ * For the full copyright and license information, please view the LICENSE
9
+ * file that was distributed with this source code.
10
+ */
11
+
12
+use Twig\Environment;
13
+use Twig\Extension\EscaperExtension;
14
+use Twig\Loader\LoaderInterface;
15
+
16
+class Twig_Tests_Extension_EscaperTest extends \PHPUnit\Framework\TestCase
17
+{
18
+    /**
19
+     * @dataProvider provideCustomEscaperCases
20
+     */
21
+    public function testCustomEscaper($expected, $string, $strategy)
22
+    {
23
+        $twig = new Environment($this->getMockBuilder(LoaderInterface::class)->getMock());
24
+        $twig->getExtension(EscaperExtension::class)->setEscaper('foo', 'foo_escaper_for_test');
25
+
26
+        $this->assertSame($expected, twig_escape_filter($twig, $string, $strategy));
27
+    }
28
+
29
+    public function provideCustomEscaperCases()
30
+    {
31
+        return [
32
+            ['fooUTF-8', 'foo', 'foo'],
33
+            ['UTF-8', null, 'foo'],
34
+            ['42UTF-8', 42, 'foo'],
35
+        ];
36
+    }
37
+
38
+    /**
39
+     * @expectedException \Twig\Error\RuntimeError
40
+     */
41
+    public function testUnknownCustomEscaper()
42
+    {
43
+        twig_escape_filter(new Environment($this->getMockBuilder(LoaderInterface::class)->getMock()), 'foo', 'bar');
44
+    }
45
+}
46
+
47
+function foo_escaper_for_test(Environment $env, $string, $charset)
48
+{
49
+    return $string.$charset;
50
+}