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