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   * Copyright 1999-2017 Horde LLC (http://www.horde.org/)
   4   *
   5   * See the enclosed file LICENSE for license information (LGPL). If you
   6   * did not receive this file, see http://www.horde.org/licenses/lgpl21.
   7   *
   8   * @category  Horde
   9   * @copyright 1999-2017 Horde LLC
  10   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  11   * @package   Mime
  12   */
  13  
  14  /**
  15   * Provide methods for dealing with MIME encoding (RFC 2045-2049);
  16   *
  17   * @author    Chuck Hagenbuch <chuck@horde.org>
  18   * @author    Michael Slusarz <slusarz@horde.org>
  19   * @category  Horde
  20   * @copyright 1999-2017 Horde LLC
  21   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  22   * @package   Mime
  23   */
  24  class Horde_Mime
  25  {
  26      /**
  27       * The RFC defined EOL string.
  28       *
  29       * @var string
  30       */
  31      const EOL = "\r\n";
  32  
  33      /**
  34       * Use windows-1252 charset when decoding ISO-8859-1 data?
  35       * HTML 5 requires this behavior, so it is the default.
  36       *
  37       * @var boolean
  38       */
  39      public static $decodeWindows1252 = true;
  40  
  41      /**
  42       * Determines if a string contains 8-bit (non US-ASCII) characters.
  43       *
  44       * @param string $string   The string to check.
  45       * @param string $charset  The charset of the string. Defaults to
  46       *                         US-ASCII. (@deprecated)
  47       *
  48       * @return boolean  True if string contains non 7-bit characters.
  49       */
  50      public static function is8bit($string, $charset = null)
  51      {
  52          $string = strval($string);
  53          for ($i = 0, $len = strlen($string); $i < $len; ++$i) {
  54              if (ord($string[$i]) > 127) {
  55                  return true;
  56              }
  57          }
  58  
  59          return false;
  60      }
  61  
  62      /**
  63       * MIME encodes a string (RFC 2047).
  64       *
  65       * @param string $text     The text to encode (UTF-8).
  66       * @param string $charset  The character set to encode to.
  67       *
  68       * @return string  The MIME encoded string (US-ASCII).
  69       */
  70      public static function encode($text, $charset = 'UTF-8')
  71      {
  72          $charset = Horde_String::lower($charset);
  73          $text = Horde_String::convertCharset($text, 'UTF-8', $charset);
  74  
  75          $encoded = $is_encoded = false;
  76          $lwsp = $word = null;
  77          $out = '';
  78  
  79          /* 0 = word unencoded
  80           * 1 = word encoded
  81           * 2 = spaces */
  82          $parts = array();
  83  
  84          /* Tokenize string. */
  85          for ($i = 0, $len = strlen($text); $i < $len; ++$i) {
  86              switch ($text[$i]) {
  87              case "\t":
  88              case "\r":
  89              case "\n":
  90                  if (!is_null($word)) {
  91                      $parts[] = array(intval($encoded), $word, $i - $word);
  92                      $word = null;
  93                  } elseif (!is_null($lwsp)) {
  94                      $parts[] = array(2, $lwsp, $i - $lwsp);
  95                      $lwsp = null;
  96                  }
  97  
  98                  $parts[] = array(0, $i, 1);
  99                  break;
 100  
 101              case ' ':
 102                  if (!is_null($word)) {
 103                      $parts[] = array(intval($encoded), $word, $i - $word);
 104                      $word = null;
 105                  }
 106                  if (is_null($lwsp)) {
 107                      $lwsp = $i;
 108                  }
 109                  break;
 110  
 111              default:
 112                  if (is_null($word)) {
 113                      $encoded = false;
 114                      $word = $i;
 115                      if (!is_null($lwsp)) {
 116                          $parts[] = array(2, $lwsp, $i - $lwsp);
 117                          $lwsp = null;
 118                      }
 119  
 120                      /* Check for MIME encoding delimiter. Encode it if
 121                       * found. */
 122                      if (($text[$i] === '=') &&
 123                          (($i + 1) < $len) &&
 124                          ($text[$i +1] === '?')) {
 125                          ++$i;
 126                          $encoded = $is_encoded = true;
 127                      }
 128                  }
 129  
 130                  /* Check for 8-bit characters or control characters. */
 131                  if (!$encoded) {
 132                      $c = ord($text[$i]);
 133                      if ($encoded = (($c & 0x80) || ($c < 32))) {
 134                          $is_encoded = true;
 135                      }
 136                  }
 137                  break;
 138              }
 139          }
 140  
 141          if (!$is_encoded) {
 142              return $text;
 143          }
 144  
 145          if (is_null($lwsp)) {
 146              $parts[] = array(intval($encoded), $word, $len);
 147          } else {
 148              $parts[] = array(2, $lwsp, $len);
 149          }
 150  
 151          /* Combine parts into MIME encoded string. */
 152          for ($i = 0, $cnt = count($parts); $i < $cnt; ++$i) {
 153              $val = $parts[$i];
 154  
 155              switch ($val[0]) {
 156              case 0:
 157              case 2:
 158                  $out .= substr($text, $val[1], $val[2]);
 159                  break;
 160  
 161              case 1:
 162                  $j = $i;
 163                  for ($k = $i + 1; $k < $cnt; ++$k) {
 164                      switch ($parts[$k][0]) {
 165                      case 0:
 166                          break 2;
 167  
 168                      case 1:
 169                          $i = $k;
 170                          break;
 171                      }
 172                  }
 173  
 174                  $encode = '';
 175                  for (; $j <= $i; ++$j) {
 176                      $encode .= substr($text, $parts[$j][1], $parts[$j][2]);
 177                  }
 178  
 179                  $delim = '=?' . $charset . '?b?';
 180                  $e_parts = explode(
 181                      self::EOL,
 182                      rtrim(
 183                          chunk_split(
 184                              base64_encode($encode),
 185                              /* strlen($delim) + 2 = space taken by MIME
 186                               * delimiter */
 187                              intval((75 - strlen($delim) + 2) / 4) * 4
 188                          )
 189                      )
 190                  );
 191  
 192                  $tmp = array();
 193                  foreach ($e_parts as $val) {
 194                      $tmp[] = $delim . $val . '?=';
 195                  }
 196  
 197                  $out .= implode(' ', $tmp);
 198                  break;
 199              }
 200          }
 201  
 202          return rtrim($out);
 203      }
 204  
 205      /**
 206       * Decodes a MIME encoded (RFC 2047) string.
 207       *
 208       * @param string $string  The MIME encoded text.
 209       *
 210       * @return string  The decoded text.
 211       */
 212      public static function decode($string)
 213      {
 214          $old_pos = 0;
 215          $out = '';
 216  
 217          while (($pos = strpos($string, '=?', $old_pos)) !== false) {
 218              /* Save any preceding text, if it is not LWSP between two
 219               * encoded words. */
 220              $pre = substr($string, $old_pos, $pos - $old_pos);
 221              if (!$old_pos ||
 222                  (strspn($pre, " \t\n\r") != strlen($pre))) {
 223                  $out .= $pre;
 224              }
 225  
 226              /* Search for first delimiting question mark (charset). */
 227              if (($d1 = strpos($string, '?', $pos + 2)) === false) {
 228                  break;
 229              }
 230  
 231              $orig_charset = substr($string, $pos + 2, $d1 - $pos - 2);
 232              if (self::$decodeWindows1252 &&
 233                  (Horde_String::lower($orig_charset) == 'iso-8859-1')) {
 234                  $orig_charset = 'windows-1252';
 235              }
 236  
 237              /* Search for second delimiting question mark (encoding). */
 238              if (($d2 = strpos($string, '?', $d1 + 1)) === false) {
 239                  break;
 240              }
 241  
 242              $encoding = substr($string, $d1 + 1, $d2 - $d1 - 1);
 243  
 244              /* Search for end of encoded data. */
 245              if (($end = strpos($string, '?=', $d2 + 1)) === false) {
 246                  break;
 247              }
 248  
 249              $encoded_text = substr($string, $d2 + 1, $end - $d2 - 1);
 250  
 251              switch ($encoding) {
 252              case 'Q':
 253              case 'q':
 254                  $out .= Horde_String::convertCharset(
 255                      quoted_printable_decode(
 256                          str_replace('_', ' ', $encoded_text)
 257                      ),
 258                      $orig_charset,
 259                      'UTF-8'
 260                  );
 261              break;
 262  
 263              case 'B':
 264              case 'b':
 265                  $out .= Horde_String::convertCharset(
 266                      base64_decode($encoded_text),
 267                      $orig_charset,
 268                      'UTF-8'
 269                  );
 270              break;
 271  
 272              default:
 273                  // Ignore unknown encoding.
 274                  break;
 275              }
 276  
 277              $old_pos = $end + 2;
 278          }
 279  
 280          return $out . substr($string, $old_pos);
 281      }
 282  
 283      /* Deprecated methods. */
 284  
 285      /**
 286       * @deprecated  Use Horde_Mime_Headers_MessageId::create() instead.
 287       */
 288      public static function generateMessageId()
 289      {
 290          return Horde_Mime_Headers_MessageId::create()->value;
 291      }
 292  
 293      /**
 294       * @deprecated  Use Horde_Mime_Uudecode instead.
 295       */
 296      public static function uudecode($input)
 297      {
 298          $uudecode = new Horde_Mime_Uudecode($input);
 299          return iterator_to_array($uudecode);
 300      }
 301  
 302      /**
 303       * @deprecated
 304       */
 305      public static $brokenRFC2231 = false;
 306  
 307      /**
 308       * @deprecated
 309       */
 310      const MIME_PARAM_QUOTED = '/[\x01-\x20\x22\x28\x29\x2c\x2f\x3a-\x40\x5b-\x5d]/';
 311  
 312      /**
 313       * @deprecated  Use Horde_Mime_Headers_ContentParam#encode() instead.
 314       */
 315      public static function encodeParam($name, $val, array $opts = array())
 316      {
 317          $cp = new Horde_Mime_Headers_ContentParam(
 318              'UNUSED',
 319              array($name => $val)
 320          );
 321  
 322          return $cp->encode(array_merge(array(
 323              'broken_rfc2231' => self::$brokenRFC2231
 324          ), $opts));
 325      }
 326  
 327      /**
 328       * @deprecated  Use Horde_Mime_Headers_ELement_ContentParam instead.
 329       */
 330      public static function decodeParam($type, $data)
 331      {
 332          $cp = new Horde_Mime_Headers_ContentParam(
 333              'UNUSED',
 334              $data
 335          );
 336  
 337          if (strlen($cp->value)) {
 338              $val = $cp->value;
 339          } else {
 340              $val = (Horde_String::lower($type) == 'content-type')
 341                  ? 'text/plain'
 342                  : 'attachment';
 343          }
 344  
 345          return array(
 346              'params' => $cp->params,
 347              'val' => $val
 348          );
 349      }
 350  
 351      /**
 352       * @deprecated  Use Horde_Mime_Id instead.
 353       */
 354      public static function mimeIdArithmetic($id, $action, $options = array())
 355      {
 356          $id_ob = new Horde_Mime_Id($id);
 357  
 358          switch ($action) {
 359          case 'down':
 360              $action = $id_ob::ID_DOWN;
 361              break;
 362  
 363          case 'next':
 364              $action = $id_ob::ID_NEXT;
 365              break;
 366  
 367          case 'prev':
 368              $action = $id_ob::ID_PREV;
 369              break;
 370  
 371          case 'up':
 372              $action = $id_ob::ID_UP;
 373              break;
 374          }
 375  
 376          return $id_ob->idArithmetic($action, $options);
 377      }
 378  
 379      /**
 380       * @deprecated  Use Horde_Mime_Id instead.
 381       */
 382      public static function isChild($base, $id)
 383      {
 384          $id_ob = new Horde_Mime_Id($base);
 385          return $id_ob->isChild($id);
 386      }
 387  
 388      /**
 389       * @deprecated  Use Horde_Mime_QuotedPrintable instead.
 390       */
 391      public static function quotedPrintableEncode($text, $eol = self::EOL,
 392                                                   $wrap = 76)
 393      {
 394          return Horde_Mime_QuotedPrintable::encode($text, $eol, $wrap);
 395      }
 396  
 397  }