Import Text_Flowed from CVS HEAD
authorMichael M Slusarz <slusarz@curecanti.org>
Mon, 23 Feb 2009 05:31:10 +0000 (22:31 -0700)
committerMichael M Slusarz <slusarz@curecanti.org>
Mon, 23 Feb 2009 05:31:10 +0000 (22:31 -0700)
framework/Mime/lib/Horde/Mime/Mail.php
framework/Mime/lib/Horde/Mime/Mdn.php
framework/Mime/lib/Horde/Mime/Viewer/plain.php
framework/Text_Flowed/lib/Horde/Text/Flowed.php [new file with mode: 0644]
framework/Text_Flowed/package.xml [new file with mode: 0644]
framework/Text_Flowed/test/Horde/Text/Flowed.phpt [new file with mode: 0644]

index c0c0037..9fa7f79 100644 (file)
@@ -412,9 +412,7 @@ class Horde_Mime_Mail
 
         /* Send in flowed format. */
         if ($flowed && !empty($this->_body)) {
-            require_once 'Text/Flowed.php';
-            $flowed = new Text_Flowed($this->_body->getContents(),
-                                      $this->_body->getCharset());
+            $flowed = new Horde_Text_Flowed($this->_body->getContents(), $this->_body->getCharset());
             $flowed->setDelSp(true);
             $this->_body->setContentTypeParameter('format', 'flowed');
             $this->_body->setContentTypeParameter('DelSp', 'Yes');
index a86f93f..54ecc9a 100644 (file)
@@ -196,8 +196,7 @@ class Horde_Mime_Mdn
         $part_one->setCharset($charset);
         if ($type == 'displayed') {
             $contents = sprintf(_("The message sent on %s to %s with subject \"%s\" has been displayed.\n\nThis is no guarantee that the message has been read or understood."), $this->_headers->getValue('Date'), $this->_headers->getValue('To'), $this->_headers->getValue('Subject'));
-            require_once 'Text/Flowed.php';
-            $flowed = new Text_Flowed($contents, $charset);
+            $flowed = new Horde_Text_Flowed($contents, $charset);
             $flowed->setDelSp(true);
             $part_one->setContentTypeParameter('format', 'flowed');
             $part_one->setContentTypeParameter('DelSp', 'Yes');
index e50bbc9..138e92b 100644 (file)
@@ -85,8 +85,7 @@ class Horde_Mime_Viewer_plain extends Horde_Mime_Viewer_Driver
      */
     protected function _formatFlowed($text, $delsp = null)
     {
-        require_once 'Text/Flowed.php';
-        $flowed = new Text_Flowed($this->_mimepart->replaceEOL($text, "\n"), $this->_mimepart->getCharset());
+        $flowed = new Horde_Text_Flowed($this->_mimepart->replaceEOL($text, "\n"), $this->_mimepart->getCharset());
         $flowed->setMaxLength(0);
         if (!is_null($delsp)) {
             $flowed->setDelSp($delsp);
diff --git a/framework/Text_Flowed/lib/Horde/Text/Flowed.php b/framework/Text_Flowed/lib/Horde/Text/Flowed.php
new file mode 100644 (file)
index 0000000..36f6994
--- /dev/null
@@ -0,0 +1,361 @@
+<?php
+/**
+ * The Text_Flowed:: class provides common methods for manipulating text
+ * using the encoding described in RFC 3676 ('flowed' text).
+ *
+ * This class is based on the Text::Flowed perl module (Version 0.14) found
+ * in the CPAN perl repository.  This module is released under the Perl
+ * license, which is compatible with the LGPL.
+ *
+ * Copyright 2002-2003 Philip Mak
+ * Copyright 2004-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author  Michael Slusarz <slusarz@horde.org>
+ * @package Horde_Text
+ */
+class Horde_Text_Flowed
+{
+    /**
+     * The maximum length that a line is allowed to be (unless faced with
+     * with a word that is unreasonably long). This class will re-wrap a
+     * line if it exceeds this length.
+     *
+     * @var integer
+     */
+    protected $_maxlength = 78;
+
+    /**
+     * When this class wraps a line, the newly created lines will be split
+     * at this length.
+     *
+     * @var integer
+     */
+    protected $_optlength = 72;
+
+    /**
+     * The text to be formatted.
+     *
+     * @var string
+     */
+    protected $_text;
+
+    /**
+     * The cached output of the formatting.
+     *
+     * @var array
+     */
+    protected $_output = array();
+
+    /**
+     * The format of the data in $_output.
+     *
+     * @var string
+     */
+    protected $_formattype = null;
+
+    /**
+     * The character set of the text.
+     *
+     * @var string
+     */
+    protected $_charset;
+
+    /**
+     * Convert text using DelSp?
+     *
+     * @var boolean
+     */
+    protected $_delsp = false;
+
+    /**
+     * Constructor.
+     *
+     * @param string $text     The text to process.
+     * @param string $charset  The character set of $text.
+     */
+    public function __construct($text, $charset = null)
+    {
+        $this->_text = $text;
+        $this->_charset = $charset;
+    }
+
+    /**
+     * Set the maximum length of a line of text.
+     *
+     * @param integer $max  A new value for $_maxlength.
+     */
+    public function setMaxLength($max)
+    {
+        $this->_maxlength = $max;
+    }
+
+    /**
+     * Set the optimal length of a line of text.
+     *
+     * @param integer $max  A new value for $_optlength.
+     */
+    public function setOptLength($opt)
+    {
+        $this->_optlength = $opt;
+    }
+
+    /**
+     * Set whether to format test using DelSp.
+     *
+     * @param boolean $delsp  Use DelSp?
+     */
+    public function setDelSp($delsp)
+    {
+        $this->_delsp = (bool) $delsp;
+    }
+
+    /**
+     * Reformats the input string, where the string is 'format=flowed' plain
+     * text as described in RFC 2646.
+     *
+     * @param boolean $quote  Add level of quoting to each line?
+     *
+     * @return string  The text converted to RFC 2646 'fixed' format.
+     */
+    public function toFixed($quote = false)
+    {
+        $txt = '';
+
+        $this->_reformat(false, $quote);
+        reset($this->_output);
+        $lines = count($this->_output) - 1;
+        while (list($no, $line) = each($this->_output)) {
+            $txt .= $line['text'] . (($lines == $no) ? '' : "\n");
+        }
+
+        return $txt;
+    }
+
+    /**
+     * Reformats the input string, and returns the output in an array format
+     * with quote level information.
+     *
+     * @param boolean $quote  Add level of quoting to each line?
+     *
+     * @return array  An array of arrays with the following elements:
+     * <pre>
+     * 'level' - The quote level of the current line.
+     * 'text'  - The text for the current line.
+     * </pre>
+     */
+    public function toFixedArray($quote = false)
+    {
+        $this->_reformat(false, $quote);
+        return $this->_output;
+    }
+
+    /**
+     * Reformats the input string, where the string is 'format=fixed' plain
+     * text as described in RFC 2646.
+     *
+     * @param boolean $quote  Add level of quoting to each line?
+     *
+     * @return string  The text converted to RFC 2646 'flowed' format.
+     */
+    public function toFlowed($quote = false)
+    {
+        $txt = '';
+
+        $this->_reformat(true, $quote);
+        reset($this->_output);
+        while (list(,$line) = each($this->_output)) {
+            $txt .= $line['text'] . "\n";
+        }
+
+        return $txt;
+    }
+
+    /**
+     * Reformats the input string, where the string is 'format=flowed' plain
+     * text as described in RFC 2646.
+     *
+     * @param boolean $toflowed  Convert to flowed?
+     * @param boolean $quote     Add level of quoting to each line?
+     */
+    protected function _reformat($toflowed, $quote)
+    {
+        $format_type = implode('|', array($toflowed, $quote));
+        if ($format_type == $this->_formattype) {
+            return;
+        }
+
+        $this->_output = array();
+        $this->_formattype = $format_type;
+
+        /* Set variables used in regexps. */
+        $delsp = ($toflowed && $this->_delsp) ? 1 : 0;
+        $opt = $this->_optlength - 1 - $delsp;
+
+        /* Process message line by line. */
+        $text = explode("\n", $this->_text);
+        $text_count = count($text) - 1;
+        $skip = 0;
+        reset($text);
+
+        while (list($no, $line) = each($text)) {
+            if ($skip) {
+                --$skip;
+                continue;
+            }
+
+            /* Per RFC 2646 [4.3], the 'Usenet Signature Convention' line
+             * (DASH DASH SP) is not considered flowed.  Watch for this when
+             * dealing with potentially flowed lines. */
+
+            /* The next three steps come from RFC 2646 [4.2]. */
+            /* STEP 1: Determine quote level for line. */
+            if (($num_quotes = $this->_numquotes($line))) {
+                $line = substr($line, $num_quotes);
+            }
+
+            /* Only combine lines if we are converting to flowed or if the
+             * current line is quoted. */
+            if (!$toflowed || $num_quotes) {
+                /* STEP 2: Remove space stuffing from line. */
+                $line = $this->_unstuff($line);
+
+                /* STEP 3: Should we interpret this line as flowed?
+                 * While line is flowed (not empty and there is a space
+                 * at the end of the line), and there is a next line, and the
+                 * next line has the same quote depth, add to the current
+                 * line. A line is not flowed if it is a signature line. */
+                if ($line != '-- ') {
+                    while (!empty($line) &&
+                           ($line[strlen($line) - 1] == ' ') &&
+                           ($text_count != $no) &&
+                           ($this->_numquotes($text[$no + 1]) == $num_quotes)) {
+                        /* If DelSp is yes and this is flowed input, we need to
+                         * remove the trailing space. */
+                        if (!$toflowed && $this->_delsp) {
+                            $line = substr($line, 0, -1);
+                        }
+                        $line .= $this->_unstuff(substr($text[++$no], $num_quotes));
+                        ++$skip;
+                    }
+                }
+            }
+
+            /* Ensure line is fixed, since we already joined all flowed
+             * lines. Remove all trailing ' ' from the line. */
+            if ($line != '-- ') {
+                $line = rtrim($line);
+            }
+
+            /* Increment quote depth if we're quoting. */
+            if ($quote) {
+                $num_quotes++;
+            }
+
+            /* The quote prefix for the line. */
+            $quotestr = str_repeat('>', $num_quotes);
+
+            if (empty($line)) {
+                /* Line is empty. */
+                $this->_output[] = array('text' => $quotestr, 'level' => $num_quotes);
+            } elseif (empty($this->_maxlength) || ((String::length($line, $this->_charset) + $num_quotes) <= $this->_maxlength)) {
+                /* Line does not require rewrapping. */
+                $this->_output[] = array('text' => $quotestr . $this->_stuff($line, $num_quotes, $toflowed), 'level' => $num_quotes);
+            } else {
+                $min = $num_quotes + 1;
+
+                /* Rewrap this paragraph. */
+                while ($line) {
+                    /* Stuff and re-quote the line. */
+                    $line = $quotestr . $this->_stuff($line, $num_quotes, $toflowed);
+                    $line_length = String::length($line, $this->_charset);
+                    if ($line_length <= $this->_optlength) {
+                        /* Remaining section of line is short enough. */
+                        $this->_output[] = array('text' => $line, 'level' => $num_quotes);
+                        break;
+                    } elseif ($m = String::regexMatch($line, array('^(.{' . $min . ',' . $opt . '}) (.*)', '^(.{' . $min . ',' . $this->_maxlength . '}) (.*)', '^(.{' . $min . ',})? (.*)'), $this->_charset)) {
+                        /* We need to wrap text at a certain number of
+                         * *characters*, not a certain number of *bytes*;
+                         * thus the need for a multibyte capable regex.
+                         * If a multibyte regex isn't available, we are stuck
+                         * with preg_match() (the function will still work -
+                         * we will just be left with shorter rows than expected
+                         * if multibyte characters exist in the row).
+                         *
+                         * Algorithim:
+                         * 1. Try to find a string as long as _optlength.
+                         * 2. Try to find a string as long as _maxlength.
+                         * 3. Take the first word. */
+                        if (empty($m[1])) {
+                            $m[1] = $m[2];
+                            $m[2] = '';
+                        }
+                        $this->_output[] = array('text' => $m[1] . ' ' . (($delsp) ? ' ' : ''), 'level' => $num_quotes);
+                        $line = $m[2];
+                    } else {
+                        /* One excessively long word left on line.  Be
+                         * absolutely sure it does not exceed 998 characters
+                         * in length or else we must truncate. */
+                        if ($line_length > 998) {
+                            $this->_output[] = array('text' => String::substr($line, 0, 998, $this->_charset), 'level' => $num_quotes);
+                            $line = String::substr($line, 998, null, $this->_charset);
+                        } else {
+                            $this->_output[] = array('text' => $line, 'level' => $num_quotes);
+                            break;
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the number of leading '>' characters in the text input.
+     * '>' characters are defined by RFC 2646 to indicate a quoted line.
+     *
+     * @param string $text  The text to analyze.
+     *
+     * @return integer  The number of leading quote characters.
+     */
+    protected function _numquotes($text)
+    {
+        return strspn($text, '>');
+    }
+
+    /**
+     * Space-stuffs if it starts with ' ' or '>' or 'From ', or if
+     * quote depth is non-zero (for aesthetic reasons so that there is a
+     * space after the '>').
+     *
+     * @param string $text        The text to stuff.
+     * @param string $num_quotes  The quote-level of this line.
+     * @param boolean $toflowed   Are we converting to flowed text?
+     *
+     * @return string  The stuffed text.
+     */
+    protected function _stuff($text, $num_quotes, $toflowed)
+    {
+        return ($toflowed && ($num_quotes || preg_match("/^(?: |>|From |From$)/", $text))
+            ? '' . $text
+            : $text;
+    }
+
+    /**
+     * Unstuffs a space stuffed line.
+     *
+     * @param string $text  The text to unstuff.
+     *
+     * @return string  The unstuffed text.
+     */
+    protected function _unstuff($text)
+    {
+        if (!empty($text) && ($text[0] == ' ')) {
+            $text = substr($text, 1);
+        }
+
+        return $text;
+    }
+
+}
diff --git a/framework/Text_Flowed/package.xml b/framework/Text_Flowed/package.xml
new file mode 100644 (file)
index 0000000..359ccbc
--- /dev/null
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>Text_Flowed</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde API for flowed text as per RFC 3676</summary>
+ <description>The Horde_Text_Flowed:: class provides common methods for
+ manipulating text using the encoding described in RFC 3676
+ (&apos;flowed&apos; text).
+ </description>
+ <lead>
+  <name>Michael Slusarz</name>
+  <user>slusarz</user>
+  <email>slusarz@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2009-02-22</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>beta</release>
+  <api>beta</api>
+ </stability>
+ <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+ <notes>* Initial Horde 4 package.
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="test">
+    <dir name="Horde">
+     <dir name="Text">
+      <file name="Flowed.phpt" role="test" />
+     </dir> <!-- /test/Horde/Text -->
+    </dir> <!-- /test/Horde -->
+   </dir> <!-- /test -->
+   <dir name="lib">
+    <dir name="Horde">
+     <dir name="Text">
+     </dir> <!-- /lib/Horde/Text -->
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+   <file name="Flowed.php" role="php" />
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.0</min>
+   </pearinstaller>
+   <package>
+    <name>Util</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/Text/Flowed.php" as="Horde/Text/Flowed.php" />
+  </filelist>
+ </phprelease>
+ <changelog>
+  <release>
+   <date>2006-05-08</date>
+   <time>23:41:38</time>
+   <version>
+    <release>0.0.2</release>
+    <api>0.0.2</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Converted to package.xml 2.0 for pear.horde.org
+   </notes>
+  </release>
+  <release>
+   <version>
+    <release>0.0.1</release>
+    <api>0.0.1</api>
+   </version>
+   <stability>
+    <release>alpha</release>
+    <api>alpha</api>
+   </stability>
+   <date>2004-10-15</date>
+   <license uri="http://www.gnu.org/copyleft/lesser.html">LGPL</license>
+   <notes>Initial release as a PEAR package
+   </notes>
+  </release>
+ </changelog>
+</package>
diff --git a/framework/Text_Flowed/test/Horde/Text/Flowed.phpt b/framework/Text_Flowed/test/Horde/Text/Flowed.phpt
new file mode 100644 (file)
index 0000000..9d35022
--- /dev/null
@@ -0,0 +1,68 @@
+--TEST--
+Horde_Text_Flowed:: tests
+--FILE--
+<?php
+
+require_once 'Horde/Util.php';
+require_once 'Horde/String.php';
+require_once 'Horde/Text/Flowed.php';
+
+echo "[FIXED -> FLOWED]\n";
+
+$flowed = new Horde_Text_Flowed("Hello, world!");
+echo $flowed->toFlowed() . "\n";
+
+$flowed = new Horde_Text_Flowed("Hello, \nworld!");
+echo $flowed->toFlowed() . "\n";
+
+$flowed = new Horde_Text_Flowed("Hello, \n world!");
+echo $flowed->toFlowed() . "\n";
+
+$flowed = new Horde_Text_Flowed("From");
+echo $flowed->toFlowed() . "\n";
+
+// See Bug #2969
+$flowed = new Horde_Text_Flowed("   >--------------------------------------------------------------------------------------------------------------------------------");
+echo $flowed->toFlowed() . "\n";
+
+echo "[FLOWED -> FIXED]\n";
+
+$flowed = new Horde_Text_Flowed(">line 1 \n>line 2 \n>line 3");
+echo $flowed->toFixed() . "\n\n";
+$flowed = new Horde_Text_Flowed(">line 1 \n>line 2 \n>line 3");
+echo $flowed->toFixed() . "\n\n";
+
+// See Bug #4832
+$flowed = new Horde_Text_Flowed("line 1\n>from line 2\nline 3");
+echo $flowed->toFixed() . "\n\n";
+$flowed = new Horde_Text_Flowed("line 1\n From line 2\nline 3");
+echo $flowed->toFixed() . "\n";
+
+?>
+--EXPECT--
+[FIXED -> FLOWED]
+Hello, world!
+
+Hello,
+world!
+
+Hello,
+  world!
+
+ From
+
+    
+>-------------------------------------------------------------------------------------------------------------------------------- 
+
+[FLOWED -> FIXED]
+>line 1 line 2 line 3
+
+>line 1 line 2 line 3
+
+line 1
+>from line 2
+line 3
+
+line 1
+From line 2
+line 3