From 24a5df3a160dcd13263e48792bf45af51d81826e Mon Sep 17 00:00:00 2001 From: Michael M Slusarz Date: Sun, 22 Feb 2009 22:31:10 -0700 Subject: [PATCH] Import Text_Flowed from CVS HEAD --- framework/Mime/lib/Horde/Mime/Mail.php | 4 +- framework/Mime/lib/Horde/Mime/Mdn.php | 3 +- framework/Mime/lib/Horde/Mime/Viewer/plain.php | 3 +- framework/Text_Flowed/lib/Horde/Text/Flowed.php | 361 ++++++++++++++++++++++ framework/Text_Flowed/package.xml | 99 ++++++ framework/Text_Flowed/test/Horde/Text/Flowed.phpt | 68 ++++ 6 files changed, 531 insertions(+), 7 deletions(-) create mode 100644 framework/Text_Flowed/lib/Horde/Text/Flowed.php create mode 100644 framework/Text_Flowed/package.xml create mode 100644 framework/Text_Flowed/test/Horde/Text/Flowed.phpt diff --git a/framework/Mime/lib/Horde/Mime/Mail.php b/framework/Mime/lib/Horde/Mime/Mail.php index c0c00371e..9fa7f793d 100644 --- a/framework/Mime/lib/Horde/Mime/Mail.php +++ b/framework/Mime/lib/Horde/Mime/Mail.php @@ -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'); diff --git a/framework/Mime/lib/Horde/Mime/Mdn.php b/framework/Mime/lib/Horde/Mime/Mdn.php index a86f93ff7..54ecc9a22 100644 --- a/framework/Mime/lib/Horde/Mime/Mdn.php +++ b/framework/Mime/lib/Horde/Mime/Mdn.php @@ -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'); diff --git a/framework/Mime/lib/Horde/Mime/Viewer/plain.php b/framework/Mime/lib/Horde/Mime/Viewer/plain.php index e50bbc9bd..138e92baf 100644 --- a/framework/Mime/lib/Horde/Mime/Viewer/plain.php +++ b/framework/Mime/lib/Horde/Mime/Viewer/plain.php @@ -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 index 000000000..36f699454 --- /dev/null +++ b/framework/Text_Flowed/lib/Horde/Text/Flowed.php @@ -0,0 +1,361 @@ + + * @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: + *
+     * 'level' - The quote level of the current line.
+     * 'text'  - The text for the current line.
+     * 
+ */ + 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 index 000000000..359ccbc0c --- /dev/null +++ b/framework/Text_Flowed/package.xml @@ -0,0 +1,99 @@ + + + Text_Flowed + pear.horde.org + Horde API for flowed text as per RFC 3676 + The Horde_Text_Flowed:: class provides common methods for + manipulating text using the encoding described in RFC 3676 + ('flowed' text). + + + Michael Slusarz + slusarz + slusarz@horde.org + yes + + 2009-02-22 + + 0.1.0 + 0.1.0 + + + beta + beta + + LGPL + * Initial Horde 4 package. + + + + + + + + + + + + + + + + + + + + + + + 5.2.0 + + + 1.5.0 + + + Util + pear.horde.org + + + + + + + + + + + 2006-05-08 + + + 0.0.2 + 0.0.2 + + + alpha + alpha + + LGPL + Converted to package.xml 2.0 for pear.horde.org + + + + + 0.0.1 + 0.0.1 + + + alpha + alpha + + 2004-10-15 + LGPL + Initial release as a PEAR 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 index 000000000..9d350223c --- /dev/null +++ b/framework/Text_Flowed/test/Horde/Text/Flowed.phpt @@ -0,0 +1,68 @@ +--TEST-- +Horde_Text_Flowed:: tests +--FILE-- + 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 -- 2.11.0