]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - include/Pear/HTML_Safe/Safe.php
Release 6.2.0beta4
[Github/sugarcrm.git] / include / Pear / HTML_Safe / Safe.php
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
4 /**
5  * HTML_Safe Parser
6  *
7  * PHP versions 4 and 5
8  *
9  * @category   HTML
10  * @package    HTML_Safe
11  * @author     Roman Ivanov <thingol@mail.ru>
12  * @copyright  2004-2005 Roman Ivanov
13  * @license    http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
14
15  * @link       http://pear.php.net/package/HTML_Safe
16  */
17
18
19 /**
20  * This package requires HTMLSax3 package
21  */
22 require_once 'include/Pear/XML_HTMLSax3/HTMLSax3.php';
23
24  
25 /**
26  *
27  * HTML_Safe Parser
28  *
29  * This parser strips down all potentially dangerous content within HTML:
30  * <ul>
31  * <li>opening tag without its closing tag</li>
32  * <li>closing tag without its opening tag</li>
33  * <li>any of these tags: "base", "basefont", "head", "html", "body", "applet", 
34  * "object", "iframe", "frame", "frameset", "script", "layer", "ilayer", "embed", 
35  * "bgsound", "link", "meta", "style", "title", "blink", "xml" etc.</li>
36  * <li>any of these attributes: on*, data*, dynsrc</li>
37  * <li>javascript:/vbscript:/about: etc. protocols</li>
38  * <li>expression/behavior etc. in styles</li>
39  * <li>any other active content</li>
40  * </ul>
41  * It also tries to convert code to XHTML valid, but htmltidy is far better 
42  * solution for this task.
43  *
44  * <b>Example:</b>
45  * <pre>
46  * $parser = new HTML_Safe();
47  * $result = $parser->parse($doc);
48  * </pre>
49  *
50  * @category   HTML
51  * @package    HTML_Safe
52  * @author     Roman Ivanov <thingol@mail.ru>
53  * @copyright  1997-2005 Roman Ivanov
54  * @license    http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
55  * @version    Release: @package_version@
56  * @link       http://pear.php.net/package/HTML_Safe
57  */
58 class HTML_Safe 
59 {
60     /**
61      * Storage for resulting HTML output
62      *
63      * @var string
64      * @access private
65      */
66     var $_xhtml = '';
67     
68     /**
69      * Array of counters for each tag
70      *
71      * @var array
72      * @access private
73      */
74     var $_counter = array();
75     
76     /**
77      * Stack of unclosed tags
78      *
79      * @var array
80      * @access private
81      */
82     var $_stack = array();
83     
84     /**
85      * Array of counters for tags that must be deleted with all content
86      *
87      * @var array
88      * @access private
89      */
90     var $_dcCounter = array();
91     
92     /**
93      * Stack of unclosed tags that must be deleted with all content
94      *
95      * @var array
96      * @access private
97      */
98     var $_dcStack = array();
99     
100     /**
101      * Stores level of list (ol/ul) nesting
102      *
103      * @var int
104      * @access private
105      */
106     var $_listScope = 0; 
107     
108     /**
109      * Stack of unclosed list tags 
110      *
111      * @var array
112      * @access private
113      */
114     var $_liStack = array();
115
116     /**
117      * Array of prepared regular expressions for protocols (schemas) matching
118      *
119      * @var array
120      * @access private
121      */
122     var $_protoRegexps = array();
123     
124     /**
125      * Array of prepared regular expressions for CSS matching
126      *
127      * @var array
128      * @access private
129      */
130     var $_cssRegexps = array();
131
132     /**
133      * List of single tags ("<tag />")
134      *
135      * @var array
136      * @access public
137      */
138     var $singleTags = array('area', 'br', 'img', 'input', 'hr', 'wbr', );
139
140     /**
141      * List of dangerous tags (such tags will be deleted)
142      *
143      * @var array
144      * @access public
145      */
146     var $deleteTags = array(
147         'applet', 'base',   'basefont', 'bgsound', 'blink',  'body', 
148         'embed',  'frame',  'frameset', 'head',    'html',   'ilayer', 
149         'iframe', 'layer',  'link',     'meta',    'object', 'style', 
150         'title',  'script', 
151         );
152
153     /**
154      * List of dangerous tags (such tags will be deleted, and all content 
155      * inside this tags will be also removed)
156      *
157      * @var array
158      * @access public
159      */
160     var $deleteTagsContent = array('script', 'style', 'title', 'xml', );
161
162     /**
163      * Type of protocols filtering ('white' or 'black')
164      *
165      * @var string
166      * @access public
167      */
168     var $protocolFiltering = 'white';
169
170     /**
171      * List of "dangerous" protocols (used for blacklist-filtering)
172      *
173      * @var array
174      * @access public
175      */
176     var $blackProtocols = array(
177         'about',   'chrome',     'data',       'disk',     'hcp',     
178         'help',    'javascript', 'livescript', 'lynxcgi',  'lynxexec', 
179         'ms-help', 'ms-its',     'mhtml',      'mocha',    'opera',   
180         'res',     'resource',   'shell',      'vbscript', 'view-source', 
181         'vnd.ms.radio',          'wysiwyg', 
182         );
183
184     /**
185      * List of "safe" protocols (used for whitelist-filtering)
186      *
187      * @var array
188      * @access public
189      */
190     var $whiteProtocols = array(
191         'ed2k',   'file', 'ftp',  'gopher', 'http',  'https', 
192         'irc',    'mailto', 'news', 'nntp', 'telnet', 'webcal', 
193         'xmpp',   'callto',
194         );
195
196     /**
197      * List of attributes that can contain protocols
198      *
199      * @var array
200      * @access public
201      */
202     var $protocolAttributes = array(
203         'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc', 'src', 
204         );
205
206     /**
207      * List of dangerous CSS keywords
208      *
209      * Whole style="" attribute will be removed, if parser will find one of 
210      * these keywords
211      *
212      * @var array
213      * @access public
214      */
215     var $cssKeywords = array(
216         'absolute', 'behavior',       'behaviour',   'content', 'expression', 
217         'fixed',    'include-source', 'moz-binding',
218         );
219
220     /**
221      * List of tags that can have no "closing tag"
222      *
223      * @var array
224      * @access public
225      * @deprecated XHTML does not allow such tags
226      */
227     var $noClose = array();
228
229     /**
230      * List of block-level tags that terminates paragraph
231      *
232      * Paragraph will be closed when this tags opened
233      *
234      * @var array
235      * @access public
236      */
237     var $closeParagraph = array(
238         'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
239         'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
240         'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
241         'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
242         'table',   'ul',         'xmp', 
243         );
244
245     /**
246      * List of table tags, all table tags outside a table will be removed
247      *
248      * @var array
249      * @access public
250      */
251     var $tableTags = array(
252         'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
253         'thead',   'tr', 
254         );
255
256     /**
257      * List of list tags
258      *
259      * @var array
260      * @access public
261      */
262     var $listTags = array('dir', 'menu', 'ol', 'ul', 'dl', );
263
264     /**
265      * List of dangerous attributes
266      *
267      * @var array
268      * @access public
269      */
270     var $attributes = array('dynsrc', 'id', 'name', );
271
272     /**
273      * List of allowed "namespaced" attributes
274      *
275      * @var array
276      * @access public
277      */
278     var $attributesNS = array('xml:lang', );
279
280     /**
281      * Constructs class
282      *
283      * @access public
284      */
285     function HTML_Safe() 
286     {
287         //making regular expressions based on Proto & CSS arrays
288         foreach ($this->blackProtocols as $proto) {
289             $preg = "/[\s\x01-\x1F]*";
290             for ($i=0; $i<strlen($proto); $i++) {
291                 $preg .= $proto{$i} . "[\s\x01-\x1F]*";
292             }
293             $preg .= ":/i";
294             $this->_protoRegexps[] = $preg;
295         }
296
297         foreach ($this->cssKeywords as $css) {
298             $this->_cssRegexps[] = '/' . $css . '/i';
299         }
300         return true;
301     }
302
303     /**
304      * Handles the writing of attributes - called from $this->_openHandler()
305      *
306      * @param array $attrs array of attributes $name => $value
307      * @return boolean
308      * @access private
309      */
310     function _writeAttrs ($attrs) 
311     {
312         if (is_array($attrs)) {
313             foreach ($attrs as $name => $value) {
314
315                 $name = strtolower($name);
316
317                 if (strpos($name, 'on') === 0) {
318                     continue;
319                 }
320                 if (strpos($name, 'data') === 0) {
321                     continue;
322                 }
323                 if (in_array($name, $this->attributes)) {
324                     continue;
325                 }
326                 if (!preg_match("/^[a-z0-9]+$/i", $name)) {
327                     if (!in_array($name, $this->attributesNS)) {
328                         continue;
329                     }
330                 }
331
332                 if (($value === TRUE) || (is_null($value))) {
333                     $value = $name;
334                 }
335
336                 if ($name == 'style') {
337                    
338                    // removes insignificant backslahes
339                    $value = str_replace("\\", '', $value);
340
341                    // removes CSS comments
342                    while (1)
343                    {
344                      $_value = preg_replace("!/\*.*?\*/!s", '', $value);
345                      if ($_value == $value) break;
346                      $value = $_value;
347                    }
348                    
349                    // replace all & to &amp;
350                    $value = str_replace('&amp;', '&', $value);
351                    $value = str_replace('&', '&amp;', $value);
352
353                    foreach ($this->_cssRegexps as $css) {
354                        if (preg_match($css, $value)) { 
355                            continue 2;
356                        }
357                    }
358                    foreach ($this->_protoRegexps as $proto) {
359                        if (preg_match($proto, $value)) {
360                            continue 2;
361                        }
362                    }
363                 }
364
365                 $tempval = preg_replace('/&#(\d+);?/me', "chr('\\1')", $value); //"'
366                 $tempval = preg_replace('/&#x([0-9a-f]+);?/mei', "chr(hexdec('\\1'))", $tempval);
367
368                 if ((in_array($name, $this->protocolAttributes)) && 
369                     (strpos($tempval, ':') !== false)) 
370                 {
371                     if ($this->protocolFiltering == 'black') {
372                         foreach ($this->_protoRegexps as $proto) {
373                             if (preg_match($proto, $tempval)) continue 2;
374                         }
375                     } else {
376                         $_tempval = explode(':', $tempval);
377                         $proto = $_tempval[0];
378                         if (!in_array($proto, $this->whiteProtocols)) {
379                             continue;
380                         }
381                     }
382                 }
383
384                 $value = str_replace("\"", "&quot;", $value);
385                 $this->_xhtml .= ' ' . $name . '="' . $value . '"';
386             }
387         }
388         return true;
389     }
390
391     /**
392      * Opening tag handler - called from HTMLSax
393      *
394      * @param object $parser HTML Parser
395      * @param string $name   tag name
396      * @param array  $attrs  tag attributes
397      * @return boolean
398      * @access private
399      */
400     function _openHandler(&$parser, $name, $attrs) 
401     {
402         $name = strtolower($name);
403
404         if (in_array($name, $this->deleteTagsContent)) {
405             array_push($this->_dcStack, $name);
406             $this->_dcCounter[$name] = isset($this->_dcCounter[$name]) ? $this->_dcCounter[$name]+1 : 1;
407         }
408         if (count($this->_dcStack) != 0) {
409             return true;
410         }
411
412         if (in_array($name, $this->deleteTags)) {
413             return true;
414         }
415         
416         if (!preg_match("/^[a-z0-9]+$/i", $name)) {
417             if (preg_match("!(?:\@|://)!i", $name)) {
418                 $this->_xhtml .= '&lt;' . $name . '&gt;';
419             }
420             return true;
421         }
422
423         if (in_array($name, $this->singleTags)) {
424             $this->_xhtml .= '<' . $name;
425             $this->_writeAttrs($attrs);
426             $this->_xhtml .= ' />';
427             return true;
428         }
429
430         // TABLES: cannot open table elements when we are not inside table
431         if ((isset($this->_counter['table'])) && ($this->_counter['table'] <= 0) 
432             && (in_array($name, $this->tableTags))) 
433         {
434             return true;
435         }
436
437         // PARAGRAPHS: close paragraph when closeParagraph tags opening
438         if ((in_array($name, $this->closeParagraph)) && (in_array('p', $this->_stack))) {
439             $this->_closeHandler($parser, 'p');
440         }
441
442         // LISTS: we should close <li> if <li> of the same level opening
443         if ($name == 'li' && count($this->_liStack) && 
444             $this->_listScope == $this->_liStack[count($this->_liStack)-1]) 
445         {
446             $this->_closeHandler($parser, 'li');
447         }
448
449         // LISTS: we want to know on what nesting level of lists we are
450         if (in_array($name, $this->listTags)) {
451             $this->_listScope++;
452         }
453         if ($name == 'li') {
454             array_push($this->_liStack, $this->_listScope);
455         }
456             
457         $this->_xhtml .= '<' . $name;
458         $this->_writeAttrs($attrs);
459         $this->_xhtml .= '>';
460         array_push($this->_stack,$name);
461         $this->_counter[$name] = isset($this->_counter[$name]) ? $this->_counter[$name]+1 : 1;
462         return true;
463     }
464
465     /**
466      * Closing tag handler - called from HTMLSax
467      *
468      * @param object $parsers HTML parser
469      * @param string $name    tag name
470      * @return boolean
471      * @access private
472      */
473     function _closeHandler(&$parser, $name) 
474     {
475
476         $name = strtolower($name);
477
478         if (isset($this->_dcCounter[$name]) && ($this->_dcCounter[$name] > 0) && 
479             (in_array($name, $this->deleteTagsContent))) 
480         {
481            while ($name != ($tag = array_pop($this->_dcStack))) {
482             $this->_dcCounter[$tag]--;
483            }
484
485            $this->_dcCounter[$name]--;
486         }
487
488         if (count($this->_dcStack) != 0) {
489             return true;
490         }
491
492         if ((isset($this->_counter[$name])) && ($this->_counter[$name] > 0)) {
493            while ($name != ($tag = array_pop($this->_stack))) {
494                $this->_closeTag($tag);
495            }
496
497            $this->_closeTag($name);
498         }
499         return true;
500     }
501
502     /**
503      * Closes tag 
504      *
505      * @param string $tag tag name
506      * @return boolean
507      * @access private
508      */
509     function _closeTag($tag) 
510     {
511         if (!in_array($tag, $this->noClose)) {
512             $this->_xhtml .= '</' . $tag . '>';
513         }
514
515         $this->_counter[$tag]--;
516
517         if (in_array($tag, $this->listTags)) {
518             $this->_listScope--;
519         }
520
521         if ($tag == 'li') {
522             array_pop($this->_liStack);
523         }
524         return true;
525     }
526
527     /**
528      * Character data handler - called from HTMLSax
529      *
530      * @param object $parser HTML parser
531      * @param string $data   textual data
532      * @return boolean
533      * @access private
534      */
535     function _dataHandler(&$parser, $data) 
536     {
537         if (count($this->_dcStack) == 0) {
538             $this->_xhtml .= $data;
539         }
540         return true;
541     }
542
543     /**
544      * Escape handler - called from HTMLSax
545      *
546      * @param object $parser HTML parser
547      * @param string $data   comments or other type of data
548      * @return boolean
549      * @access private
550      */
551     function _escapeHandler(&$parser, $data) 
552     {
553         return true;
554     }
555
556     /**
557      * Returns the XHTML document
558      *
559      * @return string Processed (X)HTML document
560      * @access public
561      */
562     function getXHTML () 
563     {
564         while ($tag = array_pop($this->_stack)) {
565             $this->_closeTag($tag);
566         }
567         
568         return $this->_xhtml;
569     }
570
571     /**
572      * Clears current document data
573      *
574      * @return boolean
575      * @access public
576      */
577     function clear() 
578     {
579         $this->_xhtml = '';
580         $this->_dcStack = array();
581         $this->_dcCounter = array();
582         return true;
583     }
584
585     /**
586      * Main parsing fuction
587      *
588      * @param string $doc HTML document for processing
589      * @return string Processed (X)HTML document
590      * @access public
591      */
592     function parse($doc) 
593     {
594
595        // Save all '<' symbols
596        $doc = preg_replace("/<(?=[^a-zA-Z\/\!\?\%])/", '&lt;', $doc);
597
598        // Web documents shouldn't contains \x00 symbol
599        $doc = str_replace("\x00", '', $doc);
600
601        // Opera6 bug workaround
602        $doc = str_replace("\xC0\xBC", '&lt;', $doc);
603
604        // UTF-7 encoding ASCII decode
605        //Bug 13697.
606        //$doc = $this->repackUTF7($doc);
607
608        // Instantiate the parser
609        $parser= new XML_HTMLSax3();
610
611        // Set up the parser
612        $parser->set_object($this);
613
614        $parser->set_element_handler('_openHandler','_closeHandler');
615        $parser->set_data_handler('_dataHandler');
616        $parser->set_escape_handler('_escapeHandler');
617
618        $parser->parse($doc);
619
620        return $this->getXHTML();
621
622     }
623
624
625     /**
626      * UTF-7 decoding fuction
627      *
628      * @param string $str HTML document for recode ASCII part of UTF-7 back to ASCII
629      * @return string Decoded document
630      * @access private
631      */
632     function repackUTF7($str)
633     {
634        return preg_replace_callback('!\+([0-9a-zA-Z/]+)\-!', array($this, 'repackUTF7Callback'), $str);
635     }
636
637     /**
638      * Additional UTF-7 decoding fuction
639      *
640      * @param string $str String for recode ASCII part of UTF-7 back to ASCII
641      * @return string Recoded string
642      * @access private
643      */
644     function repackUTF7Callback($str)
645     {
646        $str = base64_decode($str[1]);
647        $str = preg_replace_callback('/^((?:\x00.)*)((?:[^\x00].)+)/', array($this, 'repackUTF7Back'), $str);
648        return preg_replace('/\x00(.)/', '$1', $str);
649     }
650
651     /**
652      * Additional UTF-7 encoding fuction
653      *
654      * @param string $str String for recode ASCII part of UTF-7 back to ASCII
655      * @return string Recoded string
656      * @access private
657      */
658     function repackUTF7Back($str)
659     {
660        return $str[1].'+'.rtrim(base64_encode($str[2]), '=').'-';
661     }
662 }
663
664 /*
665  * Local variables:
666  * tab-width: 4
667  * c-basic-offset: 4
668  * c-hanging-comment-ender-p: nil
669  * End:
670  */
671
672 ?>