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