Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   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