try and fix tidy node modifications
[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     /**
222      * Constructs class
223      *
224      * @access public
225      */
226     function __construct($opts = array()) 
227     {
228         
229         foreach ($opts as $k =>$v) {
230             $this->$k = $v;
231         }
232         
233         //making regular expressions based on Proto & CSS arrays
234         foreach ($this->blackProtocols as $proto) {
235             $preg = "/[\s\x01-\x1F]*";
236             for ($i=0; $i<strlen($proto); $i++) {
237                 $preg .= $proto[$i] . "[\s\x01-\x1F]*";
238             }
239             $preg .= ":/i";
240             $this->_protoRegexps[] = $preg;
241         }
242
243         foreach ($this->cssKeywords as $css) {
244             $this->_cssRegexps[] = '/' . $css . '/i';
245         }
246         return true;
247     }
248
249     /**
250      * Handles the writing of attributes - called from $this->_openHandler()
251      *
252      * @param array $attrs array of attributes $name => $value
253      * @return boolean
254      * @access private
255      */
256     function _writeAttrs ($attrs) 
257     {
258         $ret = '';
259         if (is_array($attrs)) {
260             foreach ($attrs as $name => $value) {
261
262                 $name = strtolower($name);
263
264                 if (strpos($name, 'on') === 0) {
265                     continue;
266                 }
267                 if (strpos($name, 'data') === 0) {
268                     continue;
269                 }
270                 if (in_array($name, $this->attributes)) {
271                     continue;
272                 }
273                 if (!preg_match("/^[a-z0-9]+$/i", $name)) {
274                     if (!in_array($name, $this->attributesNS)) {
275                         continue;
276                     }
277                 }
278
279                 if (($value === TRUE) || (is_null($value))) {
280                     $value = $name;
281                 }
282
283                 if ($name == 'style') {
284                    
285                    // removes insignificant backslahes
286                    $value = str_replace("\\", '', $value);
287
288                    // removes CSS comments
289                     while (1)
290                     {
291                         $_value = preg_replace("!/\*.*?\*/!s", '', $value);
292                         if ($_value == $value) break;
293                         $value = $_value;
294                     }
295                    
296                     // replace all & to &amp;
297                     $value = str_replace('&amp;', '&', $value);
298                     $value = str_replace('&', '&amp;', $value);
299                     $value = $this->cleanStyle($value);
300                 }
301                 
302                 $tempval = preg_replace_callback('/&#(\d+);?/m', function($m) { return  chr($m[1]); } , $value); //"'
303                 $tempval = preg_replace_callback('/&#x([0-9a-f]+);?/mi', function($m) { return chr(hexdec($m[1])); } , $tempval);
304
305                 
306                 ///$tempval = preg_replace('/&#(\d+);?/me', "chr('\\1')", $value); //"'
307                 ///$tempval = preg_replace('/&#x([0-9a-f]+);?/mei', "chr(hexdec('\\1'))", $tempval);
308
309                 if ((in_array($name, $this->protocolAttributes)) && 
310                     (strpos($tempval, ':') !== false)) 
311                 {
312                     if ($this->protocolFiltering == 'black') {
313                         foreach ($this->_protoRegexps as $proto) {
314                             if (preg_match($proto, $tempval)) continue 2;
315                         }
316                     } else {
317                         $_tempval = explode(':', $tempval);
318                         $proto = $_tempval[0];
319                         if (!in_array($proto, $this->whiteProtocols)) {
320                             continue;
321                         }
322                     }
323                 }
324
325                 $value = str_replace("\"", "&quot;", $value);
326                 $ret .= ' ' . $name . '="' . $value . '"';
327             }
328         }
329         return $ret;
330     }
331     
332     function cleanStyle ($str)
333     {
334         static $is = false;
335         if (!$is) {
336             require_once 'HTML/CSS/InlineStyle.php';
337             $is = new HTML_CSS_InlineStyle();
338         }
339         $ar = $is->_styleToArray($str);
340         foreach($ar as $k=>$v) {
341             if (in_array(strtolower(trim($k)), $this->cssKeywords)) {
342                 //echo "Trashing BL css keyword $k=$v <br/>";
343                 unset($ar[$k]);
344                 continue;
345             }
346             foreach ($this->_protoRegexps as $proto) {
347                 if (preg_match($proto, $v)) {
348                     echo "$proto - Trashing $k=$v <br/>";
349                     unset($ar[$k]);
350                     continue 2;
351                 }
352             }
353              
354         }
355         $st = array();
356         foreach($ar as $prop => $val) {
357             $st[] = "{$prop}:{$val}";
358         }
359         return implode(';', $st);
360         
361     }
362     
363
364     /**
365      * Opening tag handler - called from HTMLSax
366      *
367      * @param object $parser HTML Parser
368      * @param string $name   tag name
369      * @param array  $attrs  tag attributes
370      * @return boolean
371      * @access private
372      */
373     function _openHandler($name, $attrs) 
374     {
375         $name = strtolower($name);
376
377         if (in_array($name, $this->deleteTagsContent)) {
378             return true;
379         }
380         
381         if (in_array($name, $this->deleteTags)) {
382             return false;
383         }
384         
385         if (!preg_match("/^[a-z0-9]+$/i", $name)) {
386             return false;
387             /*if (preg_match("!(?:\@|://)!i", $name)) {
388                 return '&lt;' . $name . '&gt;';
389                 $this->_xhtml .= '&lt;' . $name . '&gt;';
390             }
391             return true;
392             */
393         }
394         if (in_array(strtolower($name), $this->singleTags)) {
395             return '<' . $name . $this->_writeAttrs($attrs) . '/>';
396         }    
397         return '<' . $name . $this->_writeAttrs($attrs) . '>';
398         
399     }
400   
401     /*
402      * Main parsing fuction
403      *
404      * @param string $doc HTML document for processing
405      * @return string Processed (X)HTML document
406      * @access public
407      */
408     function parse($doc) 
409     {
410
411        // Save all '<' symbols
412        //$doc = preg_replace("/<(?=[^a-zA-Z\/\!\?\%])/", '&lt;', $doc);
413
414        // Web documents shouldn't contains \x00 symbol
415        //$doc = str_replace("\x00", '', $doc);
416
417        // Opera6 bug workaround
418        //$doc = str_replace("\xC0\xBC", '&lt;', $doc);
419
420        // UTF-7 encoding ASCII decode
421        //$doc = $this->repackUTF7($doc);
422
423         if (!extension_loaded('tidy')) {
424             dl('tidy.so');
425         }
426 //        print_r(strlen($doc));exit;
427         // too large!!!?
428         if (strlen($doc) > 1000000) {
429             $doc = substr($doc, 0, 1000000);
430         }
431         $tree = tidy_parse_string($doc,array(),'UTF8');
432         
433 //        print_r($tree);exit;
434         
435         return $this->tidyTree($tree->root());
436        // use tidy!!!!
437        
438         
439
440     }
441     
442     function parseFile($fn) 
443     {
444
445        // Save all '<' symbols
446        //$doc = preg_replace("/<(?=[^a-zA-Z\/\!\?\%])/", '&lt;', $doc);
447
448        // Web documents shouldn't contains \x00 symbol
449        //$doc = str_replace("\x00", '', $doc);
450
451        // Opera6 bug workaround
452        //$doc = str_replace("\xC0\xBC", '&lt;', $doc);
453
454        // UTF-7 encoding ASCII decode
455        //$doc = $this->repackUTF7($doc);
456
457         if (!extension_loaded('tidy')) {
458             die("Add tidy extension to extension.ini");
459         }
460         $tree = tidy_parse_file($fn,array(),'UTF8');
461         
462         
463         
464         return $this->tidyTree($tree->root());
465        // use tidy!!!!
466        
467         
468
469     }
470     
471     function tidyTree($node)
472     {
473 //         print_r($node);
474         $onode =  $node;
475         switch ($node->type) {
476             case TIDY_NODETYPE_TEXT:
477                 if (strlen(trim($node->value))) {
478                     $this->hasText = 1;
479                 }
480                 //echo htmlspecialchars($node->value);
481                 
482                 return $node->value;
483             case TIDY_NODETYPE_STARTEND:
484             case TIDY_NODETYPE_START:
485                 if (!empty($this->filter)) {
486                     $node = (object) (array) $node; // we can't work with the 
487                     
488                     $this->filter->apply($node);
489                 }
490                 break;
491             case TIDY_NODETYPE_END: // handled by start / singleTags..
492                 return;
493                 //$this->out .= "<". htmlspecialchars($node->name) .'/>';
494                 //return;
495             
496             case TIDY_NODETYPE_ROOT:
497                 break;
498             default:
499                 return;
500         }
501         //echo $node->name ."\n";
502         $add = '';
503         $begin = '';
504         $end = '';
505         if ($node->type != TIDY_NODETYPE_ROOT) {
506             //echo htmlspecialchars(print_r($node ,true));
507             $add = $this->_openHandler($node->name, empty($node->attribute) ? array() : $node->attribute);
508             if (is_string($add)) {
509                 $begin .= $add;
510                 if (!in_array(strtolower($node->name), $this->singleTags)) {
511                     $cr = strtolower($node->name) == 'pre' ? '' : "\n";
512                     $end = $cr . '</' . $node->name . '>';
513                 }
514                  
515             }
516             if ($add === true) {
517                 return ''; // delete this tag and all the contents..
518             }
519         }
520          
521                 // include children...
522         if(!$onode->hasChildren()){
523             return $begin . $end;
524         }
525         foreach($onode->child as $child){
526            // echo "child of ". $node->name . ':' . $child->type . "\n";
527             $begin .= $this->tidyTree($child);
528         }
529         return $begin . $end;
530              
531             
532             
533     }
534
535     /**
536      * UTF-7 decoding fuction
537      *
538      * @param string $str HTML document for recode ASCII part of UTF-7 back to ASCII
539      * @return string Decoded document
540      * @access private
541      */
542     function repackUTF7($str)
543     {
544        return preg_replace_callback('!\+([0-9a-zA-Z/]+)\-!', array($this, 'repackUTF7Callback'), $str);
545     }
546
547     /**
548      * Additional UTF-7 decoding fuction
549      *
550      * @param string $str String for recode ASCII part of UTF-7 back to ASCII
551      * @return string Recoded string
552      * @access private
553      */
554     function repackUTF7Callback($str)
555     {
556        $str = base64_decode($str[1]);
557        $str = preg_replace_callback('/^((?:\x00.)*)((?:[^\x00].)+)/', array($this, 'repackUTF7Back'), $str);
558        return preg_replace('/\x00(.)/', '$1', $str);
559     }
560
561     /**
562      * Additional UTF-7 encoding fuction
563      *
564      * @param string $str String for recode ASCII part of UTF-7 back to ASCII
565      * @return string Recoded string
566      * @access private
567      */
568     function repackUTF7Back($str)
569     {
570        return $str[1].'+'.rtrim(base64_encode($str[2]), '=').'-';
571     }
572 }
573
574 /*
575  * Local variables:
576  * tab-width: 4
577  * c-basic-offset: 4
578  * c-hanging-comment-ender-p: nil
579  * End:
580  */
581