Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
   1  <?php
   2  /**
   3   * The Text_Flowed:: class provides common methods for manipulating text
   4   * using the encoding described in RFC 3676 ('flowed' text).
   5   *
   6   * This class is based on the Text::Flowed perl module (Version 0.14) found
   7   * in the CPAN perl repository.  This module is released under the Perl
   8   * license, which is compatible with the LGPL.
   9   *
  10   * Copyright 2002-2003 Philip Mak
  11   * Copyright 2004-2017 Horde LLC (http://www.horde.org/)
  12   *
  13   * See the enclosed file LICENSE for license information (LGPL). If you
  14   * did not receive this file, see http://www.horde.org/licenses/lgpl21.
  15   *
  16   * @author   Michael Slusarz <slusarz@horde.org>
  17   * @category Horde
  18   * @license  http://www.horde.org/licenses/lgpl21 LGPL 2.1
  19   * @package  Text_Flowed
  20   */
  21  class Horde_Text_Flowed
  22  {
  23      /**
  24       * The maximum length that a line is allowed to be (unless faced with
  25       * with a word that is unreasonably long). This class will re-wrap a
  26       * line if it exceeds this length.
  27       *
  28       * @var integer
  29       */
  30      protected $_maxlength = 78;
  31  
  32      /**
  33       * When this class wraps a line, the newly created lines will be split
  34       * at this length.
  35       *
  36       * @var integer
  37       */
  38      protected $_optlength = 72;
  39  
  40      /**
  41       * The text to be formatted.
  42       *
  43       * @var string
  44       */
  45      protected $_text;
  46  
  47      /**
  48       * The cached output of the formatting.
  49       *
  50       * @var array
  51       */
  52      protected $_output = array();
  53  
  54      /**
  55       * The format of the data in $_output.
  56       *
  57       * @var string
  58       */
  59      protected $_formattype = null;
  60  
  61      /**
  62       * The character set of the text.
  63       *
  64       * @var string
  65       */
  66      protected $_charset;
  67  
  68      /**
  69       * Convert text using DelSp?
  70       *
  71       * @var boolean
  72       */
  73      protected $_delsp = false;
  74  
  75      /**
  76       * Constructor.
  77       *
  78       * @param string $text     The text to process.
  79       * @param string $charset  The character set of $text.
  80       */
  81      public function __construct($text, $charset = 'UTF-8')
  82      {
  83          $this->_text = $text;
  84          $this->_charset = $charset;
  85      }
  86  
  87      /**
  88       * Set the maximum length of a line of text.
  89       *
  90       * @param integer $max  A new value for $_maxlength.
  91       */
  92      public function setMaxLength($max)
  93      {
  94          $this->_maxlength = $max;
  95      }
  96  
  97      /**
  98       * Set the optimal length of a line of text.
  99       *
 100       * @param integer $max  A new value for $_optlength.
 101       */
 102      public function setOptLength($opt)
 103      {
 104          $this->_optlength = $opt;
 105      }
 106  
 107      /**
 108       * Set whether to format text using DelSp.
 109       *
 110       * @param boolean $delsp  Use DelSp?
 111       */
 112      public function setDelSp($delsp)
 113      {
 114          $this->_delsp = (bool)$delsp;
 115      }
 116  
 117      /**
 118       * Reformats the input string, where the string is 'format=flowed' plain
 119       * text as described in RFC 2646.
 120       *
 121       * @param boolean $quote  Add level of quoting to each line?
 122       *
 123       * @return string  The text converted to RFC 2646 'fixed' format.
 124       */
 125      public function toFixed($quote = false)
 126      {
 127          $txt = '';
 128  
 129          $this->_reformat(false, $quote);
 130          $lines = count($this->_output) - 1;
 131          foreach ($this->_output as $no => $line) {
 132              $txt .= $line['text'] . (($lines == $no) ? '' : "\n");
 133          }
 134  
 135          return $txt;
 136      }
 137  
 138      /**
 139       * Reformats the input string, and returns the output in an array format
 140       * with quote level information.
 141       *
 142       * @param boolean $quote  Add level of quoting to each line?
 143       *
 144       * @return array  An array of arrays with the following elements:
 145       * <pre>
 146       * 'level' - The quote level of the current line.
 147       * 'text'  - The text for the current line.
 148       * </pre>
 149       */
 150      public function toFixedArray($quote = false)
 151      {
 152          $this->_reformat(false, $quote);
 153          return $this->_output;
 154      }
 155  
 156      /**
 157       * Reformats the input string, where the string is 'format=fixed' plain
 158       * text as described in RFC 2646.
 159       *
 160       * @param boolean $quote  Add level of quoting to each line?
 161       * @param array $opts     Additional options:
 162       * <pre>
 163       * 'nowrap' - (boolean) If true, does not wrap unquoted lines.
 164       *            DEFAULT: false
 165       * </pre>
 166       *
 167       * @return string  The text converted to RFC 2646 'flowed' format.
 168       */
 169      public function toFlowed($quote = false, array $opts = array())
 170      {
 171          $txt = '';
 172  
 173          $this->_reformat(true, $quote, empty($opts['nowrap']));
 174          foreach ($this->_output as $line) {
 175              $txt .= $line['text'] . "\n";
 176          }
 177  
 178          return $txt;
 179      }
 180  
 181      /**
 182       * Reformats the input string, where the string is 'format=flowed' plain
 183       * text as described in RFC 2646.
 184       *
 185       * @param boolean $toflowed  Convert to flowed?
 186       * @param boolean $quote     Add level of quoting to each line?
 187       * @param boolean $wrap      Wrap unquoted lines?
 188       */
 189      protected function _reformat($toflowed, $quote, $wrap = true)
 190      {
 191          $format_type = implode('|', array($toflowed, $quote));
 192          if ($format_type == $this->_formattype) {
 193              return;
 194          }
 195  
 196          $this->_output = array();
 197          $this->_formattype = $format_type;
 198  
 199          /* Set variables used in regexps. */
 200          $delsp = ($toflowed && $this->_delsp) ? 1 : 0;
 201          $opt = $this->_optlength - 1 - $delsp;
 202  
 203          /* Process message line by line. */
 204          $text = preg_split("/\r?\n/", $this->_text);
 205          $text_count = count($text) - 1;
 206          $skip = 0;
 207  
 208          foreach ($text as $no => $line) {
 209              if ($skip) {
 210                  --$skip;
 211                  continue;
 212              }
 213  
 214              /* Per RFC 2646 [4.3], the 'Usenet Signature Convention' line
 215               * (DASH DASH SP) is not considered flowed.  Watch for this when
 216               * dealing with potentially flowed lines. */
 217  
 218              /* The next three steps come from RFC 2646 [4.2]. */
 219              /* STEP 1: Determine quote level for line. */
 220              if (($num_quotes = $this->_numquotes($line))) {
 221                  $line = substr($line, $num_quotes);
 222              }
 223  
 224              /* Only combine lines if we are converting to flowed or if the
 225               * current line is quoted. */
 226              if (!$toflowed || $num_quotes) {
 227                  /* STEP 2: Remove space stuffing from line. */
 228                  $line = $this->_unstuff($line);
 229  
 230                  /* STEP 3: Should we interpret this line as flowed?
 231                   * While line is flowed (not empty and there is a space
 232                   * at the end of the line), and there is a next line, and the
 233                   * next line has the same quote depth, add to the current
 234                   * line. A line is not flowed if it is a signature line. */
 235                  if ($line != '-- ') {
 236                      while (!empty($line) &&
 237                             (substr($line, -1) == ' ') &&
 238                             ($text_count != $no) &&
 239                             ($this->_numquotes($text[$no + 1]) == $num_quotes)) {
 240                          /* If DelSp is yes and this is flowed input, we need to
 241                           * remove the trailing space. */
 242                          if (!$toflowed && $this->_delsp) {
 243                              $line = substr($line, 0, -1);
 244                          }
 245                          $line .= $this->_unstuff(substr($text[++$no], $num_quotes));
 246                          ++$skip;
 247                      }
 248                  }
 249              }
 250  
 251              /* Ensure line is fixed, since we already joined all flowed
 252               * lines. Remove all trailing ' ' from the line. */
 253              if ($line != '-- ') {
 254                  $line = rtrim($line);
 255              }
 256  
 257              /* Increment quote depth if we're quoting. */
 258              if ($quote) {
 259                  $num_quotes++;
 260              }
 261  
 262              /* The quote prefix for the line. */
 263              $quotestr = str_repeat('>', $num_quotes);
 264  
 265              if (empty($line)) {
 266                  /* Line is empty. */
 267                  $this->_output[] = array('text' => $quotestr, 'level' => $num_quotes);
 268              } elseif ((!$wrap && !$num_quotes) ||
 269                        empty($this->_maxlength) ||
 270                        ((Horde_String::length($line, $this->_charset) + $num_quotes) <= $this->_maxlength)) {
 271                  /* Line does not require rewrapping. */
 272                  $this->_output[] = array('text' => $quotestr . $this->_stuff($line, $num_quotes, $toflowed), 'level' => $num_quotes);
 273              } else {
 274                  $min = $num_quotes + 1;
 275  
 276                  /* Rewrap this paragraph. */
 277                  while ($line) {
 278                      /* Stuff and re-quote the line. */
 279                      $line = $quotestr . $this->_stuff($line, $num_quotes, $toflowed);
 280                      $line_length = Horde_String::length($line, $this->_charset);
 281                      if ($line_length <= $this->_optlength) {
 282                          /* Remaining section of line is short enough. */
 283                          $this->_output[] = array('text' => $line, 'level' => $num_quotes);
 284                          break;
 285                      } else {
 286                          $regex = array();
 287                          if ($min <= $opt) {
 288                              $regex[] = '^(.{' . $min . ',' . $opt . '}) (.*)';
 289                          }
 290                          if ($min <= $this->_maxlength) {
 291                              $regex[] = '^(.{' . $min . ',' . $this->_maxlength . '}) (.*)';
 292                          }
 293                          $regex[] = '^(.{' . $min . ',})? (.*)';
 294  
 295                          if ($m = Horde_String::regexMatch($line, $regex, $this->_charset)) {
 296                              /* We need to wrap text at a certain number of
 297                               * *characters*, not a certain number of *bytes*;
 298                               * thus the need for a multibyte capable regex.
 299                               * If a multibyte regex isn't available, we are
 300                               * stuck with preg_match() (the function will
 301                               * still work - are just left with shorter rows
 302                               * than expected if multibyte characters exist in
 303                               * the row).
 304                               *
 305                               * 1. Try to find a string as long as _optlength.
 306                               * 2. Try to find a string as long as _maxlength.
 307                               * 3. Take the first word. */
 308                              if (empty($m[1])) {
 309                                  $m[1] = $m[2];
 310                                  $m[2] = '';
 311                              }
 312                              $this->_output[] = array('text' => $m[1] . ' ' . (($delsp) ? ' ' : ''), 'level' => $num_quotes);
 313                              $line = $m[2];
 314                          } elseif ($line_length > 998) {
 315                              /* One excessively long word left on line.  Be
 316                               * absolutely sure it does not exceed 998
 317                               * characters in length or else we must
 318                               * truncate. */
 319                              $this->_output[] = array('text' => Horde_String::substr($line, 0, 998, $this->_charset), 'level' => $num_quotes);
 320                              $line = Horde_String::substr($line, 998, null, $this->_charset);
 321                          } else {
 322                              $this->_output[] = array('text' => $line, 'level' => $num_quotes);
 323                              break;
 324                          }
 325                      }
 326                  }
 327              }
 328          }
 329      }
 330  
 331      /**
 332       * Returns the number of leading '>' characters in the text input.
 333       * '>' characters are defined by RFC 2646 to indicate a quoted line.
 334       *
 335       * @param string $text  The text to analyze.
 336       *
 337       * @return integer  The number of leading quote characters.
 338       */
 339      protected function _numquotes($text)
 340      {
 341          return strspn($text, '>');
 342      }
 343  
 344      /**
 345       * Space-stuffs if it starts with ' ' or '>' or 'From ', or if
 346       * quote depth is non-zero (for aesthetic reasons so that there is a
 347       * space after the '>').
 348       *
 349       * @param string $text        The text to stuff.
 350       * @param string $num_quotes  The quote-level of this line.
 351       * @param boolean $toflowed   Are we converting to flowed text?
 352       *
 353       * @return string  The stuffed text.
 354       */
 355      protected function _stuff($text, $num_quotes, $toflowed)
 356      {
 357          return ($toflowed && ($num_quotes || preg_match("/^(?: |>|From |From$)/", $text)))
 358              ? ' ' . $text
 359              : $text;
 360      }
 361  
 362      /**
 363       * Unstuffs a space stuffed line.
 364       *
 365       * @param string $text  The text to unstuff.
 366       *
 367       * @return string  The unstuffed text.
 368       */
 369      protected function _unstuff($text)
 370      {
 371          return (!empty($text) && ($text[0] == ' '))
 372              ? substr($text, 1)
 373              : $text;
 374      }
 375  
 376  }