HTMLPurifier 4.4.0
|
00001 <?php 00002 00006 class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef 00007 { 00008 00009 protected $mask = null; 00010 00011 public function __construct() { 00012 $this->mask = '- '; 00013 for ($c = 'a'; $c <= 'z'; $c++) $this->mask .= $c; 00014 for ($c = 'A'; $c <= 'Z'; $c++) $this->mask .= $c; 00015 for ($c = '0'; $c <= '9'; $c++) $this->mask .= $c; // cast-y, but should be fine 00016 // special bytes used by UTF-8 00017 for ($i = 0x80; $i <= 0xFF; $i++) { 00018 // We don't bother excluding invalid bytes in this range, 00019 // because the our restriction of well-formed UTF-8 will 00020 // prevent these from ever occurring. 00021 $this->mask .= chr($i); 00022 } 00023 00024 /* 00025 PHP's internal strcspn implementation is 00026 O(length of string * length of mask), making it inefficient 00027 for large masks. However, it's still faster than 00028 preg_match 8) 00029 for (p = s1;;) { 00030 spanp = s2; 00031 do { 00032 if (*spanp == c || p == s1_end) { 00033 return p - s1; 00034 } 00035 } while (spanp++ < (s2_end - 1)); 00036 c = *++p; 00037 } 00038 */ 00039 // possible optimization: invert the mask. 00040 } 00041 00042 public function validate($string, $config, $context) { 00043 static $generic_names = array( 00044 'serif' => true, 00045 'sans-serif' => true, 00046 'monospace' => true, 00047 'fantasy' => true, 00048 'cursive' => true 00049 ); 00050 $allowed_fonts = $config->get('CSS.AllowedFonts'); 00051 00052 // assume that no font names contain commas in them 00053 $fonts = explode(',', $string); 00054 $final = ''; 00055 foreach($fonts as $font) { 00056 $font = trim($font); 00057 if ($font === '') continue; 00058 // match a generic name 00059 if (isset($generic_names[$font])) { 00060 if ($allowed_fonts === null || isset($allowed_fonts[$font])) { 00061 $final .= $font . ', '; 00062 } 00063 continue; 00064 } 00065 // match a quoted name 00066 if ($font[0] === '"' || $font[0] === "'") { 00067 $length = strlen($font); 00068 if ($length <= 2) continue; 00069 $quote = $font[0]; 00070 if ($font[$length - 1] !== $quote) continue; 00071 $font = substr($font, 1, $length - 2); 00072 } 00073 00074 $font = $this->expandCSSEscape($font); 00075 00076 // $font is a pure representation of the font name 00077 00078 if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) { 00079 continue; 00080 } 00081 00082 if (ctype_alnum($font) && $font !== '') { 00083 // very simple font, allow it in unharmed 00084 $final .= $font . ', '; 00085 continue; 00086 } 00087 00088 // bugger out on whitespace. form feed (0C) really 00089 // shouldn't show up regardless 00090 $font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font); 00091 00092 // Here, there are various classes of characters which need 00093 // to be treated differently: 00094 // - Alphanumeric characters are essentially safe. We 00095 // handled these above. 00096 // - Spaces require quoting, though most parsers will do 00097 // the right thing if there aren't any characters that 00098 // can be misinterpreted 00099 // - Dashes rarely occur, but they fairly unproblematic 00100 // for parsing/rendering purposes. 00101 // The above characters cover the majority of Western font 00102 // names. 00103 // - Arbitrary Unicode characters not in ASCII. Because 00104 // most parsers give little thought to Unicode, treatment 00105 // of these codepoints is basically uniform, even for 00106 // punctuation-like codepoints. These characters can 00107 // show up in non-Western pages and are supported by most 00108 // major browsers, for example: "MS 明朝" is a 00109 // legitimate font-name 00110 // <http://ja.wikipedia.org/wiki/MS_明朝>. See 00111 // the CSS3 spec for more examples: 00112 // <http://www.w3.org/TR/2011/WD-css3-fonts-20110324/localizedfamilynames.png> 00113 // You can see live samples of these on the Internet: 00114 // <http://www.google.co.jp/search?q=font-family+MS+明朝|ゴシック> 00115 // However, most of these fonts have ASCII equivalents: 00116 // for example, 'MS Mincho', and it's considered 00117 // professional to use ASCII font names instead of 00118 // Unicode font names. Thanks Takeshi Terada for 00119 // providing this information. 00120 // The following characters, to my knowledge, have not been 00121 // used to name font names. 00122 // - Single quote. While theoretically you might find a 00123 // font name that has a single quote in its name (serving 00124 // as an apostrophe, e.g. Dave's Scribble), I haven't 00125 // been able to find any actual examples of this. 00126 // Internet Explorer's cssText translation (which I 00127 // believe is invoked by innerHTML) normalizes any 00128 // quoting to single quotes, and fails to escape single 00129 // quotes. (Note that this is not IE's behavior for all 00130 // CSS properties, just some sort of special casing for 00131 // font-family). So a single quote *cannot* be used 00132 // safely in the font-family context if there will be an 00133 // innerHTML/cssText translation. Note that Firefox 3.x 00134 // does this too. 00135 // - Double quote. In IE, these get normalized to 00136 // single-quotes, no matter what the encoding. (Fun 00137 // fact, in IE8, the 'content' CSS property gained 00138 // support, where they special cased to preserve encoded 00139 // double quotes, but still translate unadorned double 00140 // quotes into single quotes.) So, because their 00141 // fixpoint behavior is identical to single quotes, they 00142 // cannot be allowed either. Firefox 3.x displays 00143 // single-quote style behavior. 00144 // - Backslashes are reduced by one (so \\ -> \) every 00145 // iteration, so they cannot be used safely. This shows 00146 // up in IE7, IE8 and FF3 00147 // - Semicolons, commas and backticks are handled properly. 00148 // - The rest of the ASCII punctuation is handled properly. 00149 // We haven't checked what browsers do to unadorned 00150 // versions, but this is not important as long as the 00151 // browser doesn't /remove/ surrounding quotes (as IE does 00152 // for HTML). 00153 // 00154 // With these results in hand, we conclude that there are 00155 // various levels of safety: 00156 // - Paranoid: alphanumeric, spaces and dashes(?) 00157 // - International: Paranoid + non-ASCII Unicode 00158 // - Edgy: Everything except quotes, backslashes 00159 // - NoJS: Standards compliance, e.g. sod IE. Note that 00160 // with some judicious character escaping (since certain 00161 // types of escaping doesn't work) this is theoretically 00162 // OK as long as innerHTML/cssText is not called. 00163 // We believe that international is a reasonable default 00164 // (that we will implement now), and once we do more 00165 // extensive research, we may feel comfortable with dropping 00166 // it down to edgy. 00167 00168 // Edgy: alphanumeric, spaces, dashes and Unicode. Use of 00169 // str(c)spn assumes that the string was already well formed 00170 // Unicode (which of course it is). 00171 if (strspn($font, $this->mask) !== strlen($font)) { 00172 continue; 00173 } 00174 00175 // Historical: 00176 // In the absence of innerHTML/cssText, these ugly 00177 // transforms don't pose a security risk (as \\ and \" 00178 // might--these escapes are not supported by most browsers). 00179 // We could try to be clever and use single-quote wrapping 00180 // when there is a double quote present, but I have choosen 00181 // not to implement that. (NOTE: you can reduce the amount 00182 // of escapes by one depending on what quoting style you use) 00183 // $font = str_replace('\\', '\\5C ', $font); 00184 // $font = str_replace('"', '\\22 ', $font); 00185 // $font = str_replace("'", '\\27 ', $font); 00186 00187 // font possibly with spaces, requires quoting 00188 $final .= "'$font', "; 00189 } 00190 $final = rtrim($final, ', '); 00191 if ($final === '') return false; 00192 return $final; 00193 } 00194 00195 } 00196 00197 // vim: et sw=4 sts=4