Handle one part of bug 32545 while improving MediaWiki's support for Microdata in...
[lhc/web/wiklou.git] / includes / Sanitizer.php
1 <?php
2 /**
3 * XHTML sanitizer for MediaWiki
4 *
5 * Copyright © 2002-2005 Brion Vibber <brion@pobox.com> et al
6 * http://www.mediawiki.org/
7 *
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
12 *
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License along
19 * with this program; if not, write to the Free Software Foundation, Inc.,
20 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21 * http://www.gnu.org/copyleft/gpl.html
22 *
23 * @file
24 * @ingroup Parser
25 */
26
27 /**
28 * XHTML sanitizer for MediaWiki
29 * @ingroup Parser
30 */
31 class Sanitizer {
32 /**
33 * Regular expression to match various types of character references in
34 * Sanitizer::normalizeCharReferences and Sanitizer::decodeCharReferences
35 */
36 const CHAR_REFS_REGEX =
37 '/&([A-Za-z0-9\x80-\xff]+);
38 |&\#([0-9]+);
39 |&\#[xX]([0-9A-Fa-f]+);
40 |(&)/x';
41
42 /**
43 * Blacklist for evil uris like javascript:
44 * WARNING: DO NOT use this in any place that actually requires blacklisting
45 * for security reasons. There are NUMEROUS[1] ways to bypass blacklisting, the
46 * only way to be secure from javascript: uri based xss vectors is to whitelist
47 * things that you know are safe and deny everything else.
48 * [1]: http://ha.ckers.org/xss.html
49 */
50 const EVIL_URI_PATTERN = '!(^|\s|\*/\s*)(javascript|vbscript)([^\w]|$)!i';
51 const XMLNS_ATTRIBUTE_PATTERN = "/^xmlns:[:A-Z_a-z-.0-9]+$/";
52
53 /**
54 * List of all named character entities defined in HTML 4.01
55 * http://www.w3.org/TR/html4/sgml/entities.html
56 * As well as &apos; which is only defined starting in XHTML1.
57 * @private
58 */
59 static $htmlEntities = array(
60 'Aacute' => 193,
61 'aacute' => 225,
62 'Acirc' => 194,
63 'acirc' => 226,
64 'acute' => 180,
65 'AElig' => 198,
66 'aelig' => 230,
67 'Agrave' => 192,
68 'agrave' => 224,
69 'alefsym' => 8501,
70 'Alpha' => 913,
71 'alpha' => 945,
72 'amp' => 38,
73 'and' => 8743,
74 'ang' => 8736,
75 'apos' => 39, // New in XHTML & HTML 5; avoid in output for compatibility with IE.
76 'Aring' => 197,
77 'aring' => 229,
78 'asymp' => 8776,
79 'Atilde' => 195,
80 'atilde' => 227,
81 'Auml' => 196,
82 'auml' => 228,
83 'bdquo' => 8222,
84 'Beta' => 914,
85 'beta' => 946,
86 'brvbar' => 166,
87 'bull' => 8226,
88 'cap' => 8745,
89 'Ccedil' => 199,
90 'ccedil' => 231,
91 'cedil' => 184,
92 'cent' => 162,
93 'Chi' => 935,
94 'chi' => 967,
95 'circ' => 710,
96 'clubs' => 9827,
97 'cong' => 8773,
98 'copy' => 169,
99 'crarr' => 8629,
100 'cup' => 8746,
101 'curren' => 164,
102 'dagger' => 8224,
103 'Dagger' => 8225,
104 'darr' => 8595,
105 'dArr' => 8659,
106 'deg' => 176,
107 'Delta' => 916,
108 'delta' => 948,
109 'diams' => 9830,
110 'divide' => 247,
111 'Eacute' => 201,
112 'eacute' => 233,
113 'Ecirc' => 202,
114 'ecirc' => 234,
115 'Egrave' => 200,
116 'egrave' => 232,
117 'empty' => 8709,
118 'emsp' => 8195,
119 'ensp' => 8194,
120 'Epsilon' => 917,
121 'epsilon' => 949,
122 'equiv' => 8801,
123 'Eta' => 919,
124 'eta' => 951,
125 'ETH' => 208,
126 'eth' => 240,
127 'Euml' => 203,
128 'euml' => 235,
129 'euro' => 8364,
130 'exist' => 8707,
131 'fnof' => 402,
132 'forall' => 8704,
133 'frac12' => 189,
134 'frac14' => 188,
135 'frac34' => 190,
136 'frasl' => 8260,
137 'Gamma' => 915,
138 'gamma' => 947,
139 'ge' => 8805,
140 'gt' => 62,
141 'harr' => 8596,
142 'hArr' => 8660,
143 'hearts' => 9829,
144 'hellip' => 8230,
145 'Iacute' => 205,
146 'iacute' => 237,
147 'Icirc' => 206,
148 'icirc' => 238,
149 'iexcl' => 161,
150 'Igrave' => 204,
151 'igrave' => 236,
152 'image' => 8465,
153 'infin' => 8734,
154 'int' => 8747,
155 'Iota' => 921,
156 'iota' => 953,
157 'iquest' => 191,
158 'isin' => 8712,
159 'Iuml' => 207,
160 'iuml' => 239,
161 'Kappa' => 922,
162 'kappa' => 954,
163 'Lambda' => 923,
164 'lambda' => 955,
165 'lang' => 9001,
166 'laquo' => 171,
167 'larr' => 8592,
168 'lArr' => 8656,
169 'lceil' => 8968,
170 'ldquo' => 8220,
171 'le' => 8804,
172 'lfloor' => 8970,
173 'lowast' => 8727,
174 'loz' => 9674,
175 'lrm' => 8206,
176 'lsaquo' => 8249,
177 'lsquo' => 8216,
178 'lt' => 60,
179 'macr' => 175,
180 'mdash' => 8212,
181 'micro' => 181,
182 'middot' => 183,
183 'minus' => 8722,
184 'Mu' => 924,
185 'mu' => 956,
186 'nabla' => 8711,
187 'nbsp' => 160,
188 'ndash' => 8211,
189 'ne' => 8800,
190 'ni' => 8715,
191 'not' => 172,
192 'notin' => 8713,
193 'nsub' => 8836,
194 'Ntilde' => 209,
195 'ntilde' => 241,
196 'Nu' => 925,
197 'nu' => 957,
198 'Oacute' => 211,
199 'oacute' => 243,
200 'Ocirc' => 212,
201 'ocirc' => 244,
202 'OElig' => 338,
203 'oelig' => 339,
204 'Ograve' => 210,
205 'ograve' => 242,
206 'oline' => 8254,
207 'Omega' => 937,
208 'omega' => 969,
209 'Omicron' => 927,
210 'omicron' => 959,
211 'oplus' => 8853,
212 'or' => 8744,
213 'ordf' => 170,
214 'ordm' => 186,
215 'Oslash' => 216,
216 'oslash' => 248,
217 'Otilde' => 213,
218 'otilde' => 245,
219 'otimes' => 8855,
220 'Ouml' => 214,
221 'ouml' => 246,
222 'para' => 182,
223 'part' => 8706,
224 'permil' => 8240,
225 'perp' => 8869,
226 'Phi' => 934,
227 'phi' => 966,
228 'Pi' => 928,
229 'pi' => 960,
230 'piv' => 982,
231 'plusmn' => 177,
232 'pound' => 163,
233 'prime' => 8242,
234 'Prime' => 8243,
235 'prod' => 8719,
236 'prop' => 8733,
237 'Psi' => 936,
238 'psi' => 968,
239 'quot' => 34,
240 'radic' => 8730,
241 'rang' => 9002,
242 'raquo' => 187,
243 'rarr' => 8594,
244 'rArr' => 8658,
245 'rceil' => 8969,
246 'rdquo' => 8221,
247 'real' => 8476,
248 'reg' => 174,
249 'rfloor' => 8971,
250 'Rho' => 929,
251 'rho' => 961,
252 'rlm' => 8207,
253 'rsaquo' => 8250,
254 'rsquo' => 8217,
255 'sbquo' => 8218,
256 'Scaron' => 352,
257 'scaron' => 353,
258 'sdot' => 8901,
259 'sect' => 167,
260 'shy' => 173,
261 'Sigma' => 931,
262 'sigma' => 963,
263 'sigmaf' => 962,
264 'sim' => 8764,
265 'spades' => 9824,
266 'sub' => 8834,
267 'sube' => 8838,
268 'sum' => 8721,
269 'sup' => 8835,
270 'sup1' => 185,
271 'sup2' => 178,
272 'sup3' => 179,
273 'supe' => 8839,
274 'szlig' => 223,
275 'Tau' => 932,
276 'tau' => 964,
277 'there4' => 8756,
278 'Theta' => 920,
279 'theta' => 952,
280 'thetasym' => 977,
281 'thinsp' => 8201,
282 'THORN' => 222,
283 'thorn' => 254,
284 'tilde' => 732,
285 'times' => 215,
286 'trade' => 8482,
287 'Uacute' => 218,
288 'uacute' => 250,
289 'uarr' => 8593,
290 'uArr' => 8657,
291 'Ucirc' => 219,
292 'ucirc' => 251,
293 'Ugrave' => 217,
294 'ugrave' => 249,
295 'uml' => 168,
296 'upsih' => 978,
297 'Upsilon' => 933,
298 'upsilon' => 965,
299 'Uuml' => 220,
300 'uuml' => 252,
301 'weierp' => 8472,
302 'Xi' => 926,
303 'xi' => 958,
304 'Yacute' => 221,
305 'yacute' => 253,
306 'yen' => 165,
307 'Yuml' => 376,
308 'yuml' => 255,
309 'Zeta' => 918,
310 'zeta' => 950,
311 'zwj' => 8205,
312 'zwnj' => 8204
313 );
314
315 /**
316 * Character entity aliases accepted by MediaWiki
317 */
318 static $htmlEntityAliases = array(
319 'רלמ' => 'rlm',
320 'رلم' => 'rlm',
321 );
322
323 /**
324 * Lazy-initialised attributes regex, see getAttribsRegex()
325 */
326 static $attribsRegex;
327
328 /**
329 * Regular expression to match HTML/XML attribute pairs within a tag.
330 * Allows some... latitude.
331 * Used in Sanitizer::fixTagAttributes and Sanitizer::decodeTagAttributes
332 */
333 static function getAttribsRegex() {
334 if ( self::$attribsRegex === null ) {
335 $attribFirst = '[:A-Z_a-z0-9]';
336 $attrib = '[:A-Z_a-z-.0-9]';
337 $space = '[\x09\x0a\x0d\x20]';
338 self::$attribsRegex =
339 "/(?:^|$space)({$attribFirst}{$attrib}*)
340 ($space*=$space*
341 (?:
342 # The attribute value: quoted or alone
343 \"([^<\"]*)\"
344 | '([^<']*)'
345 | ([a-zA-Z0-9!#$%&()*,\\-.\\/:;<>?@[\\]^_`{|}~]+)
346 | (\#[0-9a-fA-F]+) # Technically wrong, but lots of
347 # colors are specified like this.
348 # We'll be normalizing it.
349 )
350 )?(?=$space|\$)/sx";
351 }
352 return self::$attribsRegex;
353 }
354
355 /**
356 * Cleans up HTML, removes dangerous tags and attributes, and
357 * removes HTML comments
358 * @private
359 * @param $text String
360 * @param $processCallback Callback to do any variable or parameter replacements in HTML attribute values
361 * @param $args Array for the processing callback
362 * @param $extratags Array for any extra tags to include
363 * @param $removetags Array for any tags (default or extra) to exclude
364 * @return string
365 */
366 static function removeHTMLtags( $text, $processCallback = null, $args = array(), $extratags = array(), $removetags = array() ) {
367 global $wgUseTidy, $wgHtml5, $wgAllowMicrodataAttributes;
368
369 static $htmlpairsStatic, $htmlsingle, $htmlsingleonly, $htmlnest, $tabletags,
370 $htmllist, $listtags, $htmlsingleallowed, $htmlelementsStatic, $staticInitialised;
371
372 wfProfileIn( __METHOD__ );
373
374 if ( !$staticInitialised ) {
375
376 $htmlpairsStatic = array( # Tags that must be closed
377 'b', 'del', 'i', 'ins', 'u', 'font', 'big', 'small', 'sub', 'sup', 'h1',
378 'h2', 'h3', 'h4', 'h5', 'h6', 'cite', 'code', 'em', 's',
379 'strike', 'strong', 'tt', 'var', 'div', 'center',
380 'blockquote', 'ol', 'ul', 'dl', 'table', 'caption', 'pre',
381 'ruby', 'rt' , 'rb' , 'rp', 'p', 'span', 'abbr', 'dfn',
382 'kbd', 'samp'
383 );
384 if ( $wgHtml5 ) {
385 $htmlpairsStatic = array_merge( $htmlpairsStatic, array( 'data', 'time' ) );
386 }
387 $htmlsingle = array(
388 'br', 'hr', 'li', 'dt', 'dd'
389 );
390 $htmlsingleonly = array( # Elements that cannot have close tags
391 'br', 'hr'
392 );
393 if ( $wgHtml5 && $wgAllowMicrodataAttributes ) {
394 $htmlsingle[] = $htmlsingleonly[] = 'meta';
395 $htmlsingle[] = $htmlsingleonly[] = 'link';
396 }
397 $htmlnest = array( # Tags that can be nested--??
398 'table', 'tr', 'td', 'th', 'div', 'blockquote', 'ol', 'ul',
399 'dl', 'font', 'big', 'small', 'sub', 'sup', 'span'
400 );
401 $tabletags = array( # Can only appear inside table, we will close them
402 'td', 'th', 'tr',
403 );
404 $htmllist = array( # Tags used by list
405 'ul','ol',
406 );
407 $listtags = array( # Tags that can appear in a list
408 'li',
409 );
410
411 global $wgAllowImageTag;
412 if ( $wgAllowImageTag ) {
413 $htmlsingle[] = 'img';
414 $htmlsingleonly[] = 'img';
415 }
416
417 $htmlsingleallowed = array_unique( array_merge( $htmlsingle, $tabletags ) );
418 $htmlelementsStatic = array_unique( array_merge( $htmlsingle, $htmlpairsStatic, $htmlnest ) );
419
420 # Convert them all to hashtables for faster lookup
421 $vars = array( 'htmlpairsStatic', 'htmlsingle', 'htmlsingleonly', 'htmlnest', 'tabletags',
422 'htmllist', 'listtags', 'htmlsingleallowed', 'htmlelementsStatic' );
423 foreach ( $vars as $var ) {
424 $$var = array_flip( $$var );
425 }
426 $staticInitialised = true;
427 }
428 # Populate $htmlpairs and $htmlelements with the $extratags and $removetags arrays
429 $extratags = array_flip( $extratags );
430 $removetags = array_flip( $removetags );
431 $htmlpairs = array_merge( $extratags, $htmlpairsStatic );
432 $htmlelements = array_diff_key( array_merge( $extratags, $htmlelementsStatic ) , $removetags );
433
434 # Remove HTML comments
435 $text = Sanitizer::removeHTMLcomments( $text );
436 $bits = explode( '<', $text );
437 $text = str_replace( '>', '&gt;', array_shift( $bits ) );
438 if ( !$wgUseTidy ) {
439 $tagstack = $tablestack = array();
440 foreach ( $bits as $x ) {
441 $regs = array();
442 # $slash: Does the current element start with a '/'?
443 # $t: Current element name
444 # $params: String between element name and >
445 # $brace: Ending '>' or '/>'
446 # $rest: Everything until the next element of $bits
447 if( preg_match( '!^(/?)(\\w+)([^>]*?)(/{0,1}>)([^<]*)$!', $x, $regs ) ) {
448 list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs;
449 } else {
450 $slash = $t = $params = $brace = $rest = null;
451 }
452
453 $badtag = false;
454 if ( isset( $htmlelements[$t = strtolower( $t )] ) ) {
455 # Check our stack
456 if ( $slash && isset( $htmlsingleonly[$t] ) ) {
457 $badtag = true;
458 } elseif ( $slash ) {
459 # Closing a tag... is it the one we just opened?
460 $ot = @array_pop( $tagstack );
461 if ( $ot != $t ) {
462 if ( isset( $htmlsingleallowed[$ot] ) ) {
463 # Pop all elements with an optional close tag
464 # and see if we find a match below them
465 $optstack = array();
466 array_push( $optstack, $ot );
467 wfSuppressWarnings();
468 $ot = array_pop( $tagstack );
469 wfRestoreWarnings();
470 while ( $ot != $t && isset( $htmlsingleallowed[$ot] ) ) {
471 array_push( $optstack, $ot );
472 wfSuppressWarnings();
473 $ot = array_pop( $tagstack );
474 wfRestoreWarnings();
475 }
476 if ( $t != $ot ) {
477 # No match. Push the optional elements back again
478 $badtag = true;
479 wfSuppressWarnings();
480 $ot = array_pop( $optstack );
481 wfRestoreWarnings();
482 while ( $ot ) {
483 array_push( $tagstack, $ot );
484 wfSuppressWarnings();
485 $ot = array_pop( $optstack );
486 wfRestoreWarnings();
487 }
488 }
489 } else {
490 @array_push( $tagstack, $ot );
491 # <li> can be nested in <ul> or <ol>, skip those cases:
492 if ( !isset( $htmllist[$ot] ) || !isset( $listtags[$t] ) ) {
493 $badtag = true;
494 }
495 }
496 } else {
497 if ( $t == 'table' ) {
498 $tagstack = array_pop( $tablestack );
499 }
500 }
501 $newparams = '';
502 } else {
503 # Keep track for later
504 if ( isset( $tabletags[$t] ) &&
505 !in_array( 'table', $tagstack ) ) {
506 $badtag = true;
507 } elseif ( in_array( $t, $tagstack ) &&
508 !isset( $htmlnest [$t ] ) ) {
509 $badtag = true;
510 # Is it a self closed htmlpair ? (bug 5487)
511 } elseif ( $brace == '/>' &&
512 isset( $htmlpairs[$t] ) ) {
513 $badtag = true;
514 } elseif ( isset( $htmlsingleonly[$t] ) ) {
515 # Hack to force empty tag for uncloseable elements
516 $brace = '/>';
517 } elseif ( isset( $htmlsingle[$t] ) ) {
518 # Hack to not close $htmlsingle tags
519 $brace = null;
520 } elseif ( isset( $tabletags[$t] )
521 && in_array( $t, $tagstack ) ) {
522 // New table tag but forgot to close the previous one
523 $text .= "</$t>";
524 } else {
525 if ( $t == 'table' ) {
526 array_push( $tablestack, $tagstack );
527 $tagstack = array();
528 }
529 array_push( $tagstack, $t );
530 }
531
532 # Replace any variables or template parameters with
533 # plaintext results.
534 if( is_callable( $processCallback ) ) {
535 call_user_func_array( $processCallback, array( &$params, $args ) );
536 }
537
538 if ( !Sanitizer::validateTag( $params, $t ) ) {
539 $badtag = true;
540 }
541
542 # Strip non-approved attributes from the tag
543 $newparams = Sanitizer::fixTagAttributes( $params, $t );
544 }
545 if ( !$badtag ) {
546 $rest = str_replace( '>', '&gt;', $rest );
547 $close = ( $brace == '/>' && !$slash ) ? ' /' : '';
548 $text .= "<$slash$t$newparams$close>$rest";
549 continue;
550 }
551 }
552 $text .= '&lt;' . str_replace( '>', '&gt;', $x);
553 }
554 # Close off any remaining tags
555 while ( is_array( $tagstack ) && ($t = array_pop( $tagstack )) ) {
556 $text .= "</$t>\n";
557 if ( $t == 'table' ) { $tagstack = array_pop( $tablestack ); }
558 }
559 } else {
560 # this might be possible using tidy itself
561 foreach ( $bits as $x ) {
562 preg_match( '/^(\\/?)(\\w+)([^>]*?)(\\/{0,1}>)([^<]*)$/',
563 $x, $regs );
564 @list( /* $qbar */, $slash, $t, $params, $brace, $rest ) = $regs;
565 if ( isset( $htmlelements[$t = strtolower( $t )] ) ) {
566 if( is_callable( $processCallback ) ) {
567 call_user_func_array( $processCallback, array( &$params, $args ) );
568 }
569 $newparams = Sanitizer::fixTagAttributes( $params, $t );
570 $rest = str_replace( '>', '&gt;', $rest );
571 $text .= "<$slash$t$newparams$brace$rest";
572 } else {
573 $text .= '&lt;' . str_replace( '>', '&gt;', $x);
574 }
575 }
576 }
577 wfProfileOut( __METHOD__ );
578 return $text;
579 }
580
581 /**
582 * Remove '<!--', '-->', and everything between.
583 * To avoid leaving blank lines, when a comment is both preceded
584 * and followed by a newline (ignoring spaces), trim leading and
585 * trailing spaces and one of the newlines.
586 *
587 * @private
588 * @param $text String
589 * @return string
590 */
591 static function removeHTMLcomments( $text ) {
592 wfProfileIn( __METHOD__ );
593 while (($start = strpos($text, '<!--')) !== false) {
594 $end = strpos($text, '-->', $start + 4);
595 if ($end === false) {
596 # Unterminated comment; bail out
597 break;
598 }
599
600 $end += 3;
601
602 # Trim space and newline if the comment is both
603 # preceded and followed by a newline
604 $spaceStart = max($start - 1, 0);
605 $spaceLen = $end - $spaceStart;
606 while (substr($text, $spaceStart, 1) === ' ' && $spaceStart > 0) {
607 $spaceStart--;
608 $spaceLen++;
609 }
610 while (substr($text, $spaceStart + $spaceLen, 1) === ' ')
611 $spaceLen++;
612 if (substr($text, $spaceStart, 1) === "\n" and substr($text, $spaceStart + $spaceLen, 1) === "\n") {
613 # Remove the comment, leading and trailing
614 # spaces, and leave only one newline.
615 $text = substr_replace($text, "\n", $spaceStart, $spaceLen + 1);
616 }
617 else {
618 # Remove just the comment.
619 $text = substr_replace($text, '', $start, $end - $start);
620 }
621 }
622 wfProfileOut( __METHOD__ );
623 return $text;
624 }
625
626 /**
627 * Take an array of attribute names and values and fix some deprecated values
628 * for the given element type.
629 * This does not validate properties, so you should ensure that you call
630 * validateTagAttributes AFTER this to ensure that the resulting style rule
631 * this may add is safe.
632 *
633 * - Converts most presentational attributes like align into inline css
634 *
635 * @param $attribs Array
636 * @param $element String
637 * @return Array
638 */
639 static function fixDeprecatedAttributes( $attribs, $element ) {
640 global $wgHtml5, $wgCleanupPresentationalAttributes;
641
642 // presentational attributes were removed from html5, we can leave them
643 // in when html5 is turned off
644 if ( !$wgHtml5 || !$wgCleanupPresentationalAttributes ) {
645 return $attribs;
646 }
647
648 $table = array( 'table' );
649 $cells = array( 'td', 'th' );
650 $colls = array( 'col', 'colgroup' );
651 $tblocks = array( 'tbody', 'tfoot', 'thead' );
652 $h = array( 'h1', 'h2', 'h3', 'h4', 'h5', 'h6' );
653
654 $presentationalAttribs = array(
655 'align' => array( 'text-align', array_merge( array( 'caption', 'hr', 'div', 'p', 'tr' ), $table, $cells, $colls, $tblocks, $h ) ),
656 'clear' => array( 'clear', array( 'br' ) ),
657 'height' => array( 'height', $cells ),
658 'nowrap' => array( 'white-space', $cells ),
659 'size' => array( 'height', array( 'hr' ) ),
660 'type' => array( 'list-style-type', array( 'li', 'ol', 'ul' ) ),
661 'valign' => array( 'vertical-align', array_merge( $cells, $colls, $tblocks ) ),
662 'width' => array( 'width', array_merge( array( 'hr', 'pre' ), $table, $cells, $colls ) ),
663 );
664
665 // Ensure that any upper case or mixed case attributes are converted to lowercase
666 foreach ( $attribs as $attribute => $value ) {
667 if ( $attribute !== strtolower( $attribute ) && array_key_exists( strtolower( $attribute ), $presentationalAttribs ) ) {
668 $attribs[strtolower( $attribute )] = $value;
669 unset( $attribs[$attribute] );
670 }
671 }
672
673 $style = "";
674 foreach ( $presentationalAttribs as $attribute => $info ) {
675 list( $property, $elements ) = $info;
676
677 // Skip if this attribute is not relevant to this element
678 if ( !in_array( $element, $elements ) ) {
679 continue;
680 }
681
682 // Skip if the attribute is not used
683 if ( !array_key_exists( $attribute, $attribs ) ) {
684 continue;
685 }
686
687 $value = $attribs[$attribute];
688
689 // For nowrap the value should be nowrap instead of whatever text is in the value
690 if ( $attribute === 'nowrap' ) {
691 $value = 'nowrap';
692 }
693
694 // clear="all" is clear: both; in css
695 if ( $attribute === 'clear' && strtolower( $value ) === 'all' ) {
696 $value = 'both';
697 }
698
699 // Size based properties should have px applied to them if they have no unit
700 if ( in_array( $attribute, array( 'height', 'width', 'size' ) ) ) {
701 if ( preg_match( '/^[\d.]+$/', $value ) ) {
702 $value = "{$value}px";
703 }
704 }
705
706 $style .= " $property: $value;";
707
708 unset( $attribs[$attribute] );
709 }
710
711 if ( $style ) {
712 // Prepend our style rules so that they can be overridden by user css
713 if ( isset($attribs['style']) ) {
714 $style .= " " . $attribs['style'];
715 }
716 $attribs['style'] = trim($style);
717 }
718
719 return $attribs;
720 }
721
722 /**
723 * Takes attribute names and values for a tag and the tah name and
724 * validates that the tag is allowed to be present.
725 * This DOES NOT validate the attributes, nor does it validate the
726 * tags themselves. This method only handles the special circumstances
727 * where we may want to allow a tag within content but ONLY when it has
728 * specific attributes set.
729 *
730 * @param $
731 */
732 static function validateTag( $params, $element ) {
733 $params = Sanitizer::decodeTagAttributes( $params );
734
735 if ( $element == 'meta' || $element == 'link' ) {
736 if ( !isset( $params['itemprop'] ) ) {
737 // <meta> and <link> must have an itemprop="" otherwise they are not valid or safe in content
738 return false;
739 }
740 if ( $element == 'meta' && !isset( $params['content'] ) ) {
741 // <meta> must have a content="" for the itemprop
742 return false;
743 }
744 if ( $element == 'link' && !isset( $params['href'] ) ) {
745 // <link> must have an associated href=""
746 return false;
747 }
748 }
749
750 return true;
751 }
752
753 /**
754 * Take an array of attribute names and values and normalize or discard
755 * illegal values for the given element type.
756 *
757 * - Discards attributes not on a whitelist for the given element
758 * - Unsafe style attributes are discarded
759 * - Invalid id attributes are reencoded
760 *
761 * @param $attribs Array
762 * @param $element String
763 * @return Array
764 *
765 * @todo Check for legal values where the DTD limits things.
766 * @todo Check for unique id attribute :P
767 */
768 static function validateTagAttributes( $attribs, $element ) {
769 return Sanitizer::validateAttributes( $attribs,
770 Sanitizer::attributeWhitelist( $element ) );
771 }
772
773 /**
774 * Take an array of attribute names and values and normalize or discard
775 * illegal values for the given whitelist.
776 *
777 * - Discards attributes not the given whitelist
778 * - Unsafe style attributes are discarded
779 * - Invalid id attributes are reencoded
780 *
781 * @param $attribs Array
782 * @param $whitelist Array: list of allowed attribute names
783 * @return Array
784 *
785 * @todo Check for legal values where the DTD limits things.
786 * @todo Check for unique id attribute :P
787 */
788 static function validateAttributes( $attribs, $whitelist ) {
789 global $wgAllowRdfaAttributes, $wgAllowMicrodataAttributes, $wgHtml5;
790
791 $whitelist = array_flip( $whitelist );
792 $hrefExp = '/^(' . wfUrlProtocols() . ')[^\s]+$/';
793
794 $out = array();
795 foreach( $attribs as $attribute => $value ) {
796 #allow XML namespace declaration if RDFa is enabled
797 if ( $wgAllowRdfaAttributes && preg_match( self::XMLNS_ATTRIBUTE_PATTERN, $attribute ) ) {
798 if ( !preg_match( self::EVIL_URI_PATTERN, $value ) ) {
799 $out[$attribute] = $value;
800 }
801
802 continue;
803 }
804
805 # Allow any attribute beginning with "data-", if in HTML5 mode
806 if ( !($wgHtml5 && preg_match( '/^data-/i', $attribute )) && !isset( $whitelist[$attribute] ) ) {
807 continue;
808 }
809
810 # Strip javascript "expression" from stylesheets.
811 # http://msdn.microsoft.com/workshop/author/dhtml/overview/recalc.asp
812 if( $attribute == 'style' ) {
813 $value = Sanitizer::checkCss( $value );
814 }
815
816 if ( $attribute === 'id' ) {
817 $value = Sanitizer::escapeId( $value, 'noninitial' );
818 }
819
820 //RDFa and microdata properties allow URLs, URIs and/or CURIs. check them for sanity
821 if ( $attribute === 'rel' || $attribute === 'rev' ||
822 $attribute === 'about' || $attribute === 'property' || $attribute === 'resource' || #RDFa
823 $attribute === 'datatype' || $attribute === 'typeof' || #RDFa
824 $attribute === 'itemid' || $attribute === 'itemprop' || $attribute === 'itemref' || #HTML5 microdata
825 $attribute === 'itemscope' || $attribute === 'itemtype' ) { #HTML5 microdata
826
827 //Paranoia. Allow "simple" values but suppress javascript
828 if ( preg_match( self::EVIL_URI_PATTERN, $value ) ) {
829 continue;
830 }
831 }
832
833 # NOTE: even though elements using href/src are not allowed directly, supply
834 # validation code that can be used by tag hook handlers, etc
835 if ( $attribute === 'href' || $attribute === 'src' ) {
836 if ( !preg_match( $hrefExp, $value ) ) {
837 continue; //drop any href or src attributes not using an allowed protocol.
838 //NOTE: this also drops all relative URLs
839 }
840 }
841
842 // If this attribute was previously set, override it.
843 // Output should only have one attribute of each name.
844 $out[$attribute] = $value;
845 }
846
847 if ( $wgAllowMicrodataAttributes ) {
848 # itemtype, itemid, itemref don't make sense without itemscope
849 if ( !array_key_exists( 'itemscope', $out ) ) {
850 unset( $out['itemtype'] );
851 unset( $out['itemid'] );
852 unset( $out['itemref'] );
853 }
854 # TODO: Strip itemprop if we aren't descendants of an itemscope or pointed to by an itemref.
855 }
856 return $out;
857 }
858
859 /**
860 * Merge two sets of HTML attributes. Conflicting items in the second set
861 * will override those in the first, except for 'class' attributes which
862 * will be combined (if they're both strings).
863 *
864 * @todo implement merging for other attributes such as style
865 * @param $a Array
866 * @param $b Array
867 * @return array
868 */
869 static function mergeAttributes( $a, $b ) {
870 $out = array_merge( $a, $b );
871 if( isset( $a['class'] ) && isset( $b['class'] )
872 && is_string( $a['class'] ) && is_string( $b['class'] )
873 && $a['class'] !== $b['class'] ) {
874 $classes = preg_split( '/\s+/', "{$a['class']} {$b['class']}",
875 -1, PREG_SPLIT_NO_EMPTY );
876 $out['class'] = implode( ' ', array_unique( $classes ) );
877 }
878 return $out;
879 }
880
881 /**
882 * Pick apart some CSS and check it for forbidden or unsafe structures.
883 * Returns a sanitized string. This sanitized string will have
884 * character references and escape sequences decoded, and comments
885 * stripped. If the input is just too evil, only a comment complaining
886 * about evilness will be returned.
887 *
888 * Currently URL references, 'expression', 'tps' are forbidden.
889 *
890 * NOTE: Despite the fact that character references are decoded, the
891 * returned string may contain character references given certain
892 * clever input strings. These character references must
893 * be escaped before the return value is embedded in HTML.
894 *
895 * @param $value String
896 * @return String
897 */
898 static function checkCss( $value ) {
899 // Decode character references like &#123;
900 $value = Sanitizer::decodeCharReferences( $value );
901
902 // Decode escape sequences and line continuation
903 // See the grammar in the CSS 2 spec, appendix D.
904 // This has to be done AFTER decoding character references.
905 // This means it isn't possible for this function to return
906 // unsanitized escape sequences. It is possible to manufacture
907 // input that contains character references that decode to
908 // escape sequences that decode to character references, but
909 // it's OK for the return value to contain character references
910 // because the caller is supposed to escape those anyway.
911 static $decodeRegex;
912 if ( !$decodeRegex ) {
913 $space = '[\\x20\\t\\r\\n\\f]';
914 $nl = '(?:\\n|\\r\\n|\\r|\\f)';
915 $backslash = '\\\\';
916 $decodeRegex = "/ $backslash
917 (?:
918 ($nl) | # 1. Line continuation
919 ([0-9A-Fa-f]{1,6})$space? | # 2. character number
920 (.) | # 3. backslash cancelling special meaning
921 () | # 4. backslash at end of string
922 )/xu";
923 }
924 $value = preg_replace_callback( $decodeRegex,
925 array( __CLASS__, 'cssDecodeCallback' ), $value );
926
927 // Remove any comments; IE gets token splitting wrong
928 // This must be done AFTER decoding character references and
929 // escape sequences, because those steps can introduce comments
930 // This step cannot introduce character references or escape
931 // sequences, because it replaces comments with spaces rather
932 // than removing them completely.
933 $value = StringUtils::delimiterReplace( '/*', '*/', ' ', $value );
934
935 // Remove anything after a comment-start token, to guard against
936 // incorrect client implementations.
937 $commentPos = strpos( $value, '/*' );
938 if ( $commentPos !== false ) {
939 $value = substr( $value, 0, $commentPos );
940 }
941
942 // Reject problematic keywords and control characters
943 if ( preg_match( '/[\000-\010\016-\037\177]/', $value ) ) {
944 return '/* invalid control char */';
945 } elseif ( preg_match( '! expression | filter\s*: | accelerator\s*: | url\s*\( !ix', $value ) ) {
946 return '/* insecure input */';
947 }
948 return $value;
949 }
950
951 /**
952 * @param $matches array
953 * @return String
954 */
955 static function cssDecodeCallback( $matches ) {
956 if ( $matches[1] !== '' ) {
957 // Line continuation
958 return '';
959 } elseif ( $matches[2] !== '' ) {
960 $char = codepointToUtf8( hexdec( $matches[2] ) );
961 } elseif ( $matches[3] !== '' ) {
962 $char = $matches[3];
963 } else {
964 $char = '\\';
965 }
966 if ( $char == "\n" || $char == '"' || $char == "'" || $char == '\\' ) {
967 // These characters need to be escaped in strings
968 // Clean up the escape sequence to avoid parsing errors by clients
969 return '\\' . dechex( ord( $char ) ) . ' ';
970 } else {
971 // Decode unnecessary escape
972 return $char;
973 }
974 }
975
976 /**
977 * Take a tag soup fragment listing an HTML element's attributes
978 * and normalize it to well-formed XML, discarding unwanted attributes.
979 * Output is safe for further wikitext processing, with escaping of
980 * values that could trigger problems.
981 *
982 * - Normalizes attribute names to lowercase
983 * - Discards attributes not on a whitelist for the given element
984 * - Turns broken or invalid entities into plaintext
985 * - Double-quotes all attribute values
986 * - Attributes without values are given the name as attribute
987 * - Double attributes are discarded
988 * - Unsafe style attributes are discarded
989 * - Prepends space if there are attributes.
990 *
991 * @param $text String
992 * @param $element String
993 * @return String
994 */
995 static function fixTagAttributes( $text, $element ) {
996 if( trim( $text ) == '' ) {
997 return '';
998 }
999
1000 $decoded = Sanitizer::decodeTagAttributes( $text );
1001 $decoded = Sanitizer::fixDeprecatedAttributes( $decoded, $element );
1002 $stripped = Sanitizer::validateTagAttributes( $decoded, $element );
1003
1004 $attribs = array();
1005 foreach( $stripped as $attribute => $value ) {
1006 $encAttribute = htmlspecialchars( $attribute );
1007 $encValue = Sanitizer::safeEncodeAttribute( $value );
1008
1009 $attribs[] = "$encAttribute=\"$encValue\"";
1010 }
1011 return count( $attribs ) ? ' ' . implode( ' ', $attribs ) : '';
1012 }
1013
1014 /**
1015 * Encode an attribute value for HTML output.
1016 * @param $text String
1017 * @return HTML-encoded text fragment
1018 */
1019 static function encodeAttribute( $text ) {
1020 $encValue = htmlspecialchars( $text, ENT_QUOTES );
1021
1022 // Whitespace is normalized during attribute decoding,
1023 // so if we've been passed non-spaces we must encode them
1024 // ahead of time or they won't be preserved.
1025 $encValue = strtr( $encValue, array(
1026 "\n" => '&#10;',
1027 "\r" => '&#13;',
1028 "\t" => '&#9;',
1029 ) );
1030
1031 return $encValue;
1032 }
1033
1034 /**
1035 * Encode an attribute value for HTML tags, with extra armoring
1036 * against further wiki processing.
1037 * @param $text String
1038 * @return HTML-encoded text fragment
1039 */
1040 static function safeEncodeAttribute( $text ) {
1041 $encValue = Sanitizer::encodeAttribute( $text );
1042
1043 # Templates and links may be expanded in later parsing,
1044 # creating invalid or dangerous output. Suppress this.
1045 $encValue = strtr( $encValue, array(
1046 '<' => '&lt;', // This should never happen,
1047 '>' => '&gt;', // we've received invalid input
1048 '"' => '&quot;', // which should have been escaped.
1049 '{' => '&#123;',
1050 '[' => '&#91;',
1051 "''" => '&#39;&#39;',
1052 'ISBN' => '&#73;SBN',
1053 'RFC' => '&#82;FC',
1054 'PMID' => '&#80;MID',
1055 '|' => '&#124;',
1056 '__' => '&#95;_',
1057 ) );
1058
1059 # Stupid hack
1060 $encValue = preg_replace_callback(
1061 '/(' . wfUrlProtocols() . ')/',
1062 array( 'Sanitizer', 'armorLinksCallback' ),
1063 $encValue );
1064 return $encValue;
1065 }
1066
1067 /**
1068 * Given a value, escape it so that it can be used in an id attribute and
1069 * return it. This will use HTML5 validation if $wgExperimentalHtmlIds is
1070 * true, allowing anything but ASCII whitespace. Otherwise it will use
1071 * HTML 4 rules, which means a narrow subset of ASCII, with bad characters
1072 * escaped with lots of dots.
1073 *
1074 * To ensure we don't have to bother escaping anything, we also strip ', ",
1075 * & even if $wgExperimentalIds is true. TODO: Is this the best tactic?
1076 * We also strip # because it upsets IE, and % because it could be
1077 * ambiguous if it's part of something that looks like a percent escape
1078 * (which don't work reliably in fragments cross-browser).
1079 *
1080 * @see http://www.w3.org/TR/html401/types.html#type-name Valid characters
1081 * in the id and
1082 * name attributes
1083 * @see http://www.w3.org/TR/html401/struct/links.html#h-12.2.3 Anchors with the id attribute
1084 * @see http://www.whatwg.org/specs/web-apps/current-work/multipage/elements.html#the-id-attribute
1085 * HTML5 definition of id attribute
1086 *
1087 * @param $id String: id to escape
1088 * @param $options Mixed: string or array of strings (default is array()):
1089 * 'noninitial': This is a non-initial fragment of an id, not a full id,
1090 * so don't pay attention if the first character isn't valid at the
1091 * beginning of an id. Only matters if $wgExperimentalHtmlIds is
1092 * false.
1093 * 'legacy': Behave the way the old HTML 4-based ID escaping worked even
1094 * if $wgExperimentalHtmlIds is used, so we can generate extra
1095 * anchors and links won't break.
1096 * @return String
1097 */
1098 static function escapeId( $id, $options = array() ) {
1099 global $wgHtml5, $wgExperimentalHtmlIds;
1100 $options = (array)$options;
1101
1102 if ( $wgHtml5 && $wgExperimentalHtmlIds && !in_array( 'legacy', $options ) ) {
1103 $id = Sanitizer::decodeCharReferences( $id );
1104 $id = preg_replace( '/[ \t\n\r\f_\'"&#%]+/', '_', $id );
1105 $id = trim( $id, '_' );
1106 if ( $id === '' ) {
1107 # Must have been all whitespace to start with.
1108 return '_';
1109 } else {
1110 return $id;
1111 }
1112 }
1113
1114 # HTML4-style escaping
1115 static $replace = array(
1116 '%3A' => ':',
1117 '%' => '.'
1118 );
1119
1120 $id = urlencode( Sanitizer::decodeCharReferences( strtr( $id, ' ', '_' ) ) );
1121 $id = str_replace( array_keys( $replace ), array_values( $replace ), $id );
1122
1123 if ( !preg_match( '/^[a-zA-Z]/', $id )
1124 && !in_array( 'noninitial', $options ) ) {
1125 // Initial character must be a letter!
1126 $id = "x$id";
1127 }
1128 return $id;
1129 }
1130
1131 /**
1132 * Given a value, escape it so that it can be used as a CSS class and
1133 * return it.
1134 *
1135 * @todo For extra validity, input should be validated UTF-8.
1136 *
1137 * @see http://www.w3.org/TR/CSS21/syndata.html Valid characters/format
1138 *
1139 * @param $class String
1140 * @return String
1141 */
1142 static function escapeClass( $class ) {
1143 // Convert ugly stuff to underscores and kill underscores in ugly places
1144 return rtrim(preg_replace(
1145 array('/(^[0-9\\-])|[\\x00-\\x20!"#$%&\'()*+,.\\/:;<=>?@[\\]^`{|}~]|\\xC2\\xA0/','/_+/'),
1146 '_',
1147 $class ), '_');
1148 }
1149
1150 /**
1151 * Given HTML input, escape with htmlspecialchars but un-escape entites.
1152 * This allows (generally harmless) entities like &#160; to survive.
1153 *
1154 * @param $html String to escape
1155 * @return String: escaped input
1156 */
1157 static function escapeHtmlAllowEntities( $html ) {
1158 $html = Sanitizer::decodeCharReferences( $html );
1159 # It seems wise to escape ' as well as ", as a matter of course. Can't
1160 # hurt.
1161 $html = htmlspecialchars( $html, ENT_QUOTES );
1162 return $html;
1163 }
1164
1165 /**
1166 * Regex replace callback for armoring links against further processing.
1167 * @param $matches Array
1168 * @return string
1169 */
1170 private static function armorLinksCallback( $matches ) {
1171 return str_replace( ':', '&#58;', $matches[1] );
1172 }
1173
1174 /**
1175 * Return an associative array of attribute names and values from
1176 * a partial tag string. Attribute names are forces to lowercase,
1177 * character references are decoded to UTF-8 text.
1178 *
1179 * @param $text String
1180 * @return Array
1181 */
1182 public static function decodeTagAttributes( $text ) {
1183 if( trim( $text ) == '' ) {
1184 return array();
1185 }
1186
1187 $attribs = array();
1188 $pairs = array();
1189 if( !preg_match_all(
1190 self::getAttribsRegex(),
1191 $text,
1192 $pairs,
1193 PREG_SET_ORDER ) ) {
1194 return $attribs;
1195 }
1196
1197 foreach( $pairs as $set ) {
1198 $attribute = strtolower( $set[1] );
1199 $value = Sanitizer::getTagAttributeCallback( $set );
1200
1201 // Normalize whitespace
1202 $value = preg_replace( '/[\t\r\n ]+/', ' ', $value );
1203 $value = trim( $value );
1204
1205 // Decode character references
1206 $attribs[$attribute] = Sanitizer::decodeCharReferences( $value );
1207 }
1208 return $attribs;
1209 }
1210
1211 /**
1212 * Pick the appropriate attribute value from a match set from the
1213 * attribs regex matches.
1214 *
1215 * @param $set Array
1216 * @return String
1217 */
1218 private static function getTagAttributeCallback( $set ) {
1219 if( isset( $set[6] ) ) {
1220 # Illegal #XXXXXX color with no quotes.
1221 return $set[6];
1222 } elseif( isset( $set[5] ) ) {
1223 # No quotes.
1224 return $set[5];
1225 } elseif( isset( $set[4] ) ) {
1226 # Single-quoted
1227 return $set[4];
1228 } elseif( isset( $set[3] ) ) {
1229 # Double-quoted
1230 return $set[3];
1231 } elseif( !isset( $set[2] ) ) {
1232 # In XHTML, attributes must have a value.
1233 # For 'reduced' form, return explicitly the attribute name here.
1234 return $set[1];
1235 } else {
1236 throw new MWException( "Tag conditions not met. This should never happen and is a bug." );
1237 }
1238 }
1239
1240 /**
1241 * Normalize whitespace and character references in an XML source-
1242 * encoded text for an attribute value.
1243 *
1244 * See http://www.w3.org/TR/REC-xml/#AVNormalize for background,
1245 * but note that we're not returning the value, but are returning
1246 * XML source fragments that will be slapped into output.
1247 *
1248 * @param $text String
1249 * @return String
1250 */
1251 private static function normalizeAttributeValue( $text ) {
1252 return str_replace( '"', '&quot;',
1253 self::normalizeWhitespace(
1254 Sanitizer::normalizeCharReferences( $text ) ) );
1255 }
1256
1257 /**
1258 * @param $text string
1259 * @return mixed
1260 */
1261 private static function normalizeWhitespace( $text ) {
1262 return preg_replace(
1263 '/\r\n|[\x20\x0d\x0a\x09]/',
1264 ' ',
1265 $text );
1266 }
1267
1268 /**
1269 * Normalizes whitespace in a section name, such as might be returned
1270 * by Parser::stripSectionName(), for use in the id's that are used for
1271 * section links.
1272 *
1273 * @param $section String
1274 * @return String
1275 */
1276 static function normalizeSectionNameWhitespace( $section ) {
1277 return trim( preg_replace( '/[ _]+/', ' ', $section ) );
1278 }
1279
1280 /**
1281 * Ensure that any entities and character references are legal
1282 * for XML and XHTML specifically. Any stray bits will be
1283 * &amp;-escaped to result in a valid text fragment.
1284 *
1285 * a. named char refs can only be &lt; &gt; &amp; &quot;, others are
1286 * numericized (this way we're well-formed even without a DTD)
1287 * b. any numeric char refs must be legal chars, not invalid or forbidden
1288 * c. use &#x, not &#X
1289 * d. fix or reject non-valid attributes
1290 *
1291 * @param $text String
1292 * @return String
1293 * @private
1294 */
1295 static function normalizeCharReferences( $text ) {
1296 return preg_replace_callback(
1297 self::CHAR_REFS_REGEX,
1298 array( 'Sanitizer', 'normalizeCharReferencesCallback' ),
1299 $text );
1300 }
1301 /**
1302 * @param $matches String
1303 * @return String
1304 */
1305 static function normalizeCharReferencesCallback( $matches ) {
1306 $ret = null;
1307 if( $matches[1] != '' ) {
1308 $ret = Sanitizer::normalizeEntity( $matches[1] );
1309 } elseif( $matches[2] != '' ) {
1310 $ret = Sanitizer::decCharReference( $matches[2] );
1311 } elseif( $matches[3] != '' ) {
1312 $ret = Sanitizer::hexCharReference( $matches[3] );
1313 }
1314 if( is_null( $ret ) ) {
1315 return htmlspecialchars( $matches[0] );
1316 } else {
1317 return $ret;
1318 }
1319 }
1320
1321 /**
1322 * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
1323 * return the equivalent numeric entity reference (except for the core &lt;
1324 * &gt; &amp; &quot;). If the entity is a MediaWiki-specific alias, returns
1325 * the HTML equivalent. Otherwise, returns HTML-escaped text of
1326 * pseudo-entity source (eg &amp;foo;)
1327 *
1328 * @param $name String
1329 * @return String
1330 */
1331 static function normalizeEntity( $name ) {
1332 if ( isset( self::$htmlEntityAliases[$name] ) ) {
1333 return '&' . self::$htmlEntityAliases[$name] . ';';
1334 } elseif ( in_array( $name,
1335 array( 'lt', 'gt', 'amp', 'quot' ) ) ) {
1336 return "&$name;";
1337 } elseif ( isset( self::$htmlEntities[$name] ) ) {
1338 return '&#' . self::$htmlEntities[$name] . ';';
1339 } else {
1340 return "&amp;$name;";
1341 }
1342 }
1343
1344 /**
1345 * @param $codepoint
1346 * @return null|string
1347 */
1348 static function decCharReference( $codepoint ) {
1349 $point = intval( $codepoint );
1350 if( Sanitizer::validateCodepoint( $point ) ) {
1351 return sprintf( '&#%d;', $point );
1352 } else {
1353 return null;
1354 }
1355 }
1356
1357 /**
1358 * @param $codepoint
1359 * @return null|string
1360 */
1361 static function hexCharReference( $codepoint ) {
1362 $point = hexdec( $codepoint );
1363 if( Sanitizer::validateCodepoint( $point ) ) {
1364 return sprintf( '&#x%x;', $point );
1365 } else {
1366 return null;
1367 }
1368 }
1369
1370 /**
1371 * Returns true if a given Unicode codepoint is a valid character in XML.
1372 * @param $codepoint Integer
1373 * @return Boolean
1374 */
1375 private static function validateCodepoint( $codepoint ) {
1376 return ($codepoint == 0x09)
1377 || ($codepoint == 0x0a)
1378 || ($codepoint == 0x0d)
1379 || ($codepoint >= 0x20 && $codepoint <= 0xd7ff)
1380 || ($codepoint >= 0xe000 && $codepoint <= 0xfffd)
1381 || ($codepoint >= 0x10000 && $codepoint <= 0x10ffff);
1382 }
1383
1384 /**
1385 * Decode any character references, numeric or named entities,
1386 * in the text and return a UTF-8 string.
1387 *
1388 * @param $text String
1389 * @return String
1390 */
1391 public static function decodeCharReferences( $text ) {
1392 return preg_replace_callback(
1393 self::CHAR_REFS_REGEX,
1394 array( 'Sanitizer', 'decodeCharReferencesCallback' ),
1395 $text );
1396 }
1397
1398 /**
1399 * Decode any character references, numeric or named entities,
1400 * in the next and normalize the resulting string. (bug 14952)
1401 *
1402 * This is useful for page titles, not for text to be displayed,
1403 * MediaWiki allows HTML entities to escape normalization as a feature.
1404 *
1405 * @param $text String (already normalized, containing entities)
1406 * @return String (still normalized, without entities)
1407 */
1408 public static function decodeCharReferencesAndNormalize( $text ) {
1409 global $wgContLang;
1410 $text = preg_replace_callback(
1411 self::CHAR_REFS_REGEX,
1412 array( 'Sanitizer', 'decodeCharReferencesCallback' ),
1413 $text, /* limit */ -1, $count );
1414
1415 if ( $count ) {
1416 return $wgContLang->normalize( $text );
1417 } else {
1418 return $text;
1419 }
1420 }
1421
1422 /**
1423 * @param $matches String
1424 * @return String
1425 */
1426 static function decodeCharReferencesCallback( $matches ) {
1427 if( $matches[1] != '' ) {
1428 return Sanitizer::decodeEntity( $matches[1] );
1429 } elseif( $matches[2] != '' ) {
1430 return Sanitizer::decodeChar( intval( $matches[2] ) );
1431 } elseif( $matches[3] != '' ) {
1432 return Sanitizer::decodeChar( hexdec( $matches[3] ) );
1433 }
1434 # Last case should be an ampersand by itself
1435 return $matches[0];
1436 }
1437
1438 /**
1439 * Return UTF-8 string for a codepoint if that is a valid
1440 * character reference, otherwise U+FFFD REPLACEMENT CHARACTER.
1441 * @param $codepoint Integer
1442 * @return String
1443 * @private
1444 */
1445 static function decodeChar( $codepoint ) {
1446 if( Sanitizer::validateCodepoint( $codepoint ) ) {
1447 return codepointToUtf8( $codepoint );
1448 } else {
1449 return UTF8_REPLACEMENT;
1450 }
1451 }
1452
1453 /**
1454 * If the named entity is defined in the HTML 4.0/XHTML 1.0 DTD,
1455 * return the UTF-8 encoding of that character. Otherwise, returns
1456 * pseudo-entity source (eg &foo;)
1457 *
1458 * @param $name String
1459 * @return String
1460 */
1461 static function decodeEntity( $name ) {
1462 if ( isset( self::$htmlEntityAliases[$name] ) ) {
1463 $name = self::$htmlEntityAliases[$name];
1464 }
1465 if( isset( self::$htmlEntities[$name] ) ) {
1466 return codepointToUtf8( self::$htmlEntities[$name] );
1467 } else {
1468 return "&$name;";
1469 }
1470 }
1471
1472 /**
1473 * Fetch the whitelist of acceptable attributes for a given element name.
1474 *
1475 * @param $element String
1476 * @return Array
1477 */
1478 static function attributeWhitelist( $element ) {
1479 static $list;
1480 if( !isset( $list ) ) {
1481 $list = Sanitizer::setupAttributeWhitelist();
1482 }
1483 return isset( $list[$element] )
1484 ? $list[$element]
1485 : array();
1486 }
1487
1488 /**
1489 * Foreach array key (an allowed HTML element), return an array
1490 * of allowed attributes
1491 * @return Array
1492 */
1493 static function setupAttributeWhitelist() {
1494 global $wgAllowRdfaAttributes, $wgHtml5, $wgAllowMicrodataAttributes;
1495
1496 $common = array( 'id', 'class', 'lang', 'dir', 'title', 'style' );
1497
1498 if ( $wgAllowRdfaAttributes ) {
1499 #RDFa attributes as specified in section 9 of http://www.w3.org/TR/2008/REC-rdfa-syntax-20081014
1500 $common = array_merge( $common, array(
1501 'about', 'property', 'resource', 'datatype', 'typeof',
1502 ) );
1503 }
1504
1505 if ( $wgHtml5 && $wgAllowMicrodataAttributes ) {
1506 # add HTML5 microdata tages as pecified by http://www.whatwg.org/specs/web-apps/current-work/multipage/microdata.html#the-microdata-model
1507 $common = array_merge( $common, array(
1508 'itemid', 'itemprop', 'itemref', 'itemscope', 'itemtype'
1509 ) );
1510 }
1511
1512 $block = array_merge( $common, array( 'align' ) );
1513 $tablealign = array( 'align', 'char', 'charoff', 'valign' );
1514 $tablecell = array( 'abbr',
1515 'axis',
1516 'headers',
1517 'scope',
1518 'rowspan',
1519 'colspan',
1520 'nowrap', # deprecated
1521 'width', # deprecated
1522 'height', # deprecated
1523 'bgcolor' # deprecated
1524 );
1525
1526 # Numbers refer to sections in HTML 4.01 standard describing the element.
1527 # See: http://www.w3.org/TR/html4/
1528 $whitelist = array(
1529 # 7.5.4
1530 'div' => $block,
1531 'center' => $common, # deprecated
1532 'span' => $block, # ??
1533
1534 # 7.5.5
1535 'h1' => $block,
1536 'h2' => $block,
1537 'h3' => $block,
1538 'h4' => $block,
1539 'h5' => $block,
1540 'h6' => $block,
1541
1542 # 7.5.6
1543 # address
1544
1545 # 8.2.4
1546 # bdo
1547
1548 # 9.2.1
1549 'em' => $common,
1550 'strong' => $common,
1551 'cite' => $common,
1552 'dfn' => $common,
1553 'code' => $common,
1554 'samp' => $common,
1555 'kbd' => $common,
1556 'var' => $common,
1557 'abbr' => $common,
1558 # acronym
1559
1560 # 9.2.2
1561 'blockquote' => array_merge( $common, array( 'cite' ) ),
1562 # q
1563
1564 # 9.2.3
1565 'sub' => $common,
1566 'sup' => $common,
1567
1568 # 9.3.1
1569 'p' => $block,
1570
1571 # 9.3.2
1572 'br' => array( 'id', 'class', 'title', 'style', 'clear' ),
1573
1574 # 9.3.4
1575 'pre' => array_merge( $common, array( 'width' ) ),
1576
1577 # 9.4
1578 'ins' => array_merge( $common, array( 'cite', 'datetime' ) ),
1579 'del' => array_merge( $common, array( 'cite', 'datetime' ) ),
1580
1581 # 10.2
1582 'ul' => array_merge( $common, array( 'type' ) ),
1583 'ol' => array_merge( $common, array( 'type', 'start' ) ),
1584 'li' => array_merge( $common, array( 'type', 'value' ) ),
1585
1586 # 10.3
1587 'dl' => $common,
1588 'dd' => $common,
1589 'dt' => $common,
1590
1591 # 11.2.1
1592 'table' => array_merge( $common,
1593 array( 'summary', 'width', 'border', 'frame',
1594 'rules', 'cellspacing', 'cellpadding',
1595 'align', 'bgcolor',
1596 ) ),
1597
1598 # 11.2.2
1599 'caption' => array_merge( $common, array( 'align' ) ),
1600
1601 # 11.2.3
1602 'thead' => array_merge( $common, $tablealign ),
1603 'tfoot' => array_merge( $common, $tablealign ),
1604 'tbody' => array_merge( $common, $tablealign ),
1605
1606 # 11.2.4
1607 'colgroup' => array_merge( $common, array( 'span', 'width' ), $tablealign ),
1608 'col' => array_merge( $common, array( 'span', 'width' ), $tablealign ),
1609
1610 # 11.2.5
1611 'tr' => array_merge( $common, array( 'bgcolor' ), $tablealign ),
1612
1613 # 11.2.6
1614 'td' => array_merge( $common, $tablecell, $tablealign ),
1615 'th' => array_merge( $common, $tablecell, $tablealign ),
1616
1617 # 12.2 # NOTE: <a> is not allowed directly, but the attrib whitelist is used from the Parser object
1618 'a' => array_merge( $common, array( 'href', 'rel', 'rev' ) ), # rel/rev esp. for RDFa
1619
1620 # 13.2
1621 # Not usually allowed, but may be used for extension-style hooks
1622 # such as <math> when it is rasterized, or if $wgAllowImageTag is
1623 # true
1624 'img' => array_merge( $common, array( 'alt', 'src', 'width', 'height' ) ),
1625
1626 # 15.2.1
1627 'tt' => $common,
1628 'b' => $common,
1629 'i' => $common,
1630 'big' => $common,
1631 'small' => $common,
1632 'strike' => $common,
1633 's' => $common,
1634 'u' => $common,
1635
1636 # 15.2.2
1637 'font' => array_merge( $common, array( 'size', 'color', 'face' ) ),
1638 # basefont
1639
1640 # 15.3
1641 'hr' => array_merge( $common, array( 'noshade', 'size', 'width' ) ),
1642
1643 # XHTML Ruby annotation text module, simple ruby only.
1644 # http://www.w3c.org/TR/ruby/
1645 'ruby' => $common,
1646 # rbc
1647 # rtc
1648 'rb' => $common,
1649 'rt' => $common, #array_merge( $common, array( 'rbspan' ) ),
1650 'rp' => $common,
1651
1652 # MathML root element, where used for extensions
1653 # 'title' may not be 100% valid here; it's XHTML
1654 # http://www.w3.org/TR/REC-MathML/
1655 'math' => array( 'class', 'style', 'id', 'title' ),
1656 );
1657
1658 if ( $wgHtml5 ) {
1659 # HTML5 elements, defined by:
1660 # http://www.whatwg.org/specs/web-apps/current-work/multipage/
1661 $whitelist += array(
1662 'data' => array_merge( $common, array( 'value' ) ),
1663 'time' => array_merge( $common, array( 'datetime' ) ),
1664
1665 // meta and link are only present when Microdata is allowed anyways
1666 // so we don't bother adding another condition here
1667 // meta and link are only valid for use as Microdata so we do not
1668 // allow the common attributes here.
1669 'meta' => array( 'itemprop', 'content' ),
1670 'link' => array( 'itemprop', 'href' ),
1671 );
1672 }
1673
1674 return $whitelist;
1675 }
1676
1677 /**
1678 * Take a fragment of (potentially invalid) HTML and return
1679 * a version with any tags removed, encoded as plain text.
1680 *
1681 * Warning: this return value must be further escaped for literal
1682 * inclusion in HTML output as of 1.10!
1683 *
1684 * @param $text String: HTML fragment
1685 * @return String
1686 */
1687 static function stripAllTags( $text ) {
1688 # Actual <tags>
1689 $text = StringUtils::delimiterReplace( '<', '>', '', $text );
1690
1691 # Normalize &entities and whitespace
1692 $text = self::decodeCharReferences( $text );
1693 $text = self::normalizeWhitespace( $text );
1694
1695 return $text;
1696 }
1697
1698 /**
1699 * Hack up a private DOCTYPE with HTML's standard entity declarations.
1700 * PHP 4 seemed to know these if you gave it an HTML doctype, but
1701 * PHP 5.1 doesn't.
1702 *
1703 * Use for passing XHTML fragments to PHP's XML parsing functions
1704 *
1705 * @return String
1706 */
1707 static function hackDocType() {
1708 $out = "<!DOCTYPE html [\n";
1709 foreach( self::$htmlEntities as $entity => $codepoint ) {
1710 $out .= "<!ENTITY $entity \"&#$codepoint;\">";
1711 }
1712 $out .= "]>\n";
1713 return $out;
1714 }
1715
1716 /**
1717 * @param $url string
1718 * @return mixed|string
1719 */
1720 static function cleanUrl( $url ) {
1721 # Normalize any HTML entities in input. They will be
1722 # re-escaped by makeExternalLink().
1723 $url = Sanitizer::decodeCharReferences( $url );
1724
1725 # Escape any control characters introduced by the above step
1726 $url = preg_replace_callback( '/[\][<>"\\x00-\\x20\\x7F\|]/',
1727 array( __CLASS__, 'cleanUrlCallback' ), $url );
1728
1729 # Validate hostname portion
1730 $matches = array();
1731 if( preg_match( '!^([^:]+:)(//[^/]+)?(.*)$!iD', $url, $matches ) ) {
1732 list( /* $whole */, $protocol, $host, $rest ) = $matches;
1733
1734 // Characters that will be ignored in IDNs.
1735 // http://tools.ietf.org/html/3454#section-3.1
1736 // Strip them before further processing so blacklists and such work.
1737 $strip = "/
1738 \\s| # general whitespace
1739 \xc2\xad| # 00ad SOFT HYPHEN
1740 \xe1\xa0\x86| # 1806 MONGOLIAN TODO SOFT HYPHEN
1741 \xe2\x80\x8b| # 200b ZERO WIDTH SPACE
1742 \xe2\x81\xa0| # 2060 WORD JOINER
1743 \xef\xbb\xbf| # feff ZERO WIDTH NO-BREAK SPACE
1744 \xcd\x8f| # 034f COMBINING GRAPHEME JOINER
1745 \xe1\xa0\x8b| # 180b MONGOLIAN FREE VARIATION SELECTOR ONE
1746 \xe1\xa0\x8c| # 180c MONGOLIAN FREE VARIATION SELECTOR TWO
1747 \xe1\xa0\x8d| # 180d MONGOLIAN FREE VARIATION SELECTOR THREE
1748 \xe2\x80\x8c| # 200c ZERO WIDTH NON-JOINER
1749 \xe2\x80\x8d| # 200d ZERO WIDTH JOINER
1750 [\xef\xb8\x80-\xef\xb8\x8f] # fe00-fe0f VARIATION SELECTOR-1-16
1751 /xuD";
1752
1753 $host = preg_replace( $strip, '', $host );
1754
1755 // @todo FIXME: Validate hostnames here
1756
1757 return $protocol . $host . $rest;
1758 } else {
1759 return $url;
1760 }
1761 }
1762
1763 /**
1764 * @param $matches array
1765 * @return string
1766 */
1767 static function cleanUrlCallback( $matches ) {
1768 return urlencode( $matches[0] );
1769 }
1770
1771 /**
1772 * Does a string look like an e-mail address?
1773 *
1774 * This validates an email address using an HTML5 specification found at:
1775 * http://www.whatwg.org/specs/web-apps/current-work/multipage/states-of-the-type-attribute.html#valid-e-mail-address
1776 * Which as of 2011-01-24 says:
1777 *
1778 * A valid e-mail address is a string that matches the ABNF production
1779 * 1*( atext / "." ) "@" ldh-str *( "." ldh-str ) where atext is defined
1780 * in RFC 5322 section 3.2.3, and ldh-str is defined in RFC 1034 section
1781 * 3.5.
1782 *
1783 * This function is an implementation of the specification as requested in
1784 * bug 22449.
1785 *
1786 * Client-side forms will use the same standard validation rules via JS or
1787 * HTML 5 validation; additional restrictions can be enforced server-side
1788 * by extensions via the 'isValidEmailAddr' hook.
1789 *
1790 * Note that this validation doesn't 100% match RFC 2822, but is believed
1791 * to be liberal enough for wide use. Some invalid addresses will still
1792 * pass validation here.
1793 *
1794 * @since 1.18
1795 *
1796 * @param $addr String E-mail address
1797 * @return Bool
1798 */
1799 public static function validateEmail( $addr ) {
1800 $result = null;
1801 if( !wfRunHooks( 'isValidEmailAddr', array( $addr, &$result ) ) ) {
1802 return $result;
1803 }
1804
1805 // Please note strings below are enclosed in brackets [], this make the
1806 // hyphen "-" a range indicator. Hence it is double backslashed below.
1807 // See bug 26948
1808 $rfc5322_atext = "a-z0-9!#$%&'*+\\-\/=?^_`{|}~" ;
1809 $rfc1034_ldh_str = "a-z0-9\\-" ;
1810
1811 $HTML5_email_regexp = "/
1812 ^ # start of string
1813 [$rfc5322_atext\\.]+ # user part which is liberal :p
1814 @ # 'apostrophe'
1815 [$rfc1034_ldh_str]+ # First domain part
1816 (\\.[$rfc1034_ldh_str]+)* # Following part prefixed with a dot
1817 $ # End of string
1818 /ix" ; // case Insensitive, eXtended
1819
1820 return (bool) preg_match( $HTML5_email_regexp, $addr );
1821 }
1822 }