fix image text
[pear] / HTML / Safe.php
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
4 /**
5  * Loosely Based onHTML_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  * @version    CVS: $Id:$
15  * @link       http://pear.php.net/package/HTML_Safe
16  */
17  
18 /**
19  *
20  * HTML_Safe Parser
21  *
22  * This parser strips down all potentially dangerous content within HTML:
23  * <ul>
24  * <li>opening tag without its closing tag</li>
25  * <li>closing tag without its opening tag</li>
26  * <li>any of these tags: "base", "basefont", "head", "html", "body", "applet", 
27  * "object", "iframe", "frame", "frameset", "script", "layer", "ilayer", "embed", 
28  * "bgsound", "link", "meta", "style", "title", "blink", "xml" etc.</li>
29  * <li>any of these attributes: on*, data*, dynsrc</li>
30  * <li>javascript:/vbscript:/about: etc. protocols</li>
31  * <li>expression/behavior etc. in styles</li>
32  * <li>any other active content</li>
33  * </ul>
34  * It also tries to convert code to XHTML valid, but htmltidy is far better 
35  * solution for this task.
36  *
37  * <b>Example:</b>
38  * <pre>
39  * $parser =& new HTML_Safe();
40  * $result = $parser->parse($doc);
41  * </pre>
42  *
43  * @category   HTML
44  * @package    HTML_Safe
45  * @author     Roman Ivanov <thingol@mail.ru>
46  * @copyright  1997-2005 Roman Ivanov
47  * @license    http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
48  * @version    Release: @package_version@
49  * @link       http://pear.php.net/package/HTML_Safe
50  */
51 class HTML_Safe 
52 {
53     
54      
55    
56     /**
57      * Array of prepared regular expressions for protocols (schemas) matching
58      *
59      * @var array
60      * @access private
61      */
62     var $_protoRegexps = array();
63     
64     /**
65      * Array of prepared regular expressions for CSS matching
66      *
67      * @var array
68      * @access private
69      */
70     var $_cssRegexps = array();
71
72     /**
73      * List of single tags ("<tag />")
74      *
75      * @var array
76      * @access public
77      */
78     var $singleTags = array('area', 'br', 'img', 'input', 'hr', 'wbr', );
79
80     /**
81      * List of dangerous tags (such tags will be deleted)
82      *
83      * @var array
84      * @access public
85      */
86     var $deleteTags = array(
87         'applet', 'base',   'basefont', 'bgsound', 'blink',  'body', 
88         'embed',  'frame',  'frameset', 'head',    'html',   'ilayer', 
89         'iframe', 'layer',  'link',     'meta',    'object', 'style', 
90         'title',  'script', 
91         );
92
93     /**
94      * List of dangerous tags (such tags will be deleted, and all content 
95      * inside this tags will be also removed)
96      *
97      * @var array
98      * @access public
99      */
100     var $deleteTagsContent = array('script', 'style', 'title', 'xml', );
101
102     /**
103      * Type of protocols filtering ('white' or 'black')
104      *
105      * @var string
106      * @access public
107      */
108     var $protocolFiltering = 'white';
109
110     /**
111      * List of "dangerous" protocols (used for blacklist-filtering)
112      *
113      * @var array
114      * @access public
115      */
116     var $blackProtocols = array(
117         'about',   'chrome',     'data',       'disk',     'hcp',     
118         'help',    'javascript', 'livescript', 'lynxcgi',  'lynxexec', 
119         'ms-help', 'ms-its',     'mhtml',      'mocha',    'opera',   
120         'res',     'resource',   'shell',      'vbscript', 'view-source', 
121         'vnd.ms.radio',          'wysiwyg', 
122         );
123
124     /**
125      * List of "safe" protocols (used for whitelist-filtering)
126      *
127      * @var array
128      * @access public
129      */
130     var $whiteProtocols = array(
131         'ed2k',   'file', 'ftp',  'gopher', 'http',  'https', 
132         'irc',    'mailto', 'news', 'nntp', 'telnet', 'webcal', 
133         'xmpp',   'callto',
134         );
135
136     /**
137      * List of attributes that can contain protocols
138      *
139      * @var array
140      * @access public
141      */
142     var $protocolAttributes = array(
143         'action', 'background', 'codebase', 'dynsrc', 'href', 'lowsrc', 'src', 
144         );
145
146     /**
147      * List of dangerous CSS keywords
148      *
149      * Whole style="" attribute will be removed, if parser will find one of 
150      * these keywords
151      *
152      * @var array
153      * @access public
154      */
155     var $cssKeywords = array(
156         'absolute', 'behavior',       'behaviour',   'content', 'expression', 
157         'fixed',    'include-source', 'moz-binding',
158         );
159
160     /**
161      * List of tags that can have no "closing tag"
162      *
163      * @var array
164      * @access public
165      * @deprecated XHTML does not allow such tags
166      */
167     var $noClose = array();
168
169     /**
170      * List of block-level tags that terminates paragraph
171      *
172      * Paragraph will be closed when this tags opened
173      *
174      * @var array
175      * @access public
176      */
177     var $closeParagraph = array(
178         'address', 'blockquote', 'center', 'dd',      'dir',       'div', 
179         'dl',      'dt',         'h1',     'h2',      'h3',        'h4', 
180         'h5',      'h6',         'hr',     'isindex', 'listing',   'marquee', 
181         'menu',    'multicol',   'ol',     'p',       'plaintext', 'pre', 
182         'table',   'ul',         'xmp',
183         'hgroup', 'header'
184         );
185
186     /**
187      * List of table tags, all table tags outside a table will be removed
188      *
189      * @var array
190      * @access public
191      */
192     var $tableTags = array(
193         'caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', 
194         'thead',   'tr', 
195         );
196
197     /**
198      * List of list tags
199      *
200      * @var array
201      * @access public
202      */
203     var $listTags = array('dir', 'menu', 'ol', 'ul', 'dl', );
204
205     /**
206      * List of dangerous attributes
207      *
208      * @var array
209      * @access public
210      */
211     var $attributes = array('dynsrc', 'id', 'name', );
212
213     /**
214      * List of allowed "namespaced" attributes
215      *
216      * @var array
217      * @access public
218      */
219     var $attributesNS = array('xml:lang', );
220
221     var $hasText;
222     /**
223      * Constructs class
224      *
225      * @access public
226      */
227     function __construct($opts = array()) 
228     {
229         
230         foreach ($opts as $k =>$v) {
231             $this->$k = $v;
232         }
233         
234         //making regular expressions based on Proto & CSS arrays
235         foreach ($this->blackProtocols as $proto) {
236             $preg = "/[\s\x01-\x1F]*";
237             for ($i=0; $i<strlen($proto); $i++) {
238                 $preg .= $proto[$i] . "[\s\x01-\x1F]*";
239             }
240             $preg .= ":/i";
241             $this->_protoRegexps[] = $preg;
242         }
243
244         foreach ($this->cssKeywords as $css) {
245             $this->_cssRegexps[] = '/' . $css . '/i';
246         }
247         return true;
248     }
249
250     /**
251      * Handles the writing of attributes - called from $this->_openHandler()
252      *
253      * @param array $attrs array of attributes $name => $value
254      * @return boolean
255      * @access private
256      */
257     function _writeAttrs ($attrs) 
258     {
259         $ret = '';
260         if (is_array($attrs)) {
261             foreach ($attrs as $name => $value) {
262
263                 $name = strtolower($name);
264
265                 if (strpos($name, 'on') === 0) {
266                     continue;
267                 }
268                 if (strpos($name, 'data') === 0) {
269                     continue;
270                 }
271                 if (in_array($name, $this->attributes)) {
272                     continue;
273                 }
274                 if (!preg_match("/^[a-z0-9]+$/i", $name)) {
275                     if (!in_array($name, $this->attributesNS)) {
276                         continue;
277                     }
278                 }
279
280                 if (($value === TRUE) || (is_null($value))) {
281                     $value = $name;
282                 }
283
284                 if ($name == 'style') {
285                    
286                    // removes insignificant backslahes
287                    $value = str_replace("\\", '', $value);
288
289                    // removes CSS comments
290                     while (1)
291                     {
292                         $_value = preg_replace("!/\*.*?\*/!s", '', $value);
293                         if ($_value == $value) break;
294                         $value = $_value;
295                     }
296                    
297                     // replace all & to &amp;
298                     $value = str_replace('&amp;', '&', $value);
299                     $value = str_replace('&', '&amp;', $value);
300                     $value = $this->cleanStyle($value);
301                 }
302                 
303                 $tempval = preg_replace_callback('/&#(\d+);?/m', function($m) { return  chr($m[1]); } , $value); //"'
304                 $tempval = preg_replace_callback('/&#x([0-9a-f]+);?/mi', function($m) { return chr(hexdec($m[1])); } , $tempval);
305
306                 
307                 ///$tempval = preg_replace('/&#(\d+);?/me', "chr('\\1')", $value); //"'
308                 ///$tempval = preg_replace('/&#x([0-9a-f]+);?/mei', "chr(hexdec('\\1'))", $tempval);
309
310                 if ((in_array($name, $this->protocolAttributes)) && 
311                     (strpos($tempval, ':') !== false)) 
312                 {
313                     if ($this->protocolFiltering == 'black') {
314                         foreach ($this->_protoRegexps as $proto) {
315                             if (preg_match($proto, $tempval)) continue 2;
316                         }
317                     } else {
318                         $_tempval = explode(':', $tempval);
319                         $proto = $_tempval[0];
320                         if (!in_array($proto, $this->whiteProtocols)) {
321                             continue;
322                         }
323                     }
324                 }
325
326                 $value = str_replace("\"", "&quot;", $value);
327                 $ret .= ' ' . $name . '="' . $value . '"';
328             }
329         }
330         return $ret;
331     }
332     
333     function cleanStyle ($str)
334     {
335         static $is = false;
336         if (!$is) {
337             require_once 'HTML/CSS/InlineStyle.php';
338             $is = new HTML_CSS_InlineStyle();
339         }
340         $ar = $is->_styleToArray($str);
341         foreach($ar as $k=>$v) {
342             if (in_array(strtolower(trim($k)), $this->cssKeywords)) {
343                 //echo "Trashing BL css keyword $k=$v <br/>";
344                 unset($ar[$k]);
345                 continue;
346             }
347             foreach ($this->_protoRegexps as $proto) {
348                 if (preg_match($proto, $v)) {
349                     echo "$proto - Trashing $k=$v <br/>";
350                     unset($ar[$k]);
351                     continue 2;
352                 }
353             }
354              
355         }
356         $st = array();
357         foreach($ar as $prop => $val) {
358             $st[] = "{$prop}:{$val}";
359         }
360         return implode(';', $st);
361         
362     }
363     
364
365     /**
366      * Opening tag handler - called from HTMLSax
367      *
368      * @param object $parser HTML Parser
369      * @param string $name   tag name
370      * @param array  $attrs  tag attributes
371      * @return boolean
372      * @access private
373      */
374     function _openHandler($name, $attrs) 
375     {
376         $name = strtolower($name);
377
378         if (in_array($name, $this->deleteTagsContent)) {
379             return true;
380         }
381         
382         if (in_array($name, $this->deleteTags)) {
383             return false;
384         }
385         
386         if (!preg_match("/^[a-z0-9]+$/i", $name)) {
387             return false;
388             /*if (preg_match("!(?:\@|://)!i", $name)) {
389                 return '&lt;' . $name . '&gt;';
390                 $this->_xhtml .= '&lt;' . $name . '&gt;';
391             }
392             return true;
393             */
394         }
395         if (in_array(strtolower($name), $this->singleTags)) {
396             return '<' . $name . $this->_writeAttrs($attrs) . '/>';
397         }    
398         return '<' . $name . $this->_writeAttrs($attrs) . '>';
399         
400     }
401   
402     /*
403      * Main parsing fuction
404      *
405      * @param string $doc HTML document for processing
406      * @return string Processed (X)HTML document
407      * @access public
408      */
409     function parse($doc) 
410     {
411
412        // Save all '<' symbols
413        //$doc = preg_replace("/<(?=[^a-zA-Z\/\!\?\%])/", '&lt;', $doc);
414
415        // Web documents shouldn't contains \x00 symbol
416        //$doc = str_replace("\x00", '', $doc);
417
418        // Opera6 bug workaround
419        //$doc = str_replace("\xC0\xBC", '&lt;', $doc);
420
421        // UTF-7 encoding ASCII decode
422        //$doc = $this->repackUTF7($doc);
423
424         if (!extension_loaded('tidy')) {
425             dl('tidy.so');
426         }
427 //        print_r(strlen($doc));exit;
428         // too large!!!?
429         if (strlen($doc) > 1000000) {
430             $doc = substr($doc, 0, 1000000);
431         }
432         $tree = tidy_parse_string($doc,array(),'UTF8');
433         
434 //        print_r($tree);exit;
435         
436         return $this->tidyTree($tree->root());
437        // use tidy!!!!
438        
439         
440
441     }
442     
443     function parseFile($fn) 
444     {
445
446        // Save all '<' symbols
447        //$doc = preg_replace("/<(?=[^a-zA-Z\/\!\?\%])/", '&lt;', $doc);
448
449        // Web documents shouldn't contains \x00 symbol
450        //$doc = str_replace("\x00", '', $doc);
451
452        // Opera6 bug workaround
453        //$doc = str_replace("\xC0\xBC", '&lt;', $doc);
454
455        // UTF-7 encoding ASCII decode
456        //$doc = $this->repackUTF7($doc);
457
458         if (!extension_loaded('tidy')) {
459             die("Add tidy extension to extension.ini");
460         }
461         $tree = tidy_parse_file($fn,array(),'UTF8');
462         
463         
464         
465         return $this->tidyTree($tree->root());
466        // use tidy!!!!
467        
468         
469
470     }
471      
472     function tidyTree($node)
473     {
474 //         print_r($node);
475         $onode =  $node;
476         switch ($node->type) {
477             case TIDY_NODETYPE_TEXT:
478                 if (strlen(trim($node->value))) {
479                     $this->hasText = 1;
480                 }
481                 //echo htmlspecialchars($node->value);
482                 
483                 return $node->value;
484             case TIDY_NODETYPE_STARTEND:
485             case TIDY_NODETYPE_START:
486                 if (!empty($this->filter)) {
487                     $node = (object) (array) $node; // we can't work with the 
488                     
489                     $this->filter->apply($node);
490                 }
491                 break;
492             case TIDY_NODETYPE_END: // handled by start / singleTags..
493                 return;
494                 //$this->out .= "<". htmlspecialchars($node->name) .'/>';
495                 //return;
496             
497             case TIDY_NODETYPE_ROOT:
498                 break;
499             default:
500                 return;
501         }
502         //echo $node->name ."\n";
503         $add = '';
504         $begin = '';
505         $end = '';
506         if ($node->type != TIDY_NODETYPE_ROOT) {
507             //echo htmlspecialchars(print_r($node ,true));
508             $add = $this->_openHandler($node->name, empty($node->attribute) ? array() : $node->attribute);
509             if (is_string($add)) {
510                 $begin .= $add;
511                 if (!in_array(strtolower($node->name), $this->singleTags)) {
512                     $cr = strtolower($node->name) == 'pre' ? '' : "\n";
513                     $end = $cr . '</' . $node->name . '>';
514                 }
515                  
516             }
517             if ($add === true) {
518                 return ''; // delete this tag and all the contents..
519             }
520         }
521          
522                 // include children...
523         if(!$onode->hasChildren()){
524             return $begin . $end;
525         }
526         foreach($onode->child as $child){
527            // echo "child of ". $node->name . ':' . $child->type . "\n";
528             $begin .= $this->tidyTree($child);
529         }
530         return $begin . $end;
531              
532             
533             
534     }
535
536     /**
537      * UTF-7 decoding fuction
538      *
539      * @param string $str HTML document for recode ASCII part of UTF-7 back to ASCII
540      * @return string Decoded document
541      * @access private
542      */
543     function repackUTF7($str)
544     {
545        return preg_replace_callback('!\+([0-9a-zA-Z/]+)\-!', array($this, 'repackUTF7Callback'), $str);
546     }
547
548     /**
549      * Additional UTF-7 decoding fuction
550      *
551      * @param string $str String for recode ASCII part of UTF-7 back to ASCII
552      * @return string Recoded string
553      * @access private
554      */
555     function repackUTF7Callback($str)
556     {
557        $str = base64_decode($str[1]);
558        $str = preg_replace_callback('/^((?:\x00.)*)((?:[^\x00].)+)/', array($this, 'repackUTF7Back'), $str);
559        return preg_replace('/\x00(.)/', '$1', $str);
560     }
561
562     /**
563      * Additional UTF-7 encoding fuction
564      *
565      * @param string $str String for recode ASCII part of UTF-7 back to ASCII
566      * @return string Recoded string
567      * @access private
568      */
569     function repackUTF7Back($str)
570     {
571        return $str[1].'+'.rtrim(base64_encode($str[2]), '=').'-';
572     }
573 }
574
575 /*
576  * Local variables:
577  * tab-width: 4
578  * c-basic-offset: 4
579  * c-hanging-comment-ender-p: nil
580  * End:
581  */
582