HTMLPurifier 4.4.0
/home/ezyang/Dev/htmlpurifier/library/HTMLPurifier/AttrDef/CSS/FontFamily.php
Go to the documentation of this file.
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