source: github/program/lib/html2text.php @ f50cc72

HEADcourier-fixdev-browser-capabilitiespdorelease-0.6release-0.7release-0.8
Last change on this file since f50cc72 was f50cc72, checked in by alecpl <alec@…>, 4 years ago

#1485618: fix code injection vulnerability

  • Property mode set to 100644
File size: 20.0 KB
Line 
1<?php
2
3/*************************************************************************
4 *                                                                       *
5 * class.html2text.inc                                                   *
6 *                                                                       *
7 *************************************************************************
8 *                                                                       *
9 * Converts HTML to formatted plain text                                 *
10 *                                                                       *
11 * Copyright (c) 2005-2007 Jon Abernathy <jon@chuggnutt.com>             *
12 * All rights reserved.                                                  *
13 *                                                                       *
14 * This script is free software; you can redistribute it and/or modify   *
15 * it under the terms of the GNU General Public License as published by  *
16 * the Free Software Foundation; either version 2 of the License, or     *
17 * (at your option) any later version.                                   *
18 *                                                                       *
19 * The GNU General Public License can be found at                        *
20 * http://www.gnu.org/copyleft/gpl.html.                                 *
21 *                                                                       *
22 * This script is distributed in the hope that it will be useful,        *
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of        *
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the          *
25 * GNU General Public License for more details.                          *
26 *                                                                       *
27 * Author(s): Jon Abernathy <jon@chuggnutt.com>                          *
28 *                                                                       *
29 * Last modified: 08/08/07                                               *
30 *                                                                       *
31 *************************************************************************/
32
33
34/**
35 *  Takes HTML and converts it to formatted, plain text.
36 *
37 *  Thanks to Alexander Krug (http://www.krugar.de/) to pointing out and
38 *  correcting an error in the regexp search array. Fixed 7/30/03.
39 *
40 *  Updated set_html() function's file reading mechanism, 9/25/03.
41 *
42 *  Thanks to Joss Sanglier (http://www.dancingbear.co.uk/) for adding
43 *  several more HTML entity codes to the $search and $replace arrays.
44 *  Updated 11/7/03.
45 *
46 *  Thanks to Darius Kasperavicius (http://www.dar.dar.lt/) for
47 *  suggesting the addition of $allowed_tags and its supporting function
48 *  (which I slightly modified). Updated 3/12/04.
49 *
50 *  Thanks to Justin Dearing for pointing out that a replacement for the
51 *  <TH> tag was missing, and suggesting an appropriate fix.
52 *  Updated 8/25/04.
53 *
54 *  Thanks to Mathieu Collas (http://www.myefarm.com/) for finding a
55 *  display/formatting bug in the _build_link_list() function: email
56 *  readers would show the left bracket and number ("[1") as part of the
57 *  rendered email address.
58 *  Updated 12/16/04.
59 *
60 *  Thanks to Wojciech Bajon (http://histeria.pl/) for submitting code
61 *  to handle relative links, which I hadn't considered. I modified his
62 *  code a bit to handle normal HTTP links and MAILTO links. Also for
63 *  suggesting three additional HTML entity codes to search for.
64 *  Updated 03/02/05.
65 *
66 *  Thanks to Jacob Chandler for pointing out another link condition
67 *  for the _build_link_list() function: "https".
68 *  Updated 04/06/05.
69 *
70 *  Thanks to Marc Bertrand (http://www.dresdensky.com/) for
71 *  suggesting a revision to the word wrapping functionality; if you
72 *  specify a $width of 0 or less, word wrapping will be ignored.
73 *  Updated 11/02/06.
74 *
75 *  *** Big housecleaning updates below:
76 *
77 *  Thanks to Colin Brown (http://www.sparkdriver.co.uk/) for
78 *  suggesting the fix to handle </li> and blank lines (whitespace).
79 *  Christian Basedau (http://www.movetheweb.de/) also suggested the
80 *  blank lines fix.
81 *
82 *  Special thanks to Marcus Bointon (http://www.synchromedia.co.uk/),
83 *  Christian Basedau, Norbert Laposa (http://ln5.co.uk/),
84 *  Bas van de Weijer, and Marijn van Butselaar
85 *  for pointing out my glaring error in the <th> handling. Marcus also
86 *  supplied a host of fixes.
87 *
88 *  Thanks to Jeffrey Silverman (http://www.newtnotes.com/) for pointing
89 *  out that extra spaces should be compressed--a problem addressed with
90 *  Marcus Bointon's fixes but that I had not yet incorporated.
91 *
92 *      Thanks to Daniel Schledermann (http://www.typoconsult.dk/) for
93 *  suggesting a valuable fix with <a> tag handling.
94 *
95 *  Thanks to Wojciech Bajon (again!) for suggesting fixes and additions,
96 *  including the <a> tag handling that Daniel Schledermann pointed
97 *  out but that I had not yet incorporated. I haven't (yet)
98 *  incorporated all of Wojciech's changes, though I may at some
99 *  future time.
100 *
101 *  *** End of the housecleaning updates. Updated 08/08/07.
102 *
103 *  @author Jon Abernathy <jon@chuggnutt.com>
104 *  @version 1.0.0
105 *  @since PHP 4.0.2
106 */
107class html2text
108{
109
110    /**
111     *  Contains the HTML content to convert.
112     *
113     *  @var string $html
114     *  @access public
115     */
116    var $html;
117
118    /**
119     *  Contains the converted, formatted text.
120     *
121     *  @var string $text
122     *  @access public
123     */
124    var $text;
125
126    /**
127     *  Maximum width of the formatted text, in columns.
128     *
129     *  Set this value to 0 (or less) to ignore word wrapping
130     *  and not constrain text to a fixed-width column.
131     *
132     *  @var integer $width
133     *  @access public
134     */
135    var $width = 70;
136
137    /**
138     *  List of preg* regular expression patterns to search for,
139     *  used in conjunction with $replace.
140     *
141     *  @var array $search
142     *  @access public
143     *  @see $replace
144     */
145    var $search = array(
146        "/\r/",                                  // Non-legal carriage return
147        "/[\n\t]+/",                             // Newlines and tabs
148        '/[ ]{2,}/',                             // Runs of spaces, pre-handling
149        '/<script[^>]*>.*?<\/script>/i',         // <script>s -- which strip_tags supposedly has problems with
150        '/<style[^>]*>.*?<\/style>/i',           // <style>s -- which strip_tags supposedly has problems with
151        //'/<!-- .* -->/',                         // Comments -- which strip_tags might have problem a with
152        '/<p[^>]*>/i',                           // <P>
153        '/<br[^>]*>/i',                          // <br>
154        '/<i[^>]*>(.*?)<\/i>/i',                 // <i>
155        '/<em[^>]*>(.*?)<\/em>/i',               // <em>
156        '/(<ul[^>]*>|<\/ul>)/i',                 // <ul> and </ul>
157        '/(<ol[^>]*>|<\/ol>)/i',                 // <ol> and </ol>
158        '/<li[^>]*>(.*?)<\/li>/i',               // <li> and </li>
159        '/<li[^>]*>/i',                          // <li>
160        '/<hr[^>]*>/i',                          // <hr>
161        '/(<table[^>]*>|<\/table>)/i',           // <table> and </table>
162        '/(<tr[^>]*>|<\/tr>)/i',                 // <tr> and </tr>
163        '/<td[^>]*>(.*?)<\/td>/i',               // <td> and </td>
164        '/&(nbsp|#160);/i',                      // Non-breaking space
165        '/&(quot|rdquo|ldquo|#8220|#8221|#147|#148);/i',
166                                                         // Double quotes
167        '/&(apos|rsquo|lsquo|#8216|#8217);/i',   // Single quotes
168        '/&gt;/i',                               // Greater-than
169        '/&lt;/i',                               // Less-than
170        '/&(amp|#38);/i',                        // Ampersand
171        '/&(copy|#169);/i',                      // Copyright
172        '/&(trade|#8482|#153);/i',               // Trademark
173        '/&(reg|#174);/i',                       // Registered
174        '/&(mdash|#151|#8212);/i',               // mdash
175        '/&(ndash|minus|#8211|#8722);/i',        // ndash
176        '/&(bull|#149|#8226);/i',                // Bullet
177        '/&(pound|#163);/i',                     // Pound sign
178        '/&(euro|#8364);/i',                     // Euro sign
179        '/&[^&;]+;/i',                           // Unknown/unhandled entities
180        '/[ ]{2,}/'                              // Runs of spaces, post-handling
181    );
182
183    /**
184     *  List of pattern replacements corresponding to patterns searched.
185     *
186     *  @var array $replace
187     *  @access public
188     *  @see $search
189     */
190    var $replace = array(
191        '',                                     // Non-legal carriage return
192        ' ',                                    // Newlines and tabs
193        ' ',                                    // Runs of spaces, pre-handling
194        '',                                     // <script>s -- which strip_tags supposedly has problems with
195        '',                                     // <style>s -- which strip_tags supposedly has problems with
196        //'',                                     // Comments -- which strip_tags might have problem a with
197        "\n\n",                               // <P>
198        "\n",                                   // <br>
199        '_\\1_',                                // <i>
200        '_\\1_',                                // <em>
201        "\n\n",                                 // <ul> and </ul>
202        "\n\n",                                 // <ol> and </ol>
203        "\t* \\1\n",                            // <li> and </li>
204        "\n\t* ",                               // <li>
205        "\n-------------------------\n",        // <hr>
206        "\n\n",                                 // <table> and </table>
207        "\n",                                   // <tr> and </tr>
208        "\t\t\\1\n",                            // <td> and </td>
209        ' ',                                    // Non-breaking space
210        '"',                                    // Double quotes
211        "'",                                    // Single quotes
212        '>',
213        '<',
214        '&',
215        '(c)',
216        '(tm)',
217        '(R)',
218        '--',
219        '-',
220        '*',
221        '£',
222        'EUR',                                  // Euro sign. € ?
223        '',                                     // Unknown/unhandled entities
224        ' '                                     // Runs of spaces, post-handling
225    );
226
227    /**
228     *  List of preg* regular expression patterns to search for
229     *  and replace using callback function.
230     *
231     *  @var array $callback_search
232     *  @access public
233     */
234    var $callback_search = array(
235        '/<(h)[123456][^>]*>(.*?)<\/h[123456]>/i', // H1 - H3
236        '/<(b)[^>]*>(.*?)<\/b>/i',                 // <b>
237        '/<(strong)[^>]*>(.*?)<\/strong>/i',       // <strong>
238        '/<(a) [^>]*href=("|\')([^"\']+)\2[^>]*>(.*?)<\/a>/i',
239                                                   // <a href="">
240        '/<(th)[^>]*>(.*?)<\/th>/i',               // <th> and </th>
241    );
242
243   /**
244    *  List of preg* regular expression patterns to search for in PRE body,
245    *  used in conjunction with $pre_replace.
246    *
247    *  @var array $pre_search
248    *  @access public
249    *  @see $pre_replace
250    */
251    var $pre_search = array(
252        "/\n/",
253        "/\t/",
254        '/ /',
255        '/<pre[^>]*>/',
256        '/<\/pre>/'
257    );
258
259    /**
260     *  List of pattern replacements corresponding to patterns searched for PRE body.
261     *
262     *  @var array $pre_replace
263     *  @access public
264     *  @see $pre_search
265     */
266    var $pre_replace = array(
267        '<br>',
268        '&nbsp;&nbsp;&nbsp;&nbsp;',
269        '&nbsp;',
270        '',
271        ''
272    );
273
274    /**
275     *  Contains a list of HTML tags to allow in the resulting text.
276     *
277     *  @var string $allowed_tags
278     *  @access public
279     *  @see set_allowed_tags()
280     */
281    var $allowed_tags = '';
282
283    /**
284     *  Contains the base URL that relative links should resolve to.
285     *
286     *  @var string $url
287     *  @access public
288     */
289    var $url;
290
291    /**
292     *  Indicates whether content in the $html variable has been converted yet.
293     *
294     *  @var boolean $_converted
295     *  @access private
296     *  @see $html, $text
297     */
298    var $_converted = false;
299
300    /**
301     *  Contains URL addresses from links to be rendered in plain text.
302     *
303     *  @var string $_link_list
304     *  @access private
305     *  @see _build_link_list()
306     */
307    var $_link_list = '';
308   
309    /**
310     *  Number of valid links detected in the text, used for plain text
311     *  display (rendered similar to footnotes).
312     *
313     *  @var integer $_link_count
314     *  @access private
315     *  @see _build_link_list()
316     */
317    var $_link_count = 0;
318
319    /**
320     * Boolean flag, true if a table of link URLs should be listed after the text.
321     * 
322     * @var boolean $_do_links
323     * @access private
324     * @see html2text()
325     */
326    var $_do_links = true;
327 
328    /**
329     *  Constructor.
330     *
331     *  If the HTML source string (or file) is supplied, the class
332     *  will instantiate with that source propagated, all that has
333     *  to be done it to call get_text().
334     *
335     *  @param string $source HTML content
336     *  @param boolean $from_file Indicates $source is a file to pull content from
337     *  @param boolean $do_links Indicate whether a table of link URLs is desired
338     *  @param integer $width Maximum width of the formatted text, 0 for no limit
339     *  @access public
340     *  @return void
341     */
342    function html2text( $source = '', $from_file = false, $do_links = true, $width = 75 )
343    {
344        if ( !empty($source) ) {
345            $this->set_html($source, $from_file);
346        }
347       
348        $this->set_base_url();
349        $this->_do_links = $do_links;
350        $this->width = $width;
351    }
352
353    /**
354     *  Loads source HTML into memory, either from $source string or a file.
355     *
356     *  @param string $source HTML content
357     *  @param boolean $from_file Indicates $source is a file to pull content from
358     *  @access public
359     *  @return void
360     */
361    function set_html( $source, $from_file = false )
362    {
363        if ( $from_file && file_exists($source) ) {
364            $this->html = file_get_contents($source); 
365        }
366        else
367            $this->html = $source;
368
369        $this->_converted = false;
370    }
371
372    /**
373     *  Returns the text, converted from HTML.
374     *
375     *  @access public
376     *  @return string
377     */
378    function get_text()
379    {
380        if ( !$this->_converted ) {
381            $this->_convert();
382        }
383
384        return $this->text;
385    }
386
387    /**
388     *  Prints the text, converted from HTML.
389     *
390     *  @access public
391     *  @return void
392     */
393    function print_text()
394    {
395        print $this->get_text();
396    }
397
398    /**
399     *  Alias to print_text(), operates identically.
400     *
401     *  @access public
402     *  @return void
403     *  @see print_text()
404     */
405    function p()
406    {
407        print $this->get_text();
408    }
409
410    /**
411     *  Sets the allowed HTML tags to pass through to the resulting text.
412     *
413     *  Tags should be in the form "<p>", with no corresponding closing tag.
414     *
415     *  @access public
416     *  @return void
417     */
418    function set_allowed_tags( $allowed_tags = '' )
419    {
420        if ( !empty($allowed_tags) ) {
421            $this->allowed_tags = $allowed_tags;
422        }
423    }
424
425    /**
426     *  Sets a base URL to handle relative links.
427     *
428     *  @access public
429     *  @return void
430     */
431    function set_base_url( $url = '' )
432    {
433        if ( empty($url) ) {
434                if ( !empty($_SERVER['HTTP_HOST']) ) {
435                    $this->url = 'http://' . $_SERVER['HTTP_HOST'];
436                } else {
437                    $this->url = '';
438                }
439        } else {
440            // Strip any trailing slashes for consistency (relative
441            // URLs may already start with a slash like "/file.html")
442            if ( substr($url, -1) == '/' ) {
443                $url = substr($url, 0, -1);
444            }
445            $this->url = $url;
446        }
447    }
448
449    /**
450     *  Workhorse function that does actual conversion.
451     *
452     *  First performs custom tag replacement specified by $search and
453     *  $replace arrays. Then strips any remaining HTML tags, reduces whitespace
454     *  and newlines to a readable format, and word wraps the text to
455     *  $width characters.
456     *
457     *  @access private
458     *  @return void
459     */
460    function _convert()
461    {
462        // Variables used for building the link list
463        $this->_link_count = 0;
464        $this->_link_list = '';
465
466        $text = trim(stripslashes($this->html));
467
468        // Convert <PRE>
469        $this->_convert_pre($text);
470
471        // Replace known html entities
472        $text = html_entity_decode($text, ENT_COMPAT, 'UTF-8');
473
474        // Run our defined search-and-replace
475        $text = preg_replace($this->search, $this->replace, $text);
476        $text = preg_replace_callback($this->callback_search, array('html2text', '_preg_callback'), $text);
477
478        // Strip any other HTML tags
479        $text = strip_tags($text, $this->allowed_tags);
480
481        // Bring down number of empty lines to 2 max
482        $text = preg_replace("/\n\s+\n/", "\n\n", $text);
483        $text = preg_replace("/[\n]{3,}/", "\n\n", $text);
484
485        // Add link list
486        if ( !empty($this->_link_list) ) {
487            $text .= "\n\nLinks:\n------\n" . $this->_link_list;
488        }
489
490        // Wrap the text to a readable format
491        // for PHP versions >= 4.0.2. Default width is 75
492        // If width is 0 or less, don't wrap the text.
493        if ( $this->width > 0 ) {
494                $text = wordwrap($text, $this->width);
495        }
496
497        $this->text = $text;
498
499        $this->_converted = true;
500    }
501
502    /**
503     *  Helper function called by preg_replace() on link replacement.
504     *
505     *  Maintains an internal list of links to be displayed at the end of the
506     *  text, with numeric indices to the original point in the text they
507     *  appeared. Also makes an effort at identifying and handling absolute
508     *  and relative links.
509     *
510     *  @param string $link URL of the link
511     *  @param string $display Part of the text to associate number with
512     *  @access private
513     *  @return string
514     */
515    function _build_link_list( $link, $display )
516    {
517        if ( !$this->_do_links ) return $display;
518       
519        if ( substr($link, 0, 7) == 'http://' || substr($link, 0, 8) == 'https://' ||
520             substr($link, 0, 7) == 'mailto:' ) {
521            $this->_link_count++;
522            $this->_link_list .= "[" . $this->_link_count . "] $link\n";
523            $additional = ' [' . $this->_link_count . ']';
524                } elseif ( substr($link, 0, 11) == 'javascript:' ) {
525                        // Don't count the link; ignore it
526                        $additional = '';
527                // what about href="#anchor" ?
528        } else {
529            $this->_link_count++;
530            $this->_link_list .= "[" . $this->_link_count . "] " . $this->url;
531            if ( substr($link, 0, 1) != '/' ) {
532                $this->_link_list .= '/';
533            }
534            $this->_link_list .= "$link\n";
535            $additional = ' [' . $this->_link_count . ']';
536        }
537
538        return $display . $additional;
539    }
540   
541    /**
542     *  Helper function for PRE body conversion.
543     *
544     *  @param string HTML content
545     *  @access private
546     */
547    function _convert_pre(&$text)
548    {
549        while(preg_match('/<pre[^>]*>(.*)<\/pre>/ismU', $text, $matches))
550        {
551            $result = preg_replace($this->pre_search, $this->pre_replace, $matches[1]);
552            $text = preg_replace('/<pre[^>]*>.*<\/pre>/ismU', '<div><br>' . $result . '<br></div>', $text, 1);
553        }
554    }
555
556    /**
557     *  Callback function for preg_replace_callback use.
558     *
559     *  @param  array PREG matches
560     *  @return string
561     *  @access private
562     */
563    function _preg_callback($matches)
564    {
565        switch($matches[1])
566        {
567            case 'b':
568            case 'strong':
569                return $this->_strtoupper($matches[2]);
570            case 'hr':
571                return $this->_strtoupper("\t\t". $matches[2] ."\n");
572            case 'h':
573                return $this->_strtoupper("\n\n". $matches[2] ."\n\n");
574            case 'a':
575                return $this->_build_link_list($matches[3], $matches[4]);
576        }
577    }
578   
579    /**
580     *  Strtoupper multibyte wrapper function
581     *
582     *  @param  string
583     *  @return string
584     *  @access private
585     */
586    function _strtoupper($str)
587    {
588        if (function_exists('mb_strtoupper'))
589            return mb_strtoupper($str);
590        else
591            return strtoupper($str);
592    }
593}
594
595?>
Note: See TracBrowser for help on using the repository browser.