Index: trunk/extensions/CSS/verifyCSSLoad.js |
— | — | @@ -0,0 +1,362 @@ |
| 2 | +/** |
| 3 | + * This file handles fallback for link elements with data URIs generated by |
| 4 | + * this extension. Generally it will only affect IE6/7. However, we do not |
| 5 | + * sniff for browser name or version, so other non-compliant browsers should be |
| 6 | + * handled as well. |
| 7 | + * |
| 8 | + * @file |
| 9 | + * @ingroup Extensions |
| 10 | + * @author Rusty Burchfield |
| 11 | + * @copyright © 2011 Rusty Burchfield |
| 12 | + * @licence The MIT License |
| 13 | + */ |
| 14 | + |
| 15 | +(function( $ ) { |
| 16 | + $.fn.cssExtensionDataURIFallback = function( id, color ) { |
| 17 | + // Install the canary and set its inital color |
| 18 | + var canary = $( '<div>' ) |
| 19 | + .attr( 'id', id ) |
| 20 | + .css( 'display', 'none' ) |
| 21 | + .css( 'background-color', '#FFFFFF' ) |
| 22 | + .appendTo( 'body' ); |
| 23 | + |
| 24 | + var desiredColor = new RGBColor( color ); |
| 25 | + var actualColor = canary.css( 'background-color' ); |
| 26 | + actualColor = new RGBColor( actualColor ); |
| 27 | + canary.remove(); |
| 28 | + |
| 29 | + // Did the canary change color as expected? |
| 30 | + if ( desiredColor.toHex() == actualColor.toHex() ) { |
| 31 | + return; |
| 32 | + } |
| 33 | + |
| 34 | + // Fallback to decoding href and inserting style tag |
| 35 | + var code = this.attr( 'href' ).replace( /^.*;base64,/, '' ); |
| 36 | + var text = Base64.decode( code ); |
| 37 | + |
| 38 | + var style = $( '<style>' ).attr( 'type', 'text/css' ); |
| 39 | + if ( style.get( 0 ).styleSheet ) { |
| 40 | + // IE6/7 (our main fallback targets) only work this way. |
| 41 | + style.get( 0 ).styleSheet.cssText = text; |
| 42 | + } else { |
| 43 | + style.text( text ); |
| 44 | + } |
| 45 | + style.appendTo( 'head' ); |
| 46 | + }; |
| 47 | + |
| 48 | + /* |
| 49 | + * Base64 decode |
| 50 | + * @link http://www.webtoolkit.info/javascript-base64.html |
| 51 | + * @license http://www.webtoolkit.info/license |
| 52 | + */ |
| 53 | + var Base64 = { |
| 54 | + // private property |
| 55 | + _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=", |
| 56 | + |
| 57 | + // public method for decoding |
| 58 | + decode : function(input) { |
| 59 | + var output = ""; |
| 60 | + var chr1, chr2, chr3; |
| 61 | + var enc1, enc2, enc3, enc4; |
| 62 | + var i = 0; |
| 63 | + |
| 64 | + input = input.replace(/[^A-Za-z0-9\+\/\=]/g, ""); |
| 65 | + |
| 66 | + while (i < input.length) { |
| 67 | + |
| 68 | + enc1 = this._keyStr.indexOf(input.charAt(i++)); |
| 69 | + enc2 = this._keyStr.indexOf(input.charAt(i++)); |
| 70 | + enc3 = this._keyStr.indexOf(input.charAt(i++)); |
| 71 | + enc4 = this._keyStr.indexOf(input.charAt(i++)); |
| 72 | + |
| 73 | + chr1 = (enc1 << 2) | (enc2 >> 4); |
| 74 | + chr2 = ((enc2 & 15) << 4) | (enc3 >> 2); |
| 75 | + chr3 = ((enc3 & 3) << 6) | enc4; |
| 76 | + |
| 77 | + output = output + String.fromCharCode(chr1); |
| 78 | + |
| 79 | + if (enc3 != 64) { |
| 80 | + output = output + String.fromCharCode(chr2); |
| 81 | + } |
| 82 | + if (enc4 != 64) { |
| 83 | + output = output + String.fromCharCode(chr3); |
| 84 | + } |
| 85 | + } |
| 86 | + |
| 87 | + output = Base64._utf8_decode(output); |
| 88 | + return output; |
| 89 | + }, |
| 90 | + |
| 91 | + // private method for UTF-8 decoding |
| 92 | + _utf8_decode : function(utftext) { |
| 93 | + var string = ""; |
| 94 | + var i = 0; |
| 95 | + var c = c1 = c2 = 0; |
| 96 | + |
| 97 | + while ( i < utftext.length ) { |
| 98 | + c = utftext.charCodeAt(i); |
| 99 | + |
| 100 | + if (c < 128) { |
| 101 | + string += String.fromCharCode(c); |
| 102 | + i++; |
| 103 | + } |
| 104 | + else if((c > 191) && (c < 224)) { |
| 105 | + c2 = utftext.charCodeAt(i+1); |
| 106 | + string += String.fromCharCode(((c & 31) << 6) | (c2 & 63)); |
| 107 | + i += 2; |
| 108 | + } |
| 109 | + else { |
| 110 | + c2 = utftext.charCodeAt(i+1); |
| 111 | + c3 = utftext.charCodeAt(i+2); |
| 112 | + string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63)); |
| 113 | + i += 3; |
| 114 | + } |
| 115 | + } |
| 116 | + return string; |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + /* |
| 121 | + * A class to parse color values |
| 122 | + * @author Stoyan Stefanov <sstoo@gmail.com> |
| 123 | + * @link http://www.phpied.com/rgb-color-parser-in-javascript/ |
| 124 | + * @license Use it if you like it |
| 125 | + */ |
| 126 | + function RGBColor(color_string) { |
| 127 | + this.ok = false; |
| 128 | + |
| 129 | + // strip any leading # |
| 130 | + if (color_string.charAt(0) == '#') { // remove # if any |
| 131 | + color_string = color_string.substr(1,6); |
| 132 | + } |
| 133 | + |
| 134 | + color_string = color_string.replace(/ /g,''); |
| 135 | + color_string = color_string.toLowerCase(); |
| 136 | + |
| 137 | + // before getting into regexps, try simple matches |
| 138 | + // and overwrite the input |
| 139 | + var simple_colors = { |
| 140 | + aliceblue: 'f0f8ff', |
| 141 | + antiquewhite: 'faebd7', |
| 142 | + aqua: '00ffff', |
| 143 | + aquamarine: '7fffd4', |
| 144 | + azure: 'f0ffff', |
| 145 | + beige: 'f5f5dc', |
| 146 | + bisque: 'ffe4c4', |
| 147 | + black: '000000', |
| 148 | + blanchedalmond: 'ffebcd', |
| 149 | + blue: '0000ff', |
| 150 | + blueviolet: '8a2be2', |
| 151 | + brown: 'a52a2a', |
| 152 | + burlywood: 'deb887', |
| 153 | + cadetblue: '5f9ea0', |
| 154 | + chartreuse: '7fff00', |
| 155 | + chocolate: 'd2691e', |
| 156 | + coral: 'ff7f50', |
| 157 | + cornflowerblue: '6495ed', |
| 158 | + cornsilk: 'fff8dc', |
| 159 | + crimson: 'dc143c', |
| 160 | + cyan: '00ffff', |
| 161 | + darkblue: '00008b', |
| 162 | + darkcyan: '008b8b', |
| 163 | + darkgoldenrod: 'b8860b', |
| 164 | + darkgray: 'a9a9a9', |
| 165 | + darkgreen: '006400', |
| 166 | + darkkhaki: 'bdb76b', |
| 167 | + darkmagenta: '8b008b', |
| 168 | + darkolivegreen: '556b2f', |
| 169 | + darkorange: 'ff8c00', |
| 170 | + darkorchid: '9932cc', |
| 171 | + darkred: '8b0000', |
| 172 | + darksalmon: 'e9967a', |
| 173 | + darkseagreen: '8fbc8f', |
| 174 | + darkslateblue: '483d8b', |
| 175 | + darkslategray: '2f4f4f', |
| 176 | + darkturquoise: '00ced1', |
| 177 | + darkviolet: '9400d3', |
| 178 | + deeppink: 'ff1493', |
| 179 | + deepskyblue: '00bfff', |
| 180 | + dimgray: '696969', |
| 181 | + dodgerblue: '1e90ff', |
| 182 | + feldspar: 'd19275', |
| 183 | + firebrick: 'b22222', |
| 184 | + floralwhite: 'fffaf0', |
| 185 | + forestgreen: '228b22', |
| 186 | + fuchsia: 'ff00ff', |
| 187 | + gainsboro: 'dcdcdc', |
| 188 | + ghostwhite: 'f8f8ff', |
| 189 | + gold: 'ffd700', |
| 190 | + goldenrod: 'daa520', |
| 191 | + gray: '808080', |
| 192 | + green: '008000', |
| 193 | + greenyellow: 'adff2f', |
| 194 | + honeydew: 'f0fff0', |
| 195 | + hotpink: 'ff69b4', |
| 196 | + indianred : 'cd5c5c', |
| 197 | + indigo : '4b0082', |
| 198 | + ivory: 'fffff0', |
| 199 | + khaki: 'f0e68c', |
| 200 | + lavender: 'e6e6fa', |
| 201 | + lavenderblush: 'fff0f5', |
| 202 | + lawngreen: '7cfc00', |
| 203 | + lemonchiffon: 'fffacd', |
| 204 | + lightblue: 'add8e6', |
| 205 | + lightcoral: 'f08080', |
| 206 | + lightcyan: 'e0ffff', |
| 207 | + lightgoldenrodyellow: 'fafad2', |
| 208 | + lightgrey: 'd3d3d3', |
| 209 | + lightgreen: '90ee90', |
| 210 | + lightpink: 'ffb6c1', |
| 211 | + lightsalmon: 'ffa07a', |
| 212 | + lightseagreen: '20b2aa', |
| 213 | + lightskyblue: '87cefa', |
| 214 | + lightslateblue: '8470ff', |
| 215 | + lightslategray: '778899', |
| 216 | + lightsteelblue: 'b0c4de', |
| 217 | + lightyellow: 'ffffe0', |
| 218 | + lime: '00ff00', |
| 219 | + limegreen: '32cd32', |
| 220 | + linen: 'faf0e6', |
| 221 | + magenta: 'ff00ff', |
| 222 | + maroon: '800000', |
| 223 | + mediumaquamarine: '66cdaa', |
| 224 | + mediumblue: '0000cd', |
| 225 | + mediumorchid: 'ba55d3', |
| 226 | + mediumpurple: '9370d8', |
| 227 | + mediumseagreen: '3cb371', |
| 228 | + mediumslateblue: '7b68ee', |
| 229 | + mediumspringgreen: '00fa9a', |
| 230 | + mediumturquoise: '48d1cc', |
| 231 | + mediumvioletred: 'c71585', |
| 232 | + midnightblue: '191970', |
| 233 | + mintcream: 'f5fffa', |
| 234 | + mistyrose: 'ffe4e1', |
| 235 | + moccasin: 'ffe4b5', |
| 236 | + navajowhite: 'ffdead', |
| 237 | + navy: '000080', |
| 238 | + oldlace: 'fdf5e6', |
| 239 | + olive: '808000', |
| 240 | + olivedrab: '6b8e23', |
| 241 | + orange: 'ffa500', |
| 242 | + orangered: 'ff4500', |
| 243 | + orchid: 'da70d6', |
| 244 | + palegoldenrod: 'eee8aa', |
| 245 | + palegreen: '98fb98', |
| 246 | + paleturquoise: 'afeeee', |
| 247 | + palevioletred: 'd87093', |
| 248 | + papayawhip: 'ffefd5', |
| 249 | + peachpuff: 'ffdab9', |
| 250 | + peru: 'cd853f', |
| 251 | + pink: 'ffc0cb', |
| 252 | + plum: 'dda0dd', |
| 253 | + powderblue: 'b0e0e6', |
| 254 | + purple: '800080', |
| 255 | + red: 'ff0000', |
| 256 | + rosybrown: 'bc8f8f', |
| 257 | + royalblue: '4169e1', |
| 258 | + saddlebrown: '8b4513', |
| 259 | + salmon: 'fa8072', |
| 260 | + sandybrown: 'f4a460', |
| 261 | + seagreen: '2e8b57', |
| 262 | + seashell: 'fff5ee', |
| 263 | + sienna: 'a0522d', |
| 264 | + silver: 'c0c0c0', |
| 265 | + skyblue: '87ceeb', |
| 266 | + slateblue: '6a5acd', |
| 267 | + slategray: '708090', |
| 268 | + snow: 'fffafa', |
| 269 | + springgreen: '00ff7f', |
| 270 | + steelblue: '4682b4', |
| 271 | + tan: 'd2b48c', |
| 272 | + teal: '008080', |
| 273 | + thistle: 'd8bfd8', |
| 274 | + tomato: 'ff6347', |
| 275 | + turquoise: '40e0d0', |
| 276 | + violet: 'ee82ee', |
| 277 | + violetred: 'd02090', |
| 278 | + wheat: 'f5deb3', |
| 279 | + white: 'ffffff', |
| 280 | + whitesmoke: 'f5f5f5', |
| 281 | + yellow: 'ffff00', |
| 282 | + yellowgreen: '9acd32' |
| 283 | + }; |
| 284 | + for (var key in simple_colors) { |
| 285 | + if (color_string == key) { |
| 286 | + color_string = simple_colors[key]; |
| 287 | + } |
| 288 | + } |
| 289 | + // emd of simple type-in colors |
| 290 | + |
| 291 | + // array of color definition objects |
| 292 | + var color_defs = [ |
| 293 | + { |
| 294 | + re: /^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/, |
| 295 | + example: ['rgb(123, 234, 45)', 'rgb(255,234,245)'], |
| 296 | + process: function(bits) { |
| 297 | + return [ |
| 298 | + parseInt(bits[1]), |
| 299 | + parseInt(bits[2]), |
| 300 | + parseInt(bits[3]) |
| 301 | + ]; |
| 302 | + } |
| 303 | + }, |
| 304 | + { |
| 305 | + re: /^(\w{2})(\w{2})(\w{2})$/, |
| 306 | + example: ['#00ff00', '336699'], |
| 307 | + process: function(bits) { |
| 308 | + return [ |
| 309 | + parseInt(bits[1], 16), |
| 310 | + parseInt(bits[2], 16), |
| 311 | + parseInt(bits[3], 16) |
| 312 | + ]; |
| 313 | + } |
| 314 | + }, |
| 315 | + { |
| 316 | + re: /^(\w{1})(\w{1})(\w{1})$/, |
| 317 | + example: ['#fb0', 'f0f'], |
| 318 | + process: function(bits) { |
| 319 | + return [ |
| 320 | + parseInt(bits[1] + bits[1], 16), |
| 321 | + parseInt(bits[2] + bits[2], 16), |
| 322 | + parseInt(bits[3] + bits[3], 16) |
| 323 | + ]; |
| 324 | + } |
| 325 | + } |
| 326 | + ]; |
| 327 | + |
| 328 | + // search through the definitions to find a match |
| 329 | + for (var i = 0; i < color_defs.length; i++) { |
| 330 | + var re = color_defs[i].re; |
| 331 | + var processor = color_defs[i].process; |
| 332 | + var bits = re.exec(color_string); |
| 333 | + if (bits) { |
| 334 | + channels = processor(bits); |
| 335 | + this.r = channels[0]; |
| 336 | + this.g = channels[1]; |
| 337 | + this.b = channels[2]; |
| 338 | + this.ok = true; |
| 339 | + } |
| 340 | + |
| 341 | + } |
| 342 | + |
| 343 | + // validate/cleanup values |
| 344 | + this.r = (this.r < 0 || isNaN(this.r)) ? 0 : ((this.r > 255) ? 255 : this.r); |
| 345 | + this.g = (this.g < 0 || isNaN(this.g)) ? 0 : ((this.g > 255) ? 255 : this.g); |
| 346 | + this.b = (this.b < 0 || isNaN(this.b)) ? 0 : ((this.b > 255) ? 255 : this.b); |
| 347 | + |
| 348 | + // some getters |
| 349 | + this.toRGB = function() { |
| 350 | + return 'rgb(' + this.r + ', ' + this.g + ', ' + this.b + ')'; |
| 351 | + } |
| 352 | + |
| 353 | + this.toHex = function() { |
| 354 | + var r = this.r.toString(16); |
| 355 | + var g = this.g.toString(16); |
| 356 | + var b = this.b.toString(16); |
| 357 | + if (r.length == 1) r = '0' + r; |
| 358 | + if (g.length == 1) g = '0' + g; |
| 359 | + if (b.length == 1) b = '0' + b; |
| 360 | + return '#' + r + g + b; |
| 361 | + } |
| 362 | + } |
| 363 | +})( jQuery ); |
Index: trunk/extensions/CSS/CSS.php |
— | — | @@ -16,15 +16,17 @@ |
17 | 17 | * @licence GNU General Public Licence 2.0 or later |
18 | 18 | */ |
19 | 19 | |
20 | | -if ( !defined( 'MEDIAWIKI') ) die('Not an entry point.' ); |
| 20 | +if ( !defined( 'MEDIAWIKI' ) ) die( 'Not an entry point.' ); |
21 | 21 | |
22 | | -define( 'CSS_VERSION', '2.0, 2011-10-27' ); |
| 22 | +define( 'CSS_VERSION', '3.0, 2011-11-18' ); |
23 | 23 | |
24 | 24 | $wgCSSMagic = 'css'; |
25 | 25 | $wgCSSPath = false; |
| 26 | +$wgCSSIdentifier = 'css-extension'; |
26 | 27 | |
27 | 28 | $wgHooks['ParserFirstCallInit'][] = 'wfCSSParserFirstCallInit'; |
28 | 29 | $wgHooks['LanguageGetMagic'][] = 'wfCSSLanguageGetMagic'; |
| 30 | +$wgHooks['RawPageViewBeforeOutput'][] = 'wfCSSRawPageViewBeforeOutput'; |
29 | 31 | |
30 | 32 | $wgExtensionCredits['parserhook'][] = array( |
31 | 33 | 'path' => __FILE__, |
— | — | @@ -37,27 +39,73 @@ |
38 | 40 | |
39 | 41 | $wgExtensionMessagesFiles['CSS'] = dirname( __FILE__ ) . '/' . 'CSS.i18n.php'; |
40 | 42 | |
| 43 | +$wgResourceModules['ext.CSS'] = array( |
| 44 | + 'scripts' => 'verifyCSSLoad.js', |
| 45 | + 'localBasePath' => dirname( __FILE__ ), |
| 46 | + 'remoteExtPath' => 'CSS', |
| 47 | +); |
| 48 | + |
41 | 49 | function wfCSSRender( &$parser, $css ) { |
42 | | - global $wgCSSPath, $wgScriptPath; |
| 50 | + global $wgCSSPath, $wgStylePath, $wgCSSIdentifier; |
43 | 51 | |
44 | 52 | $css = trim( $css ); |
45 | 53 | $title = Title::newFromText( $css ); |
46 | | - if ( is_object( $title ) && $title->isKnown() ) { |
47 | | - # "blue link" Article |
48 | | - $url = $title->getLocalURL( 'action=raw&ctype=text/css' ); |
49 | | - $headItem = HTML::linkedStyle( $url ); |
| 54 | + $rawProtection = "$wgCSSIdentifier=1"; |
| 55 | + $headItem = '<!-- Begin Extension:CSS -->'; |
| 56 | + |
| 57 | + if ( is_object( $title ) && $title->exists() ) { |
| 58 | + # Article actually in the db |
| 59 | + $params = "action=raw&ctype=text/css&$rawProtection"; |
| 60 | + $url = $title->getLocalURL( $params ); |
| 61 | + $headItem .= HTML::linkedStyle( $url ); |
50 | 62 | } elseif ( $css[0] == '/' ) { |
51 | 63 | # Regular file |
52 | | - $base = $wgCSSPath === false ? $wgScriptPath : $wgCSSPath; |
53 | | - $headItem = HTML::linkedStyle( $base . $css ); |
| 64 | + $base = $wgCSSPath === false ? $wgStylePath : $wgCSSPath; |
| 65 | + $url = wfAppendQuery( $base . $css, $rawProtection ); |
| 66 | + |
| 67 | + # Verify the expanded URL is still using the base URL |
| 68 | + if ( strpos( wfExpandUrl( $url ), wfExpandUrl( $base ) ) === 0 ) { |
| 69 | + $headItem .= HTML::linkedStyle( $url ); |
| 70 | + } else { |
| 71 | + $headItem .= '<!-- Invalid/malicious path -->'; |
| 72 | + } |
54 | 73 | } else { |
55 | | - # Inline CSS |
56 | | - $headItem = HTML::inlineStyle( Sanitizer::checkCss( $css ) ); |
| 74 | + # Inline CSS; use data URI to prevent injection. JavaScript |
| 75 | + # will use a canary to verify load and will safely convert to |
| 76 | + # style tag if load fails. |
| 77 | + |
| 78 | + # Generate random CSS color that isn't black or white. |
| 79 | + $color = dechex( mt_rand( 1, hexdec( 'fffffe' ) ) ); |
| 80 | + $color = str_pad( $color, 6, '0', STR_PAD_LEFT ); |
| 81 | + |
| 82 | + # Prepend canary CSS to sanitized user CSS |
| 83 | + $canaryId = "$wgCSSIdentifier-canary-$color"; |
| 84 | + $canaryCSS = "#$canaryId{background:#$color !important}"; |
| 85 | + $css = $canaryCSS . Sanitizer::checkCss( $css ); |
| 86 | + |
| 87 | + # Encode data URI and append link tag |
| 88 | + $dataPrefix = 'data:text/css;charset=UTF-8;base64,'; |
| 89 | + $url = $dataPrefix . base64_encode( $css ); |
| 90 | + $headItem .= HTML::linkedStyle( $url ); |
| 91 | + |
| 92 | + # Calculate URI prefix to match link tag |
| 93 | + $hrefPrefix = $dataPrefix . base64_encode( '#' . $canaryId ); |
| 94 | + $hrefPrefix = substr( $url, 0, strlen( $hrefPrefix ) ); |
| 95 | + |
| 96 | + # Add JS to verify the link tag loaded and fallback if needed |
| 97 | + $parser->getOutput()->addModules( 'ext.CSS' ); |
| 98 | + $headItem .= HTML::inlineScript( <<<INLINESCRIPT |
| 99 | +jQuery( function( $ ) { |
| 100 | + $( 'link[href^="$hrefPrefix"]' ) |
| 101 | + .cssExtensionDataURIFallback( '$canaryId', '$color' ); |
| 102 | +} ); |
| 103 | +INLINESCRIPT |
| 104 | + ); |
57 | 105 | } |
58 | 106 | |
59 | | - $headItem = FormatJson::encode( $headItem ); |
60 | | - $script = HTML::inlineScript( "$('head').append( $headItem );" ); |
61 | | - return array( $script, 'isHTML' => true ); |
| 107 | + $headItem .= '<!-- End Extension:CSS -->'; |
| 108 | + $parser->getOutput()->addHeadItem( $headItem ); |
| 109 | + return ''; |
62 | 110 | } |
63 | 111 | |
64 | 112 | function wfCSSParserFirstCallInit( $parser ) { |
— | — | @@ -71,3 +119,11 @@ |
72 | 120 | $magicWords[$wgCSSMagic] = array( $langCode, $wgCSSMagic ); |
73 | 121 | return true; |
74 | 122 | } |
| 123 | + |
| 124 | +function wfCSSRawPageViewBeforeOutput( &$rawPage, &$text ) { |
| 125 | + global $wgCSSIdentifier; |
| 126 | + if ( $rawPage->getRequest()->getBool( $wgCSSIdentifier ) ) { |
| 127 | + $text = Sanitizer::checkCss( $text ); |
| 128 | + } |
| 129 | + return true; |
| 130 | +} |