PageRenderTime 59ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/lib/horde/framework/Horde/Text/Flowed.php

https://gitlab.com/unofficial-mirrors/moodle
PHP | 376 lines | 153 code | 36 blank | 187 comment | 34 complexity | 5b21490ee7b64094cd4a189b4df85e55 MD5 | raw file
  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 COPYING 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. * When this class wraps a line, the newly created lines will be split
  33. * at this length.
  34. *
  35. * @var integer
  36. */
  37. protected $_optlength = 72;
  38. /**
  39. * The text to be formatted.
  40. *
  41. * @var string
  42. */
  43. protected $_text;
  44. /**
  45. * The cached output of the formatting.
  46. *
  47. * @var array
  48. */
  49. protected $_output = array();
  50. /**
  51. * The format of the data in $_output.
  52. *
  53. * @var string
  54. */
  55. protected $_formattype = null;
  56. /**
  57. * The character set of the text.
  58. *
  59. * @var string
  60. */
  61. protected $_charset;
  62. /**
  63. * Convert text using DelSp?
  64. *
  65. * @var boolean
  66. */
  67. protected $_delsp = false;
  68. /**
  69. * Constructor.
  70. *
  71. * @param string $text The text to process.
  72. * @param string $charset The character set of $text.
  73. */
  74. public function __construct($text, $charset = 'UTF-8')
  75. {
  76. $this->_text = $text;
  77. $this->_charset = $charset;
  78. }
  79. /**
  80. * Set the maximum length of a line of text.
  81. *
  82. * @param integer $max A new value for $_maxlength.
  83. */
  84. public function setMaxLength($max)
  85. {
  86. $this->_maxlength = $max;
  87. }
  88. /**
  89. * Set the optimal length of a line of text.
  90. *
  91. * @param integer $max A new value for $_optlength.
  92. */
  93. public function setOptLength($opt)
  94. {
  95. $this->_optlength = $opt;
  96. }
  97. /**
  98. * Set whether to format text using DelSp.
  99. *
  100. * @param boolean $delsp Use DelSp?
  101. */
  102. public function setDelSp($delsp)
  103. {
  104. $this->_delsp = (bool)$delsp;
  105. }
  106. /**
  107. * Reformats the input string, where the string is 'format=flowed' plain
  108. * text as described in RFC 2646.
  109. *
  110. * @param boolean $quote Add level of quoting to each line?
  111. *
  112. * @return string The text converted to RFC 2646 'fixed' format.
  113. */
  114. public function toFixed($quote = false)
  115. {
  116. $txt = '';
  117. $this->_reformat(false, $quote);
  118. $lines = count($this->_output) - 1;
  119. foreach ($this->_output as $no => $line) {
  120. $txt .= $line['text'] . (($lines == $no) ? '' : "\n");
  121. }
  122. return $txt;
  123. }
  124. /**
  125. * Reformats the input string, and returns the output in an array format
  126. * with quote level information.
  127. *
  128. * @param boolean $quote Add level of quoting to each line?
  129. *
  130. * @return array An array of arrays with the following elements:
  131. * <pre>
  132. * 'level' - The quote level of the current line.
  133. * 'text' - The text for the current line.
  134. * </pre>
  135. */
  136. public function toFixedArray($quote = false)
  137. {
  138. $this->_reformat(false, $quote);
  139. return $this->_output;
  140. }
  141. /**
  142. * Reformats the input string, where the string is 'format=fixed' plain
  143. * text as described in RFC 2646.
  144. *
  145. * @param boolean $quote Add level of quoting to each line?
  146. * @param array $opts Additional options:
  147. * <pre>
  148. * 'nowrap' - (boolean) If true, does not wrap unquoted lines.
  149. * DEFAULT: false
  150. * </pre>
  151. *
  152. * @return string The text converted to RFC 2646 'flowed' format.
  153. */
  154. public function toFlowed($quote = false, array $opts = array())
  155. {
  156. $txt = '';
  157. $this->_reformat(true, $quote, empty($opts['nowrap']));
  158. foreach ($this->_output as $line) {
  159. $txt .= $line['text'] . "\n";
  160. }
  161. return $txt;
  162. }
  163. /**
  164. * Reformats the input string, where the string is 'format=flowed' plain
  165. * text as described in RFC 2646.
  166. *
  167. * @param boolean $toflowed Convert to flowed?
  168. * @param boolean $quote Add level of quoting to each line?
  169. * @param boolean $wrap Wrap unquoted lines?
  170. */
  171. protected function _reformat($toflowed, $quote, $wrap = true)
  172. {
  173. $format_type = implode('|', array($toflowed, $quote));
  174. if ($format_type == $this->_formattype) {
  175. return;
  176. }
  177. $this->_output = array();
  178. $this->_formattype = $format_type;
  179. /* Set variables used in regexps. */
  180. $delsp = ($toflowed && $this->_delsp) ? 1 : 0;
  181. $opt = $this->_optlength - 1 - $delsp;
  182. /* Process message line by line. */
  183. $text = preg_split("/\r?\n/", $this->_text);
  184. $text_count = count($text) - 1;
  185. $skip = 0;
  186. foreach ($text as $no => $line) {
  187. if ($skip) {
  188. --$skip;
  189. continue;
  190. }
  191. /* Per RFC 2646 [4.3], the 'Usenet Signature Convention' line
  192. * (DASH DASH SP) is not considered flowed. Watch for this when
  193. * dealing with potentially flowed lines. */
  194. /* The next three steps come from RFC 2646 [4.2]. */
  195. /* STEP 1: Determine quote level for line. */
  196. if (($num_quotes = $this->_numquotes($line))) {
  197. $line = substr($line, $num_quotes);
  198. }
  199. /* Only combine lines if we are converting to flowed or if the
  200. * current line is quoted. */
  201. if (!$toflowed || $num_quotes) {
  202. /* STEP 2: Remove space stuffing from line. */
  203. $line = $this->_unstuff($line);
  204. /* STEP 3: Should we interpret this line as flowed?
  205. * While line is flowed (not empty and there is a space
  206. * at the end of the line), and there is a next line, and the
  207. * next line has the same quote depth, add to the current
  208. * line. A line is not flowed if it is a signature line. */
  209. if ($line != '-- ') {
  210. while (!empty($line) &&
  211. (substr($line, -1) == ' ') &&
  212. ($text_count != $no) &&
  213. ($this->_numquotes($text[$no + 1]) == $num_quotes)) {
  214. /* If DelSp is yes and this is flowed input, we need to
  215. * remove the trailing space. */
  216. if (!$toflowed && $this->_delsp) {
  217. $line = substr($line, 0, -1);
  218. }
  219. $line .= $this->_unstuff(substr($text[++$no], $num_quotes));
  220. ++$skip;
  221. }
  222. }
  223. }
  224. /* Ensure line is fixed, since we already joined all flowed
  225. * lines. Remove all trailing ' ' from the line. */
  226. if ($line != '-- ') {
  227. $line = rtrim($line);
  228. }
  229. /* Increment quote depth if we're quoting. */
  230. if ($quote) {
  231. $num_quotes++;
  232. }
  233. /* The quote prefix for the line. */
  234. $quotestr = str_repeat('>', $num_quotes);
  235. if (empty($line)) {
  236. /* Line is empty. */
  237. $this->_output[] = array('text' => $quotestr, 'level' => $num_quotes);
  238. } elseif ((!$wrap && !$num_quotes) ||
  239. empty($this->_maxlength) ||
  240. ((Horde_String::length($line, $this->_charset) + $num_quotes) <= $this->_maxlength)) {
  241. /* Line does not require rewrapping. */
  242. $this->_output[] = array('text' => $quotestr . $this->_stuff($line, $num_quotes, $toflowed), 'level' => $num_quotes);
  243. } else {
  244. $min = $num_quotes + 1;
  245. /* Rewrap this paragraph. */
  246. while ($line) {
  247. /* Stuff and re-quote the line. */
  248. $line = $quotestr . $this->_stuff($line, $num_quotes, $toflowed);
  249. $line_length = Horde_String::length($line, $this->_charset);
  250. if ($line_length <= $this->_optlength) {
  251. /* Remaining section of line is short enough. */
  252. $this->_output[] = array('text' => $line, 'level' => $num_quotes);
  253. break;
  254. } else {
  255. $regex = array();
  256. if ($min <= $opt) {
  257. $regex[] = '^(.{' . $min . ',' . $opt . '}) (.*)';
  258. }
  259. if ($min <= $this->_maxlength) {
  260. $regex[] = '^(.{' . $min . ',' . $this->_maxlength . '}) (.*)';
  261. }
  262. $regex[] = '^(.{' . $min . ',})? (.*)';
  263. if ($m = Horde_String::regexMatch($line, $regex, $this->_charset)) {
  264. /* We need to wrap text at a certain number of
  265. * *characters*, not a certain number of *bytes*;
  266. * thus the need for a multibyte capable regex.
  267. * If a multibyte regex isn't available, we are
  268. * stuck with preg_match() (the function will
  269. * still work - are just left with shorter rows
  270. * than expected if multibyte characters exist in
  271. * the row).
  272. *
  273. * 1. Try to find a string as long as _optlength.
  274. * 2. Try to find a string as long as _maxlength.
  275. * 3. Take the first word. */
  276. if (empty($m[1])) {
  277. $m[1] = $m[2];
  278. $m[2] = '';
  279. }
  280. $this->_output[] = array('text' => $m[1] . ' ' . (($delsp) ? ' ' : ''), 'level' => $num_quotes);
  281. $line = $m[2];
  282. } elseif ($line_length > 998) {
  283. /* One excessively long word left on line. Be
  284. * absolutely sure it does not exceed 998
  285. * characters in length or else we must
  286. * truncate. */
  287. $this->_output[] = array('text' => Horde_String::substr($line, 0, 998, $this->_charset), 'level' => $num_quotes);
  288. $line = Horde_String::substr($line, 998, null, $this->_charset);
  289. } else {
  290. $this->_output[] = array('text' => $line, 'level' => $num_quotes);
  291. break;
  292. }
  293. }
  294. }
  295. }
  296. }
  297. }
  298. /**
  299. * Returns the number of leading '>' characters in the text input.
  300. * '>' characters are defined by RFC 2646 to indicate a quoted line.
  301. *
  302. * @param string $text The text to analyze.
  303. *
  304. * @return integer The number of leading quote characters.
  305. */
  306. protected function _numquotes($text)
  307. {
  308. return strspn($text, '>');
  309. }
  310. /**
  311. * Space-stuffs if it starts with ' ' or '>' or 'From ', or if
  312. * quote depth is non-zero (for aesthetic reasons so that there is a
  313. * space after the '>').
  314. *
  315. * @param string $text The text to stuff.
  316. * @param string $num_quotes The quote-level of this line.
  317. * @param boolean $toflowed Are we converting to flowed text?
  318. *
  319. * @return string The stuffed text.
  320. */
  321. protected function _stuff($text, $num_quotes, $toflowed)
  322. {
  323. return ($toflowed && ($num_quotes || preg_match("/^(?: |>|From |From$)/", $text)))
  324. ? ' ' . $text
  325. : $text;
  326. }
  327. /**
  328. * Unstuffs a space stuffed line.
  329. *
  330. * @param string $text The text to unstuff.
  331. *
  332. * @return string The unstuffed text.
  333. */
  334. protected function _unstuff($text)
  335. {
  336. return (!empty($text) && ($text[0] == ' '))
  337. ? substr($text, 1)
  338. : $text;
  339. }
  340. }