File indexing completed on 2025-01-26 05:29:06
0001 <?php 0002 0003 /** 0004 * Validates a font family list according to CSS spec 0005 */ 0006 class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef 0007 { 0008 0009 protected $mask = null; 0010 0011 public function __construct() 0012 { 0013 $this->mask = '_- '; 0014 for ($c = 'a'; $c <= 'z'; $c++) { 0015 $this->mask .= $c; 0016 } 0017 for ($c = 'A'; $c <= 'Z'; $c++) { 0018 $this->mask .= $c; 0019 } 0020 for ($c = '0'; $c <= '9'; $c++) { 0021 $this->mask .= $c; 0022 } // cast-y, but should be fine 0023 // special bytes used by UTF-8 0024 for ($i = 0x80; $i <= 0xFF; $i++) { 0025 // We don't bother excluding invalid bytes in this range, 0026 // because the our restriction of well-formed UTF-8 will 0027 // prevent these from ever occurring. 0028 $this->mask .= chr($i); 0029 } 0030 0031 /* 0032 PHP's internal strcspn implementation is 0033 O(length of string * length of mask), making it inefficient 0034 for large masks. However, it's still faster than 0035 preg_match 8) 0036 for (p = s1;;) { 0037 spanp = s2; 0038 do { 0039 if (*spanp == c || p == s1_end) { 0040 return p - s1; 0041 } 0042 } while (spanp++ < (s2_end - 1)); 0043 c = *++p; 0044 } 0045 */ 0046 // possible optimization: invert the mask. 0047 } 0048 0049 /** 0050 * @param string $string 0051 * @param HTMLPurifier_Config $config 0052 * @param HTMLPurifier_Context $context 0053 * @return bool|string 0054 */ 0055 public function validate($string, $config, $context) 0056 { 0057 static $generic_names = array( 0058 'serif' => true, 0059 'sans-serif' => true, 0060 'monospace' => true, 0061 'fantasy' => true, 0062 'cursive' => true 0063 ); 0064 $allowed_fonts = $config->get('CSS.AllowedFonts'); 0065 0066 // assume that no font names contain commas in them 0067 $fonts = explode(',', $string); 0068 $final = ''; 0069 foreach ($fonts as $font) { 0070 $font = trim($font); 0071 if ($font === '') { 0072 continue; 0073 } 0074 // match a generic name 0075 if (isset($generic_names[$font])) { 0076 if ($allowed_fonts === null || isset($allowed_fonts[$font])) { 0077 $final .= $font . ', '; 0078 } 0079 continue; 0080 } 0081 // match a quoted name 0082 if ($font[0] === '"' || $font[0] === "'") { 0083 $length = strlen($font); 0084 if ($length <= 2) { 0085 continue; 0086 } 0087 $quote = $font[0]; 0088 if ($font[$length - 1] !== $quote) { 0089 continue; 0090 } 0091 $font = substr($font, 1, $length - 2); 0092 } 0093 0094 $font = $this->expandCSSEscape($font); 0095 0096 // $font is a pure representation of the font name 0097 0098 if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) { 0099 continue; 0100 } 0101 0102 if (ctype_alnum($font) && $font !== '') { 0103 // very simple font, allow it in unharmed 0104 $final .= $font . ', '; 0105 continue; 0106 } 0107 0108 // bugger out on whitespace. form feed (0C) really 0109 // shouldn't show up regardless 0110 $font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font); 0111 0112 // Here, there are various classes of characters which need 0113 // to be treated differently: 0114 // - Alphanumeric characters are essentially safe. We 0115 // handled these above. 0116 // - Spaces require quoting, though most parsers will do 0117 // the right thing if there aren't any characters that 0118 // can be misinterpreted 0119 // - Dashes rarely occur, but they fairly unproblematic 0120 // for parsing/rendering purposes. 0121 // The above characters cover the majority of Western font 0122 // names. 0123 // - Arbitrary Unicode characters not in ASCII. Because 0124 // most parsers give little thought to Unicode, treatment 0125 // of these codepoints is basically uniform, even for 0126 // punctuation-like codepoints. These characters can 0127 // show up in non-Western pages and are supported by most 0128 // major browsers, for example: "MS 明朝" is a 0129 // legitimate font-name 0130 // <http://ja.wikipedia.org/wiki/MS_明朝>. See 0131 // the CSS3 spec for more examples: 0132 // <http://www.w3.org/TR/2011/WD-css3-fonts-20110324/localizedfamilynames.png> 0133 // You can see live samples of these on the Internet: 0134 // <http://www.google.co.jp/search?q=font-family+MS+明朝|ゴシック> 0135 // However, most of these fonts have ASCII equivalents: 0136 // for example, 'MS Mincho', and it's considered 0137 // professional to use ASCII font names instead of 0138 // Unicode font names. Thanks Takeshi Terada for 0139 // providing this information. 0140 // The following characters, to my knowledge, have not been 0141 // used to name font names. 0142 // - Single quote. While theoretically you might find a 0143 // font name that has a single quote in its name (serving 0144 // as an apostrophe, e.g. Dave's Scribble), I haven't 0145 // been able to find any actual examples of this. 0146 // Internet Explorer's cssText translation (which I 0147 // believe is invoked by innerHTML) normalizes any 0148 // quoting to single quotes, and fails to escape single 0149 // quotes. (Note that this is not IE's behavior for all 0150 // CSS properties, just some sort of special casing for 0151 // font-family). So a single quote *cannot* be used 0152 // safely in the font-family context if there will be an 0153 // innerHTML/cssText translation. Note that Firefox 3.x 0154 // does this too. 0155 // - Double quote. In IE, these get normalized to 0156 // single-quotes, no matter what the encoding. (Fun 0157 // fact, in IE8, the 'content' CSS property gained 0158 // support, where they special cased to preserve encoded 0159 // double quotes, but still translate unadorned double 0160 // quotes into single quotes.) So, because their 0161 // fixpoint behavior is identical to single quotes, they 0162 // cannot be allowed either. Firefox 3.x displays 0163 // single-quote style behavior. 0164 // - Backslashes are reduced by one (so \\ -> \) every 0165 // iteration, so they cannot be used safely. This shows 0166 // up in IE7, IE8 and FF3 0167 // - Semicolons, commas and backticks are handled properly. 0168 // - The rest of the ASCII punctuation is handled properly. 0169 // We haven't checked what browsers do to unadorned 0170 // versions, but this is not important as long as the 0171 // browser doesn't /remove/ surrounding quotes (as IE does 0172 // for HTML). 0173 // 0174 // With these results in hand, we conclude that there are 0175 // various levels of safety: 0176 // - Paranoid: alphanumeric, spaces and dashes(?) 0177 // - International: Paranoid + non-ASCII Unicode 0178 // - Edgy: Everything except quotes, backslashes 0179 // - NoJS: Standards compliance, e.g. sod IE. Note that 0180 // with some judicious character escaping (since certain 0181 // types of escaping doesn't work) this is theoretically 0182 // OK as long as innerHTML/cssText is not called. 0183 // We believe that international is a reasonable default 0184 // (that we will implement now), and once we do more 0185 // extensive research, we may feel comfortable with dropping 0186 // it down to edgy. 0187 0188 // Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of 0189 // str(c)spn assumes that the string was already well formed 0190 // Unicode (which of course it is). 0191 if (strspn($font, $this->mask) !== strlen($font)) { 0192 continue; 0193 } 0194 0195 // Historical: 0196 // In the absence of innerHTML/cssText, these ugly 0197 // transforms don't pose a security risk (as \\ and \" 0198 // might--these escapes are not supported by most browsers). 0199 // We could try to be clever and use single-quote wrapping 0200 // when there is a double quote present, but I have choosen 0201 // not to implement that. (NOTE: you can reduce the amount 0202 // of escapes by one depending on what quoting style you use) 0203 // $font = str_replace('\\', '\\5C ', $font); 0204 // $font = str_replace('"', '\\22 ', $font); 0205 // $font = str_replace("'", '\\27 ', $font); 0206 0207 // font possibly with spaces, requires quoting 0208 $final .= "'$font', "; 0209 } 0210 $final = rtrim($final, ', '); 0211 if ($final === '') { 0212 return false; 0213 } 0214 return $final; 0215 } 0216 0217 } 0218 0219 // vim: et sw=4 sts=4