File indexing completed on 2024-06-16 05:29:43

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