HTML/FlexyFramework/Generator.php
[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 HTML_Safe($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 //         print_r($node);
473         
474         switch ($node->type) {
475             case TIDY_NODETYPE_TEXT:
476                 if (strlen(trim($node->value))) {
477                     $this->hasText = 1;
478                 }
479                 //echo htmlspecialchars($node->value);
480                 
481                 return $node->value;
482             case TIDY_NODETYPE_STARTEND:
483             case TIDY_NODETYPE_START:
484                 if (!empty($this->filter)) {
485                     $this->filter->apply($node);
486                 }
487                 break;
488             case TIDY_NODETYPE_END: // handled by start / singleTags..
489                 return;
490                 //$this->out .= "<". htmlspecialchars($node->name) .'/>';
491                 //return;
492             
493             case TIDY_NODETYPE_ROOT:
494                 break;
495             default:
496                 return;
497         }
498         //echo $node->name ."\n";
499         $add = '';
500         $begin = '';
501         $end = '';
502         if ($node->type != TIDY_NODETYPE_ROOT) {
503             //echo htmlspecialchars(print_r($node ,true));
504             $add = $this->_openHandler($node->name, empty($node->attribute) ? array() : $node->attribute);
505             if (is_string($add)) {
506                 $begin .= $add;
507                 if (!in_array(strtolower($node->name), $this->singleTags)) {
508                     $cr = strtolower($node->name) == 'pre' ? '' : "\n";
509                     $end = $cr . '</' . $node->name . '>';
510                 }
511                  
512             }
513             if ($add === true) {
514                 return ''; // delete this tag and all the contents..
515             }
516         }
517          
518                 // include children...
519         if(!$node->hasChildren()){
520             return $begin . $end;
521         }
522         foreach($node->child as $child){
523            // echo "child of ". $node->name . ':' . $child->type . "\n";
524             $begin .= $this->tidyTree($child);
525         }
526         return $begin . $end;
527              
528             
529             
530     }
531
532     /**
533      * UTF-7 decoding fuction
534      *
535      * @param string $str HTML document for recode ASCII part of UTF-7 back to ASCII
536      * @return string Decoded document
537      * @access private
538      */
539     function repackUTF7($str)
540     {
541        return preg_replace_callback('!\+([0-9a-zA-Z/]+)\-!', array($this, 'repackUTF7Callback'), $str);
542     }
543
544     /**
545      * Additional UTF-7 decoding fuction
546      *
547      * @param string $str String for recode ASCII part of UTF-7 back to ASCII
548      * @return string Recoded string
549      * @access private
550      */
551     function repackUTF7Callback($str)
552     {
553        $str = base64_decode($str[1]);
554        $str = preg_replace_callback('/^((?:\x00.)*)((?:[^\x00].)+)/', array($this, 'repackUTF7Back'), $str);
555        return preg_replace('/\x00(.)/', '$1', $str);
556     }
557
558     /**
559      * Additional UTF-7 encoding fuction
560      *
561      * @param string $str String for recode ASCII part of UTF-7 back to ASCII
562      * @return string Recoded string
563      * @access private
564      */
565     function repackUTF7Back($str)
566     {
567        return $str[1].'+'.rtrim(base64_encode($str[2]), '=').'-';
568     }
569 }
570
571 /*
572  * Local variables:
573  * tab-width: 4
574  * c-basic-offset: 4
575  * c-hanging-comment-ender-p: nil
576  * End:
577  */
578