1<?php
2/**
3* Plugin Skeleton: Displays "Hello World!"
4*
5* @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
6* @author Christopher Smith <chris@jalakai.co.uk>
7*/
8
9if (!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/');
10if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
11require_once(DOKU_PLUGIN . 'syntax.php');
12require_once(DOKU_INC . 'inc/cache.php');
13
14define('HTML_OK_INVALIDID', 0);
15define('HTML_OK_NOTSUPPORTED', 1);
16define('HTML_OK_EXCLUDEDWINDOW', 2);
17define('HTML_OK_BADCLASSNAME', 3);
18define('HTML_OK_BADSELECTOR', 4);
19define('HTML_OK_BADFNNAME', 5);
20// ini_set('display_errors', "on");
21// ini_set('error_reporting', E_ALL);
22/**
23* All DokuWiki plugins to extend the parser/rendering mechanism
24* need to inherit from this class
25*/
26class syntax_plugin_htmlOKay extends DokuWiki_Syntax_Plugin
27{
28    // $access_levels = array('none'=>0, 'strict'=>1, 'medium'=>2, 'lax'=>3, 'su'=>4));
29    var $access_level = 0;
30    var $client;
31    var $msgs;
32    var $htmlOK_errors;
33    var $cycle = 0;
34    var $open_div = 0;
35    var $closed_div = 0;
36    var $divs_reported = false;
37    var $JS_ErrString = "";
38    var $helper;
39
40    function __construct()
41    {
42
43        $this->htmlOK_errors = array('Invalid ID', 'Element or Attribute not supported',
44            'Internal Window Elements Not Supported', 'Invalid CSS Class name(s)',
45            'ID Selectors not supported', 'Invalid Javascript function name(s)');
46
47        $this->msgs = "";
48
49    }
50
51
52    /**
53    * What kind of syntax are we?
54    */
55    function getType()
56    {
57        return 'protected';
58    }
59
60    /**
61    * What about paragraphs? (optional)
62    */
63    function getPType()
64    {
65        return 'block';
66    }
67
68    /**
69    * Where to sort in?
70    */
71    function getSort()
72    {
73        return 180;
74    }
75
76    /**
77    * Connect pattern to lexer
78    *       $this->Lexer->addPattern('<(?i)\w+\s+.*?ID\s*=.*?>','plugin_htmlOKay');
79    *       $this->Lexer->addPattern('<(?i)\w+\s+.*?CLASS\s*=.*?>','plugin_htmlOKay');
80    */
81    function connectTo($mode)
82    {
83
84        $this->cycle++;
85
86        $this->Lexer->addEntryPattern('<html>(?=.*?</html>)', $mode, 'plugin_htmlOKay');
87        $this->Lexer->addPattern('.<(?i)IFRAME.*?/IFRAME\s*>', 'plugin_htmlOKay');
88        $this->Lexer->addPattern('.<(?i)ILAYER.*?/ILAYER\s*>', 'plugin_htmlOKay');
89        $this->Lexer->addPattern('<(?i)a.*?javascript.*?</a\s*>', 'plugin_htmlOKay');
90        $this->Lexer->addPattern('<(?i)FORM.*?</FORM\s*>', 'plugin_htmlOKay');
91        $this->Lexer->addPattern('<(?i)DIV.*?>', 'plugin_htmlOKay');
92        $this->Lexer->addPattern('<(?i)/DIV.*?>', 'plugin_htmlOKay');
93        $this->Lexer->addPattern('.<(?i)STYLE.*?/STYLE\s*>', 'plugin_htmlOKay');
94        $this->Lexer->addPattern('<(?i)SCRIPT.*?/script\s*>', 'plugin_htmlOKay');
95        $this->Lexer->addPattern('<(?i)TABLE.*?</TABLE\s*>', 'plugin_htmlOKay');
96
97        $this->Lexer->addPattern('(?i)ID\s*=\s*\W?.*?\W', 'plugin_htmlOKay');
98        $this->Lexer->addPattern('(?i)class\s*=\s*\W?.*?\W', 'plugin_htmlOKay');
99      }
100
101    function postConnect()
102    {
103        $this->Lexer->addExitPattern('</html>', 'plugin_htmlOKay');
104    }
105
106    /**
107    *   level 2 permissions:  guarded access
108    */
109    function rewrite_match_medium($match)
110    {
111        if (preg_match('/<FORM(.*?)>/i', $match, $matches))
112        {
113            if (preg_match('/(action)/i', $matches[1], $action))
114            {
115                return $this->getError(HTML_OK_NOTSUPPORTED, $match, $action[1]);
116            } elseif (preg_match('/(onsubmit)/i', $matches[1], $onsubmit))
117            {
118                return $this->getError(HTML_OK_NOTSUPPORTED, $match, $onsubmit[1]);
119            }
120        }
121        elseif (preg_match('/<script/i', $match))
122        {
123            return $this->script_matches($match, 2);
124        }
125        elseif (preg_match('/<IFRAME/i', $match, $matches))
126        {
127            return $this->getError(HTML_OK_EXCLUDEDWINDOW, $match, "IFRAME");
128        }
129        elseif (preg_match('/<ILAYER/i', $match, $matches))
130        {
131            return $this->getError(HTML_OK_EXCLUDEDWINDOW, $match, "ILAYER");
132        }
133        elseif (preg_match('/<(DIV)/i', $match, $matches))
134        {
135            $this->open_div++;
136            return $this->getError(HTML_OK_NOTSUPPORTED, $match, $matches[1], "div");
137        }
138        elseif (preg_match('/<a.*?javascript.*?<\/a\s*>/i', $match))
139        {
140            return $this->getError(HTML_OK_NOTSUPPORTED, $match, "javascript urls");
141        }
142        elseif (preg_match('/<STYLE/i', $match))
143        {
144            return $this->style_matches($match, 2);
145        }
146        elseif (preg_match('/<TABLE/i', $match, $matches))
147        {
148            return $this->getError(HTML_OK_NOTSUPPORTED, $match, "TABLE");
149        }
150
151        $retv = $this->class_id_matches($match);
152        if ($retv !== false) return $retv;
153
154        return $match;
155    }
156
157    /**
158    * treat level 1 permissions:  restricted access
159    */
160
161    function rewrite_match_strict($match)
162    {
163        if (preg_match('/<FORM/i', $match))
164        {
165            return $this->getError(HTML_OK_NOTSUPPORTED, $match, 'FORM');
166
167        } elseif (preg_match('/<script/i', $match))
168        {
169            return $this->getError(HTML_OK_NOTSUPPORTED, $match, 'SCRIPT');
170
171        } elseif (preg_match('/<IFRAME/i', $match, $matches))
172        {
173            return $this->getError(HTML_OK_EXCLUDEDWINDOW, $match, "IFRAME");
174        }
175        elseif (preg_match('/<ILAYER/i', $match, $matches))
176        {
177            return $this->getError(HTML_OK_EXCLUDEDWINDOW, $match, "ILAYER");
178
179        } elseif (preg_match('/<(DIV)/i', $match, $matches))
180        {
181            $this->open_div++;
182            return $this->getError(HTML_OK_NOTSUPPORTED, $match, $matches[1], "div");
183
184        } elseif (preg_match('/<TABLE/i', $match, $matches))
185        {
186            return $this->getError(HTML_OK_NOTSUPPORTED, $match, "TABLE");
187
188        } elseif (preg_match('/<STYLE/i', $match))
189        {
190            return $this->getError(HTML_OK_NOTSUPPORTED, $match, "STYLE");
191
192        } elseif (preg_match('/<a.*?javascript.*?<\/a\s*>/i', $match))
193        {
194            return $this->getError(HTML_OK_NOTSUPPORTED, $match, "javascript urls");
195        }
196
197        $retv = $this->class_id_matches($match);
198        if ($retv !== false) return $retv;
199
200        return $match;
201    }
202
203    function rewrite_match_lax($match)
204    {
205        $div = false;
206        if (preg_match('/<FORM(.*?)>/i', $match, $matches))
207        {
208            if (preg_match('/action/i', $matches[1]))
209            {
210                return $this->getError(HTML_OK_NOTSUPPORTED, $match, $matches[1], "form");
211            }
212
213        } elseif (preg_match('/<STYLE/i', $match))
214        {
215            $match = $this->style_matches($match, 3);
216            return $match;
217
218        } elseif (preg_match('/<(IFRAME|ILAYER)/i', $match, $matches))
219        {
220            return $this->getError(HTML_OK_EXCLUDEDWINDOW, $match, $matches[1], "");
221
222        } elseif (preg_match('/<script/i', $match))
223        {
224            return $this->script_matches($match, 3);
225
226        } elseif (preg_match('/<(DIV)/i', $match))
227        {
228            $div = true;
229            $this->open_div++;
230        }
231
232        $retv = $this->class_id_matches($match, $div);
233        if ($retv !== false) return $retv;
234
235        return $match;
236    }
237
238    /**
239    * Super-user access
240    */
241    function rewrite_match_su($match)
242    {
243        global $conf;
244        if($conf['plugin']['htmlOKay']['su_unrestricted'])
245           return $match;
246
247        $div = false;
248        if (preg_match('/<STYLE/i', $match))
249        {
250            $match = $this->style_matches($match, 4);
251            return $match;
252        } elseif (preg_match('/<(DIV)/i', $match))
253        {
254            $div = true;
255            $this->open_div++;
256        }
257
258        $retv = $this->class_id_matches($match, $div);
259        if ($retv !== false) return $retv;
260
261        return $match;
262    }
263
264    function script_matches($match, $level)
265    {
266        if ($level < 3)
267        {
268            if (preg_match('/\blocation\b/', $match))
269            {
270                return $this->getError(HTML_OK_NOTSUPPORTED, $match, "location");
271            }
272
273            if (preg_match('/(ActiveX|XMLHttpRequest)/i', $match, $matches))
274            {
275                return $this->getError(HTML_OK_NOTSUPPORTED, $match, $matches[1]);
276            }
277
278            if (preg_match('/(onsubmit|addEventListener|createEvent|attachEvent|captureEvents)/i', $match, $matches))
279            {
280                return $this->getError(HTML_OK_NOTSUPPORTED, $match, $matches[1]);
281            }
282        }
283
284        if (preg_match_all('/function\s+(.*?)[\s\n]*\(/i', $match, $matches))
285        {
286            $err = array();
287            foreach($matches[1] as $index => $m)
288            {
289                if(!preg_match('/^htmlO_K_/', $m)) {
290                    $err[] = $m;
291                }
292            }
293            if (count($err))
294            {
295                return $this->getError(HTML_OK_BADFNNAME, $match, $err);
296            }
297        }
298
299        return $match;
300    }
301
302    function class_id_matches($match, $div = "")
303    {
304        if (preg_match('/id\s*=\s*\W?(.*)/i', $match, $matches))
305        {
306            if (isset($matches[1]) && !preg_match('/^htmlO_K_/', $matches[1]))
307            {
308                if ($div) $div = 'div';
309                $value = rtrim($matches[1], ' ">');
310                return $this->getError(HTML_OK_INVALIDID, $match, $value, $div);
311            }
312        }
313
314        if (preg_match('/class\s*=\s*\W?(.*)/i', $match, $matches))
315        {
316            if (isset($matches[1]) && !preg_match('/^htmlO_K_/', $matches[1]))
317            {
318                if ($div) $div = 'div';
319                $value = rtrim($matches[1], ' "');
320                return $this->getError(HTML_OK_INVALIDID, $match, $value, $div);
321            }
322        }
323
324        return false;
325    }
326
327
328    /**
329    *
330    *   level 2: no use of #id's, check all for class name errors, missing htmlO_K_
331    */
332    function style_matches($match, $level)
333    {
334        if (!isset($match)) return "";
335        // medium: no use of id's
336        if ($level == 2 && preg_match_all('/(#\w+)/', $match, $matches))
337        {
338            $err = array();
339            foreach($matches[1] as $index => $m)
340            {
341                $err[] = $m;
342            }
343            if (count($err))
344            {
345                return $this->getError(HTML_OK_BADSELECTOR, $match, $err);
346            }
347        }
348
349        if (preg_match_all('/\.(\w+)/', $match, $matches))
350        {
351            $err = array();
352            foreach($matches[1] as $index => $m)
353            {
354                if (!preg_match('/^htmlO_K_/', $m))
355                {
356                    $err[] = $m;
357                }
358            }
359            if (count($err))
360            {
361                return $this->getError(HTML_OK_BADCLASSNAME, $match, $err);
362            }
363        }
364        return $match;
365    }
366
367    /**
368    * Handle the match
369    */
370    function handle($match, $state, $pos, Doku_Handler $handler)
371    {
372        global $conf;
373        $this->pos = $pos;
374
375        $this->helper = plugin_load('helper', 'htmlOKay');
376        $this->helper->set_permissions();
377        $this->access_level = $this->helper->get_access();
378
379        if (!$conf['htmlok'])
380        {
381            $match = preg_replace ('/</', '&lt;', $match) . '<br />';
382            return array($state, $match);
383        }
384        elseif($this->JS_ErrString) {
385              echo $this->JS_ErrString;
386              $this->JS_ErrString = false;
387        }
388
389        switch ($state)
390        {
391            case DOKU_LEXER_ENTER :
392                return array($state, $match);
393                break;
394
395            case DOKU_LEXER_MATCHED :
396                if (preg_match('/\/DIV\s*>/i', $match))
397                {
398                    $this->closed_div++;
399                }
400                if ($this->access_level == 1)
401                {
402                    $match = $this->rewrite_match_strict($match);
403                } elseif ($this->access_level == 2)
404                {
405                    $match = $this->rewrite_match_medium($match);
406                } elseif ($this->access_level == 3)
407                {
408                    $match = $this->rewrite_match_lax($match);
409                } elseif ($this->access_level == 4)
410                {
411                    $match = $this->rewrite_match_su($match);
412                }
413
414                return array($state, $match);
415                break;
416
417            case DOKU_LEXER_UNMATCHED :
418                if (preg_match('/<h(\d)>.*?<\/\\1>/i', $match, $matches))
419                {
420                    $match = preg_replace('/<h\d>/', '<h' . $matches[1] . '  style="border-bottom:0px;">', $match);
421
422                }
423
424                return array($state, $match);
425                break;
426
427            case DOKU_LEXER_EXIT :
428                if ($this->open_div != $this->closed_div)
429                {
430                    if (!$this->divs_reported)
431                    //    echo $this->get_JSErrString("<b>Mismatched Div Elements:</b> Open Divs: {$this->open_div}; Closed Divs:  {$this->closed_div}");
432                    $this->divs_reported = true;
433                }
434
435                return array($state, $match);
436                break;
437
438            case DOKU_LEXER_SPECIAL :
439                return array($state, $match);
440
441                break;
442        }
443        return array();
444    }
445
446    /**
447    * Create output
448    */
449
450    function render($mode, Doku_Renderer $renderer, $data)
451    {
452        if ($mode == 'xhtml')
453        {
454            list($state, $match) = $data;
455            switch ($state)
456            {
457                case DOKU_LEXER_ENTER :
458                    break;
459                case DOKU_LEXER_UNMATCHED :
460                    $renderer->doc .= $match;
461                    break;
462                case DOKU_LEXER_MATCHED :
463                    $renderer->doc .= $match;
464                    break;
465                case DOKU_LEXER_EXIT :
466
467                    break;
468            }
469
470            return true;
471        }
472       if($mode = 'metadata')
473        {         //   msg('<pre>'. print_r($renderer->meta,1) . '</pre>' );
474                     $renderer->meta['relation']['htmlokay'] = time();
475             return true;
476        }
477
478        return false;
479    }
480
481    /*
482    * $htmlOK_ERRORS = array('Invalid ID', 'Element not supported', 'Internal Window Elements Not Supported');
483    *
484    */
485
486    function getError($TYPE, $match, $problem_str, $xtra = "")
487    {
488        $error_string = "";
489
490        if ($xtra) $xtra = "<{$xtra}>";
491        if ($TYPE == HTML_OK_INVALIDID)
492        {
493            $xtra = ">{$xtra}";
494        }
495
496        $error_string = "{$xtra}<center><dl style='border-width:0px 0px 0px 0px; border-color: #ffffff; '><DT><DD><TABLE WIDTH='80%' cellpadding='10' border>\n"
497         . "<TR><TD  align='center' style='background-color:#eeeeee;font-weight:normal; font-size: 10pt; font-family:sans-serif;'>\n";
498
499        if (is_string($problem_str))
500        {
501            $error_string .= compact_string(preg_replace ('/</', '&lt;', $match)) . '<br />';
502            $problem_str = htmlspecialchars ($problem_str, ENT_QUOTES);
503        }
504
505        switch ($TYPE)
506        {
507            case HTML_OK_INVALIDID:
508                $error_string .= $this->htmlOK_errors[$TYPE] . ": <b>{$problem_str}</b><br />";
509                $js = $this->htmlOK_errors[$TYPE] . '.'
510                 . "  htmlO_K_ prefix required for all ID's: <b>htmlO_K_{$problem_str}</b>";
511                $error_string .= $this->get_JSErrString($js);
512                break;
513
514            case HTML_OK_NOTSUPPORTED:
515                $error_string .= $this->htmlOK_errors[$TYPE] . ": <b>{$problem_str}</b><br />";
516                $js = $this->htmlOK_errors[$TYPE] . " at current HTML access level:  <b>{$problem_str}</b>";
517                $error_string .= $this->get_JSErrString($js);
518                break;
519
520            case HTML_OK_EXCLUDEDWINDOW:
521                $error_string .= $this->htmlOK_errors[$TYPE] . ": <b>{$problem_str}</b><br />";
522                $js = $this->htmlOK_errors[$TYPE] . ". External files cannot be included in wiki documents: <b>{$problem_str}</b>. ";
523                $error_string .= $this->get_JSErrString($js);
524                break;
525
526            case HTML_OK_BADCLASSNAME:
527            case HTML_OK_BADSELECTOR:
528            case HTML_OK_BADFNNAME:
529                $error_string .= $this->htmlOK_errors[$TYPE] . ':<BR />';
530                $name_errs = "";
531                foreach($problem_str as $p)
532                {
533                    $p = htmlspecialchars ($p, ENT_QUOTES);
534                    $name_errs .= " {$p}, ";
535                }
536
537                $name_errs = rtrim($name_errs, ' ,');
538                $name_errs = "<b>$name_errs</b>";
539                $error_string .= $name_errs;
540                if ($TYPE == HTML_OK_BADCLASSNAME)
541                {
542                    $js = $this->htmlOK_errors[$TYPE] . ". htmlO_K_ prefix required for class names: <b>{$name_errs}</b>. ";
543                } elseif ($TYPE == HTML_OK_BADFNNAME)
544                {
545                    $js = $this->htmlOK_errors[$TYPE] . ". htmlO_K_ prefix required for function names:<BR />&nbsp;&nbsp;&nbsp;&nbsp;<b>{$name_errs}</b>. ";
546                }
547                else
548                {
549                    $js = $this->htmlOK_errors[$TYPE] . "  at current HTML access level:   <b>{$name_errs}</b>. ";
550                }
551                $error_string .= $this->get_JSErrString($js);
552                break;
553
554            default:
555                break;
556        }
557
558        return $error_string . '</TR></TD></TABLE></dl></center><br />';
559    }
560
561    /**
562    * Constructs the Javascript Error String for output in the Errors window
563    */
564    function get_JSErrString($msg)
565    {
566        global $INFO;
567        $msg = trim($msg);
568        static $msgs_inx = -1;
569
570        if (!isset($msg) || empty($msg)) return ";";
571        $msgs_inx++;
572        $msg = '<script language="javascript">htmlOK_ERRORS_ARRAY[' . $msgs_inx . ']="' . $msg . '"; </script>' . "\n";
573        return $msg;
574    }
575}
576
577function compact_string($string_x)
578{
579    if ($len = strlen($string_x) > 400)
580    {
581
582        $string_a = substr($string_x, 0, 200);
583        $string_b = substr($string_x, -200);
584        $string_x = $string_a . '<br /><b>. . .</b><br />' . $string_b;
585    }
586
587    return $string_x;
588}
589
590?>
591