1<?php
2/**
3 * Plugin Webcode: Show webcode (Css, HTML) in a iframe
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Nicolas GERARD
7 */
8
9// must be run within Dokuwiki
10use dokuwiki\Extension\Event;
11
12if (!defined('DOKU_INC')) die();
13
14/**
15 * All DokuWiki plugins to extend the parser/rendering mechanism
16 * need to inherit from this class
17 */
18class syntax_plugin_webcode_basis extends DokuWiki_Syntax_Plugin
19{
20
21    const EXTERNAL_RESOURCES_ATTRIBUTE_DISPLAY = 'externalResources'; // In the action bar
22    const EXTERNAL_RESOURCES_ATTRIBUTE_KEY = 'externalresources'; // In the code
23
24    // Simple cache bursting implementation for the webCodeConsole.(js|css) file
25    // They must be incremented manually when they changed
26    const WEB_CONSOLE_CSS_VERSION = 1.1;
27    const WEB_CONSOLE_JS_VERSION = 2.1;
28
29    /**
30     * @var array that holds the iframe attributes
31     */
32    private $attributes = array();
33    /**
34     * @var array That holds the code parts
35     */
36
37    private $codes = array();
38
39    /**
40     * Print the output of the console javascript function ?
41     */
42    private $useConsole = false;
43
44    /**
45     * @param $mode
46     * @param array $instructions
47     * @param array $info - the $info of the renderer to pass context information
48     * @return string|null
49     * Wrapper around {@link p_render()} that pass the $info through tho the created Renderer
50     */
51    private static function p_render($mode, array $instructions, array &$info)
52    {
53        if (is_null($instructions)) return '';
54        if ($instructions === false) return '';
55
56        $Renderer = p_get_renderer($mode);
57        if (is_null($Renderer)) return null;
58
59        $Renderer->reset();
60        if (!empty($info)) {
61            $Renderer->info = $info;
62        }
63
64        $Renderer->smileys = getSmileys();
65        $Renderer->entities = getEntities();
66        $Renderer->acronyms = getAcronyms();
67        $Renderer->interwiki = getInterwiki();
68
69        // Loop through the instructions
70        foreach ($instructions as $instruction) {
71            // Execute the callback against the Renderer
72            if (method_exists($Renderer, $instruction[0])) {
73                call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1] ? $instruction[1] : array());
74            }
75        }
76
77        //set info array
78        $info = $Renderer->info;
79
80        // Post process and return the output
81        $data = array($mode, & $Renderer->doc);
82        Event::createAndTrigger('RENDERER_CONTENT_POSTPROCESS', $data);
83        return $Renderer->doc;
84
85    }
86
87
88    /**
89     * Syntax Type.
90     *
91     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
92     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
93     *
94     * container because it may contain header in case of how to
95     */
96    public function getType()
97    {
98        // formatting ?
99        // container
100        return 'container';
101    }
102
103    /**
104     * @return array
105     * Allow which kind of plugin inside
106     *
107     * array('container', 'baseonly','formatting', 'substition', 'protected', 'disabled', 'paragraphs')
108     *
109     */
110    public function getAllowedTypes()
111    {
112        return array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs');
113    }
114
115    /*
116     * Don't accept the code mode
117     * in order to get the code block
118     * in the DOKU_LEXER_MATCHED state through addPattern
119     */
120    function accepts($mode)
121    {
122        if ($mode == "code" || $mode == "plugin_combo_code") {
123            return false;
124        }
125        return parent::accepts($mode);
126    }
127
128
129    /**
130     * @see Doku_Parser_Mode::getSort()
131     * The mode (plugin) with the lowest sort number will win out
132     *
133     * See {@link Doku_Parser_Mode_code}
134     */
135    public function getSort()
136    {
137        return 100;
138    }
139
140    /**
141     * Called before any calls to ConnectTo
142     * @return void
143     */
144    function preConnect()
145    {
146    }
147
148    /**
149     * Create a pattern that will called this plugin
150     *
151     * @param string $mode
152     *
153     * All dokuwiki mode can be seen in the parser.php file
154     * @see Doku_Parser_Mode::connectTo()
155     */
156    public function connectTo($mode)
157    {
158
159        $this->Lexer->addEntryPattern('<webcode.*?>(?=.*?</webcode>)', $mode, $this->getPluginMode());
160
161    }
162
163
164    // This where the addPattern and addExitPattern are defined
165    public function postConnect()
166    {
167
168        /**
169         * Capture all code block
170         * See {@link Doku_Parser_Mode_code}
171         */
172        $this->Lexer->addPattern('<code.*?</code>', $this->getPluginMode());
173
174        /**
175         * End
176         */
177        $this->Lexer->addExitPattern('</webcode>', $this->getPluginMode());
178
179    }
180
181
182    /**
183     * Handle the match
184     * You get the match for each pattern in the $match variable
185     * $state says if it's an entry, exit or match pattern
186     *
187     * This is an instruction block and is cached apart from the rendering output
188     * There is two caches levels
189     * This cache may be suppressed with the url parameters ?purge=true
190     *
191     * The returned values are cached in an array that will be passed to the render method
192     * The handle function goal is to parse the matched syntax through the pattern function
193     * and to return the result for use in the renderer
194     * This result is always cached until the page is modified.
195     * @param string $match
196     * @param int $state
197     * @param int $pos
198     * @param Doku_Handler $handler
199     * @return array|bool
200     * @throws Exception
201     * @see DokuWiki_Syntax_Plugin::handle()
202     *
203     */
204    public function handle($match, $state, $pos, Doku_Handler $handler)
205    {
206        switch ($state) {
207
208            case DOKU_LEXER_ENTER :
209
210                // We got the first webcode tag and its attributes
211
212                $match = substr($match, 8, -1); //9 = strlen("<webcode")
213
214                // Reset of the attributes
215                // With some framework the php object may be still persisted in memory
216                // And you may get some attributes from other page
217                $attributes = array();
218                $attributes['frameborder'] = 1;
219                $attributes['width'] = '100%';
220
221                $renderingModeKey = 'renderingmode';
222                $attributes[$renderingModeKey] = 'story';
223
224                // config Parameters will get their value in lowercase
225                $configAttributes = [$renderingModeKey];
226
227                // /i not case sensitive
228                $attributePattern = "\s*(\w+)\s*=\s*\"?([^\"\s]+)\"?\\s*";
229                $result = preg_match_all('/' . $attributePattern . '/i', $match, $matches);
230
231
232                if ($result != 0) {
233                    foreach ($matches[1] as $key => $lang) {
234                        $attributeKey = strtolower($lang);
235                        $attributeValue = $matches[2][$key];
236                        if (in_array($attributeKey, $configAttributes)) {
237                            $attributeValue = strtolower($attributeValue);
238                        }
239                        $attributes[$attributeKey] = $attributeValue;
240                    }
241                }
242
243                // We set the attributes on a class scope
244                // to be used in the DOKU_LEXER_UNMATCHED step
245                $this->attributes = $attributes;
246
247                // Cache the values to be used by the render method
248                return array($state, $attributes);
249
250
251            /**
252             * The code block as asked
253             * by addPattern() into {@link postConnect}
254             */
255            case DOKU_LEXER_MATCHED:
256
257                $xhtmlWebCode = "";
258
259                // We got the content between the webcode tag and its attributes
260                // We parse it in order to extract the code in the codes array
261                $codes = array();
262                /**
263                 * Does the javascript contains a console statement
264                 */
265                $useConsole = false;
266
267                // Regexp Pattern to parse the codes block
268                $codePattern = "<code\s*([^>\s]*)\s*([^>\s]*)>(.+?)<\/code>";
269                // The first group is the lang
270                // The second group is the file name and options
271                // The third group is the code
272                $result = preg_match_all('/' . $codePattern . '/msi', $match, $matches, PREG_PATTERN_ORDER);
273                if ($result) {
274
275                    // Loop through the block codes
276                    foreach ($matches[1] as $key => $lang) {
277
278                        // Get the code (The content between the code nodes)
279                        // We ltrim because the match gives us the \n at the beginning and at the end
280                        $code = ltrim($matches[3][$key]);
281
282                        // String are in lowercase
283                        $lowerCodeName = strtolower($lang);
284
285                        // Xml is html
286                        if ($lowerCodeName == 'xml') {
287                            $lowerCodeName = 'html';
288                        }
289
290                        // If the code doesn't exist in the array, index it otherwise append it
291                        if (!array_key_exists($lowerCodeName, $codes)) {
292                            $codes[$lowerCodeName] = $code;
293                        } else {
294                            $codes[$lowerCodeName] = $codes[$lowerCodeName] . $code;
295                        }
296
297                        // Check if a javascript console function is used, only if the flag is not set to true
298                        if (!$useConsole == true) {
299                            if (in_array($lowerCodeName, array('babel', 'javascript', 'html', 'xml'))) {
300                                // if the code contains 'console.'
301                                $result = preg_match('/' . 'console\.' . '/is', $code);
302                                if ($result) {
303                                    $useConsole = true;
304                                }
305                            }
306                        }
307                    }
308                    $matchedTextToRender = "";
309                    // Render the whole
310                    if ($this->attributes["renderingmode"] != "onlyresult") {
311
312                        // Replace babel by javascript because babel highlight does not exist in the dokuwiki and babel is only javascript ES2015
313                        $matchedTextToRender = preg_replace('/<code[\s]+babel/', '<code javascript', $match);
314
315                        // Delete a display="none" block
316                        $matchedTextToRender = preg_replace('/<code([a-z\s]*)\[([a-z\s]*)display="none"([a-z\s]*)\]>(.*)<\/code>/msiU', '', $matchedTextToRender);
317
318                    }
319                    return array($state, $matchedTextToRender, $codes, $useConsole);
320
321                } else {
322                    throw new Exception("There was a match of the pattern but not when parsing");
323                }
324
325
326            case DOKU_LEXER_UNMATCHED :
327
328                // Cache the values
329                return array($state, $match);
330
331            case DOKU_LEXER_EXIT:
332
333                // Cache the values
334                return array($state);
335
336        }
337
338    }
339
340    /**
341     * Render the output
342     * @param string $mode
343     * @param Doku_Renderer $renderer
344     * @param array $data - what the function handle() return'ed
345     * @return bool - rendered correctly (not used)
346     *
347     * The rendering process
348     * @see DokuWiki_Syntax_Plugin::render()
349     *
350     */
351    public function render($mode, Doku_Renderer $renderer, $data)
352    {
353        // The $data variable comes from the handle() function
354        //
355        // $mode = 'xhtml' means that we output html
356        // There is other mode such as metadata where you can output data for the headers (Not 100% sure)
357        if ($mode == 'xhtml') {
358
359            /** @var Doku_Renderer_xhtml $renderer */
360
361            $state = $data[0];
362            switch ($state) {
363
364                case DOKU_LEXER_ENTER :
365
366                    // The extracted data are the attribute of the webcode tag
367                    // We put in a class variable so that we can use in the last step (DOKU_LEXER_EXIT)
368                    $this->attributes = $data[1];
369
370                    // Reinit the codes to make sure that the code does not leak into another webcode
371                    $this->useConsole = false;
372                    $this->codes = array();
373                    break;
374
375                case DOKU_LEXER_MATCHED :
376
377                    // The extracted data are the codes for this step
378                    // We put them in a class variable so that we can use them in the last step (DOKU_LEXER_EXIT)
379                    $code = $data[2];
380                    $codeType = key($code);
381                    $this->codes[$codeType] = $this->codes[$codeType] . $code[$codeType];
382
383                    // if not true, see if it's true
384                    if (!$this->useConsole) {
385                        $this->useConsole = $data[3];
386                    }
387
388                    // Render
389                    $textToRender = $data[1];
390                    if (!empty($textToRender)) {
391                        $instructions = p_get_instructions($textToRender);
392                        $xhtmlWebCode = self::p_render('xhtml', $instructions, $renderer->info);
393                        $renderer->doc .= $xhtmlWebCode;
394                    }
395                    break;
396
397                case DOKU_LEXER_UNMATCHED :
398
399                    // Render and escape
400                    $renderer->doc .= $renderer->_xmlEntities($data[1]);
401                    break;
402
403                case DOKU_LEXER_EXIT :
404                    // Create the real output of webcode
405                    if (sizeof($this->codes) == 0) {
406                        return false;
407                    }
408                    // Dokuwiki Code ?
409                    if (array_key_exists('dw', $this->codes)) {
410                        $instructions = p_get_instructions($this->codes['dw']);
411                        $renderer->doc .= self::p_render('xhtml', $instructions, $renderer->info);
412                    } else {
413
414                        // Js, Html, Css
415                        $htmlContent = '<html><head>';
416                        $htmlContent .= '<meta http-equiv="content-type" content="text/html; charset=UTF-8">';
417                        $htmlContent .= '<title>Made by Webcode</title>';
418                        $htmlContent .= '<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css">';
419
420
421                        // External Resources such as css stylesheet or js
422                        $externalResources = array();
423                        if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $this->attributes)) {
424                            $externalResources = explode(",", $this->attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]);
425                        }
426
427                        // Babel Preprocessor, if babel is used, add it to the external resources
428                        if (array_key_exists('babel', $this->codes)) {
429                            $babelMin = "https://unpkg.com/babel-standalone@6/babel.min.js";
430                            // a load of babel invoke it (be sure to not have it twice
431                            if (!(array_key_exists($babelMin, $externalResources))) {
432                                $externalResources[] = $babelMin;
433                            }
434                        }
435
436                        // Add the external resources
437                        foreach ($externalResources as $externalResource) {
438                            $pathInfo = pathinfo($externalResource);
439                            $fileExtension = $pathInfo['extension'];
440                            switch ($fileExtension) {
441                                case 'css':
442                                    $htmlContent .= '<link rel="stylesheet" type="text/css" href="' . $externalResource . '">';
443                                    break;
444                                case 'js':
445                                    $htmlContent .= '<script type="text/javascript" src="' . $externalResource . '"></script>';
446                                    break;
447                            }
448                        }
449
450
451                        // WebConsole style sheet
452                        if ($this->useConsole) {
453                            $htmlContent .= '<link rel="stylesheet" type="text/css" href="' . DOKU_URL . 'lib/plugins/webcode/webCodeConsole.css?ver=' . self::WEB_CONSOLE_CSS_VERSION . '"/>';
454                        }
455
456                        if (array_key_exists('css', $this->codes)) {
457                            $htmlContent .= '<!-- The CSS code -->';
458                            $htmlContent .= '<style>' . $this->codes['css'] . '</style>';
459                        };
460                        $htmlContent .= '</head><body style="margin:10px">';
461                        if (array_key_exists('html', $this->codes)) {
462                            $htmlContent .= '<!-- The HTML code -->';
463                            $htmlContent .= $this->codes['html'];
464                        }
465                        // The javascript console area is based at the end of the HTML document
466                        if ($this->useConsole) {
467                            $htmlContent .= '<!-- WebCode Console -->';
468                            $htmlContent .= '<div><p class=\'webConsoleTitle\'>Console Output:</p>';
469                            $htmlContent .= '<div id=\'webCodeConsole\'/>';
470                            $htmlContent .= '<script type=\'text/javascript\' src=\'' . DOKU_URL . 'lib/plugins/webcode/webCodeConsole.js?ver=' . self::WEB_CONSOLE_JS_VERSION . '\'></script>';
471                            $htmlContent .= '</div>';
472                        }
473                        // The javascript comes at the end because it may want to be applied on previous HTML element
474                        // as the page load in the IO order, javascript must be placed at the end
475                        if (array_key_exists('javascript', $this->codes)) {
476                            $htmlContent .= '<!-- The Javascript code -->';
477                            $htmlContent .= '<script type="text/javascript">' . $this->codes['javascript'] . '</script>';
478                        }
479                        if (array_key_exists('babel', $this->codes)) {
480                            $htmlContent .= '<!-- The Babel code -->';
481                            $htmlContent .= '<script type="text/babel">' . $this->codes['babel'] . '</script>';
482                        }
483                        $htmlContent .= '</body></html>';
484
485                        // Here the magic from the plugin happens
486                        // We add the Iframe and the JsFiddleButton
487                        $iFrameHtml = '<iframe ';
488
489                        // We add the name HTML attribute
490                        $name = "WebCode iFrame";
491                        if (array_key_exists('name', $this->attributes)) {
492                            $name .= ' ' . $this->attributes['name'];
493                        }
494                        $iFrameHtml .= ' name="' . $name . '" ';
495
496                        // The class to be able to select them
497                        $iFrameHtml .= ' class="webCode" ';
498
499                        // We add the others HTML attributes
500                        $iFrameHtmlAttributes = array('width', 'height', 'frameborder', 'scrolling');
501                        foreach ($this->attributes as $attribute => $value) {
502                            if (in_array($attribute, $iFrameHtmlAttributes)) {
503                                $iFrameHtml .= ' ' . $attribute . '=' . $value;
504                            }
505                        }
506                        $iFrameHtml .= ' srcdoc="' . htmlentities($htmlContent) . '" ></iframe>';//
507
508                        // Credits bar
509                        $bar = '<div class="webcode-bar">';
510                        $bar .= '<div class="webcode-bar-item"><a href="https://combostrap.com/webcode">' . $this->getLang('RenderedBy') . '</a></div>';
511                        $bar .= '<div class="webcode-bar-item">'.$this->addJsFiddleButton($this->codes, $this->attributes).'</div>';
512                        $bar .= '</div>';
513                        $renderer->doc .= '<div class="webcode">' . $iFrameHtml . $bar . '</div>';
514                    }
515
516                    break;
517            }
518
519            return true;
520        }
521        return false;
522    }
523
524    /**
525     * @param array $codes the array containing the codes
526     * @param array $attributes the attributes of a call (for now the externalResources)
527     * @return string the HTML form code
528     *
529     * Specification, see http://doc.jsfiddle.net/api/post.html
530     */
531    public function addJsFiddleButton($codes, $attributes)
532    {
533
534        $postURL = "https://jsfiddle.net/api/post/library/pure/"; //No Framework
535
536        $externalResources = array();
537        if (array_key_exists(self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY, $attributes)) {
538            $externalResources = explode(",", $attributes[self::EXTERNAL_RESOURCES_ATTRIBUTE_KEY]);
539        }
540
541
542        if ($this->useConsole) {
543            // If their is a console.log function, add the Firebug Lite support of JsFiddle
544            // Seems to work only with the Edge version of jQuery
545            // $postURL .= "edge/dependencies/Lite/";
546            // The firebug logging is not working anymore because of 404
547            // Adding them here
548            $externalResources[] = 'The firebug resources for the console.log features';
549            $externalResources[] = DOKU_URL . 'lib/plugins/webcode/vendor/firebug-lite.css';
550            $externalResources[] = DOKU_URL . 'lib/plugins/webcode/vendor/firebug-lite-1.2.js';
551        }
552
553        // The below code is to prevent this JsFiddle bug: https://github.com/jsfiddle/jsfiddle-issues/issues/726
554        // The order of the resources is not guaranteed
555        // We pass then the resources only if their is one resources
556        // Otherwise we pass them as a script element in the HTML.
557        if (count($externalResources) <= 1) {
558            $externalResourcesInput = '<input type="hidden" name="resources" value="' . implode(",", $externalResources) . '">';
559        } else {
560            $codes['html'] .= "\n\n\n\n\n<!-- The resources -->\n";
561            $codes['html'] .= "<!-- They have been added here because their order is not guarantee through the API. -->\n";
562            $codes['html'] .= "<!-- See: https://github.com/jsfiddle/jsfiddle-issues/issues/726 -->\n";
563            foreach ($externalResources as $externalResource) {
564                if ($externalResource != "") {
565                    $extension = pathinfo($externalResource)['extension'];
566                    switch ($extension) {
567                        case "css":
568                            $codes['html'] .= "<link href=\"" . $externalResource . "\" rel=\"stylesheet\">\n";
569                            break;
570                        case "js":
571                            $codes['html'] .= "<script src=\"" . $externalResource . "\"></script>\n";
572                            break;
573                        default:
574                            $codes['html'] .= "<!-- " . $externalResource . " -->\n";
575                    }
576                }
577            }
578        }
579
580        $jsCode = $codes['javascript'];
581        $jsPanel = 0; // language for the js specific panel (0 = JavaScript)
582        if (array_key_exists('babel', $this->codes)) {
583            $jsCode = $codes['babel'];
584            $jsPanel = 3; // 3 = Babel
585        }
586
587        // Title and description
588        global $ID;
589        $title = $attributes['name'];
590        $pageTitle = tpl_pagetitle($ID, true);
591        if (!$title) {
592
593            $title = "Code from " . $pageTitle;
594        }
595        $description = "Code from the page '" . $pageTitle . "' \n" . wl($ID, $absolute = true);
596        return '<form  method="post" action="' . $postURL . '" target="_blank">' .
597        '<input type="hidden" name="title" value="' . htmlentities($title) . '">' .
598        '<input type="hidden" name="description" value="' . htmlentities($description) . '">' .
599        '<input type="hidden" name="css" value="' . htmlentities($codes['css']) . '">' .
600        '<input type="hidden" name="html" value="' . htmlentities("<!-- The HTML -->" . $codes['html']) . '">' .
601        '<input type="hidden" name="js" value="' . htmlentities($jsCode) . '">' .
602        '<input type="hidden" name="panel_js" value="' . htmlentities($jsPanel) . '">' .
603        '<input type="hidden" name="wrap" value="b">' .  //javascript no wrap in body
604        $externalResourcesInput .
605        '<button>' . $this->getLang('JsFiddleButtonContent') . '</button>' .
606        '</form>';
607
608    }
609
610    /**
611     * @param $codes the array containing the codes
612     * @param $attributes the attributes of a call (for now the externalResources)
613     * @return string the HTML form code
614     */
615    public function addCodePenButton($codes, $attributes)
616    {
617        // TODO
618        // http://blog.codepen.io/documentation/api/prefill/
619    }
620
621
622    /**
623     * @return string the mode (the name of this plugin for the lexer)
624     */
625    public function getPluginMode()
626    {
627        $pluginName = $this->getPluginName();
628        $pluginComponent = $this->getPluginComponent();
629        return 'plugin_' . $pluginName . '_' . $pluginComponent;
630    }
631
632
633}
634