1: <?php
2:
3: /**
4: * Validates a font family list according to CSS spec
5: */
6: class HTMLPurifier_AttrDef_CSS_FontFamily extends HTMLPurifier_AttrDef
7: {
8:
9: protected $mask = null;
10:
11: public function __construct()
12: {
13: $this->mask = '_- ';
14: for ($c = 'a'; $c <= 'z'; $c++) {
15: $this->mask .= $c;
16: }
17: for ($c = 'A'; $c <= 'Z'; $c++) {
18: $this->mask .= $c;
19: }
20: for ($c = '0'; $c <= '9'; $c++) {
21: $this->mask .= $c;
22: } // cast-y, but should be fine
23: // special bytes used by UTF-8
24: for ($i = 0x80; $i <= 0xFF; $i++) {
25: // We don't bother excluding invalid bytes in this range,
26: // because the our restriction of well-formed UTF-8 will
27: // prevent these from ever occurring.
28: $this->mask .= chr($i);
29: }
30:
31: /*
32: PHP's internal strcspn implementation is
33: O(length of string * length of mask), making it inefficient
34: for large masks. However, it's still faster than
35: preg_match 8)
36: for (p = s1;;) {
37: spanp = s2;
38: do {
39: if (*spanp == c || p == s1_end) {
40: return p - s1;
41: }
42: } while (spanp++ < (s2_end - 1));
43: c = *++p;
44: }
45: */
46: // possible optimization: invert the mask.
47: }
48:
49: /**
50: * @param string $string
51: * @param HTMLPurifier_Config $config
52: * @param HTMLPurifier_Context $context
53: * @return bool|string
54: */
55: public function validate($string, $config, $context)
56: {
57: static $generic_names = array(
58: 'serif' => true,
59: 'sans-serif' => true,
60: 'monospace' => true,
61: 'fantasy' => true,
62: 'cursive' => true
63: );
64: $allowed_fonts = $config->get('CSS.AllowedFonts');
65:
66: // assume that no font names contain commas in them
67: $fonts = explode(',', $string);
68: $final = '';
69: foreach ($fonts as $font) {
70: $font = trim($font);
71: if ($font === '') {
72: continue;
73: }
74: // match a generic name
75: if (isset($generic_names[$font])) {
76: if ($allowed_fonts === null || isset($allowed_fonts[$font])) {
77: $final .= $font . ', ';
78: }
79: continue;
80: }
81: // match a quoted name
82: if ($font[0] === '"' || $font[0] === "'") {
83: $length = strlen($font);
84: if ($length <= 2) {
85: continue;
86: }
87: $quote = $font[0];
88: if ($font[$length - 1] !== $quote) {
89: continue;
90: }
91: $font = substr($font, 1, $length - 2);
92: }
93:
94: $font = $this->expandCSSEscape($font);
95:
96: // $font is a pure representation of the font name
97:
98: if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) {
99: continue;
100: }
101:
102: if (ctype_alnum($font) && $font !== '') {
103: // very simple font, allow it in unharmed
104: $final .= $font . ', ';
105: continue;
106: }
107:
108: // bugger out on whitespace. form feed (0C) really
109: // shouldn't show up regardless
110: $font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font);
111:
112: // Here, there are various classes of characters which need
113: // to be treated differently:
114: // - Alphanumeric characters are essentially safe. We
115: // handled these above.
116: // - Spaces require quoting, though most parsers will do
117: // the right thing if there aren't any characters that
118: // can be misinterpreted
119: // - Dashes rarely occur, but they fairly unproblematic
120: // for parsing/rendering purposes.
121: // The above characters cover the majority of Western font
122: // names.
123: // - Arbitrary Unicode characters not in ASCII. Because
124: // most parsers give little thought to Unicode, treatment
125: // of these codepoints is basically uniform, even for
126: // punctuation-like codepoints. These characters can
127: // show up in non-Western pages and are supported by most
128: // major browsers, for example: "MS 明朝" is a
129: // legitimate font-name
130: // <http://ja.wikipedia.org/wiki/MS_明朝>. See
131: // the CSS3 spec for more examples:
132: // <http://www.w3.org/TR/2011/WD-css3-fonts-20110324/localizedfamilynames.png>
133: // You can see live samples of these on the Internet:
134: // <http://www.google.co.jp/search?q=font-family+MS+明朝|ゴシック>
135: // However, most of these fonts have ASCII equivalents:
136: // for example, 'MS Mincho', and it's considered
137: // professional to use ASCII font names instead of
138: // Unicode font names. Thanks Takeshi Terada for
139: // providing this information.
140: // The following characters, to my knowledge, have not been
141: // used to name font names.
142: // - Single quote. While theoretically you might find a
143: // font name that has a single quote in its name (serving
144: // as an apostrophe, e.g. Dave's Scribble), I haven't
145: // been able to find any actual examples of this.
146: // Internet Explorer's cssText translation (which I
147: // believe is invoked by innerHTML) normalizes any
148: // quoting to single quotes, and fails to escape single
149: // quotes. (Note that this is not IE's behavior for all
150: // CSS properties, just some sort of special casing for
151: // font-family). So a single quote *cannot* be used
152: // safely in the font-family context if there will be an
153: // innerHTML/cssText translation. Note that Firefox 3.x
154: // does this too.
155: // - Double quote. In IE, these get normalized to
156: // single-quotes, no matter what the encoding. (Fun
157: // fact, in IE8, the 'content' CSS property gained
158: // support, where they special cased to preserve encoded
159: // double quotes, but still translate unadorned double
160: // quotes into single quotes.) So, because their
161: // fixpoint behavior is identical to single quotes, they
162: // cannot be allowed either. Firefox 3.x displays
163: // single-quote style behavior.
164: // - Backslashes are reduced by one (so \\ -> \) every
165: // iteration, so they cannot be used safely. This shows
166: // up in IE7, IE8 and FF3
167: // - Semicolons, commas and backticks are handled properly.
168: // - The rest of the ASCII punctuation is handled properly.
169: // We haven't checked what browsers do to unadorned
170: // versions, but this is not important as long as the
171: // browser doesn't /remove/ surrounding quotes (as IE does
172: // for HTML).
173: //
174: // With these results in hand, we conclude that there are
175: // various levels of safety:
176: // - Paranoid: alphanumeric, spaces and dashes(?)
177: // - International: Paranoid + non-ASCII Unicode
178: // - Edgy: Everything except quotes, backslashes
179: // - NoJS: Standards compliance, e.g. sod IE. Note that
180: // with some judicious character escaping (since certain
181: // types of escaping doesn't work) this is theoretically
182: // OK as long as innerHTML/cssText is not called.
183: // We believe that international is a reasonable default
184: // (that we will implement now), and once we do more
185: // extensive research, we may feel comfortable with dropping
186: // it down to edgy.
187:
188: // Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of
189: // str(c)spn assumes that the string was already well formed
190: // Unicode (which of course it is).
191: if (strspn($font, $this->mask) !== strlen($font)) {
192: continue;
193: }
194:
195: // Historical:
196: // In the absence of innerHTML/cssText, these ugly
197: // transforms don't pose a security risk (as \\ and \"
198: // might--these escapes are not supported by most browsers).
199: // We could try to be clever and use single-quote wrapping
200: // when there is a double quote present, but I have choosen
201: // not to implement that. (NOTE: you can reduce the amount
202: // of escapes by one depending on what quoting style you use)
203: // $font = str_replace('\\', '\\5C ', $font);
204: // $font = str_replace('"', '\\22 ', $font);
205: // $font = str_replace("'", '\\27 ', $font);
206:
207: // font possibly with spaces, requires quoting
208: $final .= "'$font', ";
209: }
210: $final = rtrim($final, ', ');
211: if ($final === '') {
212: return false;
213: }
214: return $final;
215: }
216:
217: }
218:
219: // vim: et sw=4 sts=4
220: