/
Escaper.php
350 lines (306 loc) · 11.2 KB
/
Escaper.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
<?php
namespace Symfony\Component\OutputEscaper;
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien.potencier@symfony-project.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
/**
* Escaper provides output escaping features.
*
* @author Fabien Potencier <fabien.potencier@symfony-project.com>
* @author Mike Squire <mike@somosis.co.uk>
*/
class Escaper
{
static protected $charset = 'UTF-8';
static protected $safeClasses = array();
static protected $escapers;
/**
* Decorates a PHP variable with something that will escape any data obtained
* from it.
*
* The following cases are dealt with:
*
* - The value is null or false: null or false is returned.
* - The value is scalar: the result of applying the escaping method is
* returned.
* - The value is an array or an object that implements the ArrayAccess
* interface: the array is decorated such that accesses to elements yield
* an escaped value.
* - The value implements the Traversable interface (either an Iterator, an
* IteratorAggregate or an internal PHP class that implements
* Traversable): decorated much like the array.
* - The value is another type of object: decorated such that the result of
* method calls is escaped.
*
* The escaping method is actually a PHP callable. This class hosts a set
* of standard escaping strategies.
*
* @param mixed $escaper The escaping method (a PHP callable or a named escaper) to apply to the value
* @param mixed $value The value to escape
*
* @return mixed Escaped value
*
* @throws \InvalidArgumentException If the escaping fails
*/
static public function escape($escaper, $value)
{
if (null === $value) {
return $value;
}
if (null === self::$escapers) {
self::initializeEscapers();
}
if (is_string($escaper) && isset(self::$escapers[$escaper])) {
$escaper = self::$escapers[$escaper];
}
// Scalars are anything other than arrays, objects and resources.
if (is_scalar($value)) {
return call_user_func($escaper, $value);
}
if (is_array($value)) {
return new ArrayDecorator($escaper, $value);
}
if (is_object($value)) {
if ($value instanceof BaseEscaper) {
// avoid double decoration
$copy = clone $value;
$copy->setEscaper($escaper);
return $copy;
}
if ($value instanceof SafeDecorator) {
// do not escape objects marked as safe
// return the original object
return $value->getRawValue();
}
if (self::isClassMarkedAsSafe(get_class($value)) || $value instanceof SafeDecoratorInterface) {
// the class or one of its children is marked as safe
// return the unescaped object
return $value;
}
if ($value instanceof \Traversable) {
return new IteratorDecorator($escaper, $value);
}
return new ObjectDecorator($escaper, $value);
}
// it must be a resource; cannot escape that.
throw new \InvalidArgumentException(sprintf('Unable to escape value "%s".', var_export($value, true)));
}
/**
* Unescapes a value that has been escaped previously with the escape() method.
*
* @param mixed $value The value to unescape
*
* @return mixed Unescaped value
*
* @throws \InvalidArgumentException If the escaping fails
*/
static public function unescape($value)
{
if (null === $value || is_bool($value)) {
return $value;
}
if (is_scalar($value)) {
return html_entity_decode($value, ENT_QUOTES, self::$charset);
}
if (is_array($value)) {
foreach ($value as $name => $v) {
$value[$name] = self::unescape($v);
}
return $value;
}
if (is_object($value)) {
return $value instanceof BaseEscaper ? $value->getRawValue() : $value;
}
return $value;
}
/**
* Returns true if the class if marked as safe.
*
* @param string $class A class name
*
* @return bool true if the class if safe, false otherwise
*/
static public function isClassMarkedAsSafe($class)
{
if (in_array($class, self::$safeClasses)) {
return true;
}
foreach (self::$safeClasses as $safeClass) {
if (is_subclass_of($class, $safeClass)) {
return true;
}
}
return false;
}
/**
* Marks an array of classes (and all its children) as being safe for output.
*
* @param array $classes An array of class names
*/
static public function markClassesAsSafe(array $classes)
{
self::$safeClasses = array_unique(array_merge(self::$safeClasses, $classes));
}
/**
* Marks a class (and all its children) as being safe for output.
*
* @param string $class A class name
*/
static public function markClassAsSafe($class)
{
self::markClassesAsSafe(array($class));
}
/**
* Sets the current charset.
*
* @param string $charset The current charset
*/
static public function setCharset($charset)
{
self::$charset = $charset;
}
/**
* Gets the current charset.
*
* @return string The current charset
*/
static public function getCharset()
{
return self::$charset;
}
/**
* Adds a named escaper.
*
* Warning: An escaper must be able to deal with
* double-escaping correctly.
*
* @param string $name The escaper name
* @param mixed $escaper A PHP callable
*/
static public function setEscaper($name, $escaper)
{
self::$escapers[$name] = $escaper;
}
/**
* Gets a named escaper.
*
* @param string $name The escaper name
*
* @return mixed $escaper A PHP callable
*/
static public function getEscaper($escaper)
{
if (null === self::$escapers) {
self::initializeEscapers();
}
return is_string($escaper) && isset(self::$escapers[$escaper]) ? self::$escapers[$escaper] : $escaper;
}
/**
* Initializes the built-in escapers.
*
* Each function specifies a way for applying a transformation to a string
* passed to it. The purpose is for the string to be "escaped" so it is
* suitable for the format it is being displayed in.
*
* For example, the string: "It's required that you enter a username & password.\n"
* If this were to be displayed as HTML it would be sensible to turn the
* ampersand into '&' and the apostrophe into '&aps;'. However if it were
* going to be used as a string in JavaScript to be displayed in an alert box
* it would be right to leave the string as-is, but c-escape the apostrophe and
* the new line.
*
* For each function there is a define to avoid problems with strings being
* incorrectly specified.
*/
static function initializeEscapers()
{
self::$escapers = array(
'htmlspecialchars' =>
/**
* Runs the PHP function htmlspecialchars on the value passed.
*
* @param string $value the value to escape
*
* @return string the escaped value
*/
function ($value)
{
// Numbers and boolean values get turned into strings which can cause problems
// with type comparisons (e.g. === or is_int() etc).
return is_string($value) ? htmlspecialchars($value, ENT_QUOTES, Escaper::getCharset(), false) : $value;
},
'entities' =>
/**
* Runs the PHP function htmlentities on the value passed.
*
* @param string $value the value to escape
* @return string the escaped value
*/
function ($value)
{
// Numbers and boolean values get turned into strings which can cause problems
// with type comparisons (e.g. === or is_int() etc).
return is_string($value) ? htmlentities($value, ENT_QUOTES, Escaper::getCharset(), false) : $value;
},
'raw' =>
/**
* An identity function that merely returns that which it is given, the purpose
* being to be able to specify that the value is not to be escaped in any way.
*
* @param string $value the value to escape
* @return string the escaped value
*/
function ($value)
{
return $value;
},
'js' =>
/**
* A function that escape all non-alphanumeric characters
* into their \xHH or \uHHHH representations
*
* @param string $value the value to escape
* @return string the escaped value
*/
function ($value)
{
if ('UTF-8' != Escaper::getCharset()) {
$string = Escaper::convertEncoding($string, 'UTF-8', Escaper::getCharset());
}
$callback = function ($matches)
{
$char = $matches[0];
// \xHH
if (!isset($char[1])) {
return '\\x'.substr('00'.bin2hex($char), -2);
}
// \uHHHH
$char = Escaper::convertEncoding($char, 'UTF-16BE', 'UTF-8');
return '\\u'.substr('0000'.bin2hex($char), -4);
};
if (null === $string = preg_replace_callback('#[^\p{L}\p{N} ]#u', $callback, $string)) {
throw new InvalidArgumentException('The string to escape is not a valid UTF-8 string.');
}
if ('UTF-8' != Escaper::getCharset()) {
$string = Escaper::convertEncoding($string, Escaper::getCharset(), 'UTF-8');
}
return $string;
},
);
}
static public function convertEncoding($string, $to, $from)
{
if (function_exists('iconv')) {
return iconv($from, $to, $string);
} elseif (function_exists('mb_convert_encoding')) {
return mb_convert_encoding($string, $to, $from);
} else {
throw new RuntimeException('No suitable convert encoding function (use UTF-8 as your encoding or install the iconv or mbstring extension).');
}
}
}