1<?php
2
3namespace ComboStrap\Tag;
4
5use ComboStrap\CallStack;
6use ComboStrap\Dimension;
7use ComboStrap\Display;
8use ComboStrap\ExceptionBadState;
9use ComboStrap\ExceptionCompile;
10use ComboStrap\ExceptionNotFound;
11use ComboStrap\ExecutionContext;
12use ComboStrap\FetcherMarkup;
13use ComboStrap\FetcherMarkupWebcode;
14use ComboStrap\FetcherRawLocalPath;
15use ComboStrap\LogUtility;
16use ComboStrap\PluginUtility;
17use ComboStrap\TagAttribute\StyleAttribute;
18use ComboStrap\TagAttributes;
19use ComboStrap\WikiPath;
20use syntax_plugin_combo_code;
21use syntax_plugin_combo_codemarkdown;
22
23class WebCodeTag
24{
25
26    public const TAG = 'webcode';
27    /**
28     * The tag that have codes
29     */
30    public const CODE_TAGS = array(
31        syntax_plugin_combo_code::CODE_TAG,
32        "plugin_combo_code",
33        syntax_plugin_combo_codemarkdown::TAG
34    );
35    /**
36     * The attribute names in the array
37     */
38    public const CODES_ATTRIBUTE = "codes";
39    public const EXTERNAL_RESOURCES_ATTRIBUTE_DISPLAY = 'externalResources';
40    public const USE_CONSOLE_ATTRIBUTE = "useConsole";
41    public const RENDERING_ONLY_RESULT_DEPRECATED = "onlyresult";
42    public const CANONICAL = WebCodeTag::TAG;
43    public const DOKUWIKI_LANG = 'dw';
44    public const FRAMEBORDER_ATTRIBUTE = "frameborder";
45    /**
46     * @deprecated for type
47     */
48    public const RENDERING_MODE_ATTRIBUTE = 'renderingmode';
49    public const MARKIS = [WebCodeTag::MARKI_LANG, WebCodeTag::DOKUWIKI_LANG];
50    public const EXTERNAL_RESOURCES_ATTRIBUTE_KEY = 'externalresources';
51    /**
52     * Marki code
53     */
54    public const MARKI_LANG = 'marki';
55    public const IFRAME_BOOLEAN_ATTRIBUTE = "iframe";
56    const STORY_TYPE = "story";
57    const RESULT_TYPE = "result";
58    const INJECT_TYPE = "inject";
59
60    public static function getClass(): string
61    {
62        return StyleAttribute::addComboStrapSuffix(WebCodeTag::TAG);
63    }
64
65    public static function getKnownTypes(): array
66    {
67        return [self::STORY_TYPE, self::RESULT_TYPE, self::INJECT_TYPE];
68    }
69
70    public static function getDefaultAttributes(): array
71    {
72        $defaultAttributes = array();
73        $defaultAttributes[Dimension::WIDTH_KEY] = '100%';
74        // 'type': no default to see if it was set because the default now is dependent on the content
75        // 'height' is set by the javascript if not set
76        // 'width' and 'scrolling' gets their natural value
77        return $defaultAttributes;
78    }
79
80    public static function handleExit(\Doku_Handler $handler): array
81    {
82        /**
83         * Capture all codes
84         */
85        $codes = array();
86        /**
87         * Does the javascript contains a console statement
88         */
89        $useConsole = false;
90
91        /**
92         * Callstack
93         */
94        $callStack = CallStack::createFromHandler($handler);
95        $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
96        $type = $openingTag->getType();
97        $renderingMode = $openingTag->getAttribute(WebCodeTag::RENDERING_MODE_ATTRIBUTE);
98        if ($renderingMode !== null) {
99            LogUtility::warning("The `renderingmode` attribute has been deprecated for the webcode `type` attribute.");
100            if ($type === null) {
101                $type = strtolower($renderingMode);
102            }
103        }
104        if ($type === WebCodeTag::RENDERING_ONLY_RESULT_DEPRECATED) {
105            LogUtility::warning("The `type` value (" . self::RENDERING_ONLY_RESULT_DEPRECATED . ") should be replaced by (" . self::RESULT_TYPE . ")");
106            $type = WebCodeTag::RESULT_TYPE;
107        }
108
109        /**
110         * The mime (ie xml,html, ...) and code content are in two differents
111         * call. To be able to set the content to the good type
112         * we keep a trace of it
113         */
114        $actualCodeType = "";
115
116        /**
117         * Loop
118         */
119        while ($actualTag = $callStack->next()) {
120
121
122            $tagName = $actualTag->getTagName();
123            if (in_array($tagName, WebCodeTag::CODE_TAGS)) {
124
125                /**
126                 * Only result or inject mode, we don't display the code
127                 * on all node (enter, exit and unmatched)
128                 */
129                if (in_array($type, [WebCodeTag::RESULT_TYPE, self::INJECT_TYPE])) {
130                    $actualTag->addAttribute(Display::DISPLAY, Display::DISPLAY_NONE_VALUE);
131                }
132
133                switch ($actualTag->getState()) {
134
135                    case DOKU_LEXER_ENTER:
136                        // Get the code (The content between the code nodes)
137                        // We ltrim because the match gives us the \n at the beginning and at the end
138                        $actualCodeType = strtolower(trim($actualTag->getType()));
139
140                        // Xml is html
141                        if ($actualCodeType === 'xml') {
142                            $actualCodeType = 'html';
143                        }
144
145                        // markdown, dokuwiki is marki
146                        if (in_array($actualCodeType, ['md', 'markdown', 'dw'])) {
147                            $actualCodeType = WebCodeTag::MARKI_LANG;
148                        }
149
150                        // The code for a language may be scattered in multiple block
151                        if (!isset($codes[$actualCodeType])) {
152                            $codes[$actualCodeType] = "";
153                        }
154
155                        continue 2;
156
157                    case DOKU_LEXER_UNMATCHED:
158
159                        $codeContent = $actualTag->getPluginData()[PluginUtility::PAYLOAD];
160
161                        if (empty($actualCodeType)) {
162                            LogUtility::msg("The type of the code should not be null for the code content " . $codeContent, LogUtility::LVL_MSG_WARNING, WebCodeTag::TAG);
163                            continue 2;
164                        }
165
166                        // Append it
167                        $codes[$actualCodeType] = $codes[$actualCodeType] . $codeContent;
168
169                        // Check if a javascript console function is used, only if the flag is not set to true
170                        if (!$useConsole) {
171                            if (in_array($actualCodeType, array('babel', 'javascript', 'html', 'xml'))) {
172                                // if the code contains 'console.'
173                                $result = preg_match('/' . 'console\.' . '/is', $codeContent);
174                                if ($result) {
175                                    $useConsole = true;
176                                }
177                            }
178                        }
179                        // Reset
180                        $actualCodeType = "";
181                        break;
182
183                }
184            }
185
186        }
187
188        /**
189         * By default, markup code
190         * is rendered inside the page
191         * We got less problem such as iframe overflow
192         * due to lazy loading, such as relative link, ...
193         */
194        if (
195            array_key_exists(WebCodeTag::MARKI_LANG, $codes)
196            && count($codes) === 1
197            && $openingTag->getAttribute(WebCodeTag::IFRAME_BOOLEAN_ATTRIBUTE) === null
198            && $openingTag->getType() === null
199        ) {
200            $openingTag->setType(self::INJECT_TYPE);
201        }
202
203        return [
204            WebCodeTag::CODES_ATTRIBUTE => $codes,
205            WebCodeTag::USE_CONSOLE_ATTRIBUTE => $useConsole,
206            PluginUtility::ATTRIBUTES => $openingTag->getAttributes()
207        ];
208    }
209
210    /**
211     * Tag is of an iframe (Web code) or a div (wiki markup)
212     */
213    public static function renderExit(TagAttributes $tagAttributes, array $data)
214    {
215
216        $codes = $data[WebCodeTag::CODES_ATTRIBUTE];
217
218        $type = $tagAttributes->getType();
219        if ($type === null) {
220            $type = self::STORY_TYPE;
221        }
222
223        /**
224         * Rendering mode is used in handle exit, we delete it
225         * to not get it in the HTML output
226         */
227        $tagAttributes->removeComponentAttributeIfPresent(WebCodeTag::RENDERING_MODE_ATTRIBUTE);
228
229        // Create the real output of webcode
230        if (sizeof($codes) == 0) {
231            return false;
232        }
233
234
235        // Css
236        $snippetSystem = PluginUtility::getSnippetManager();
237        $snippetSystem->attachCssInternalStyleSheet(WebCodeTag::TAG);
238        $snippetSystem->attachJavascriptFromComponentId(WebCodeTag::TAG);
239
240        // Mermaid code ?
241        if (array_key_exists(MermaidTag::MERMAID_CODE, $codes)) {
242            $mermaidCode = "";
243            foreach ($codes as $codeKey => $code) {
244                if ($codeKey !== MermaidTag::MERMAID_CODE) {
245                    LogUtility::error("The code type ($codeKey) was mixed with mermaid code in a webcode and this is not yet supported. The code was skipped");
246                    continue;
247                }
248                $mermaidCode .= $code;
249            }
250            $tagAttributes->addComponentAttributeValue(MermaidTag::MARKUP_CONTENT_ATTRIBUTE, $mermaidCode);
251            return MermaidTag::renderEnter($tagAttributes);
252        }
253
254        /**
255         * Dokuwiki Code
256         * (Just HTML)
257         */
258        if (array_key_exists(WebCodeTag::MARKI_LANG, $codes)) {
259
260            $markupCode = $codes[WebCodeTag::MARKI_LANG];
261
262            if ($type === self::INJECT_TYPE) {
263                /**
264                 * the div is to be able to apply some CSS
265                 * such as don't show editbutton on webcode
266                 */
267                $html = $tagAttributes->toHtmlEnterTag("div");
268                try {
269                    $contextPath = ExecutionContext::getActualOrCreateFromEnv()
270                        ->getContextPath();
271                    $html .= FetcherMarkup::confChild()
272                        ->setRequestedMarkupString($markupCode)
273                        ->setDeleteRootBlockElement(false)
274                        ->setIsDocument(false)
275                        ->setRequestedContextPath($contextPath)
276                        ->setRequestedMimeToXhtml()
277                        ->build()
278                        ->getFetchString();
279                } catch (ExceptionCompile $e) {
280                    $html .= $e->getMessage();
281                    LogUtility::log2file("Error while rendering webcode", LogUtility::LVL_MSG_ERROR, WebCodeTag::CANONICAL, $e);
282                }
283                $html .= "</div>";
284                return $html;
285            }
286
287            /**
288             * Iframe output
289             */
290            $tagAttributes->removeComponentAttribute(WebCodeTag::IFRAME_BOOLEAN_ATTRIBUTE);
291
292            if (!$tagAttributes->hasAttribute(TagAttributes::NAME_ATTRIBUTE)) {
293                $tagAttributes->addOutputAttributeValueIfNotEmpty(TagAttributes::NAME_ATTRIBUTE, "WebCode iFrame");
294            }
295            try {
296                $url = FetcherMarkupWebcode::createFetcherMarkup($markupCode)
297                    ->getFetchUrl()
298                    ->toString();
299                $tagAttributes->addOutputAttributeValue("src", $url);
300            } catch (ExceptionBadState $e) {
301                // The markup is provided, we shouldn't have a bad state
302                LogUtility::internalError("We were unable to set the iframe URL. Error:{$e->getMessage()}", WebCodeTag::CANONICAL);
303            }
304            return self::finishIframe($tagAttributes);
305
306
307        }
308
309
310        /**
311         * Js Html Css language
312         */
313        if ($type === self::INJECT_TYPE) {
314            $htmlToInject = self::getCss($codes);
315            return $htmlToInject . self::getBodyHtmlAndJavascript($codes, false);
316        }
317
318        /** @noinspection JSUnresolvedLibraryURL */
319
320        $headIFrame = <<<EOF
321<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
322<link id="normalize" rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/3.0.3/normalize.min.css"/>
323EOF;
324
325
326        // External Resources such as css stylesheet or js
327        $externalResources = [];
328        if ($tagAttributes->hasComponentAttribute(WebCodeTag::EXTERNAL_RESOURCES_ATTRIBUTE_KEY)) {
329            LogUtility::warning("The (" . WebCodeTag::EXTERNAL_RESOURCES_ATTRIBUTE_KEY . ") has been deprecated. You should put your script/link in a code block with the `display` attribute set to `none`.");
330            $resources = $tagAttributes->getValueAndRemove(WebCodeTag::EXTERNAL_RESOURCES_ATTRIBUTE_KEY);
331            $externalResources = explode(",", $resources);
332        }
333
334        // Jsx / Babel Preprocessor, if babel is used, add it to the external resources
335        if (array_key_exists('babel', $codes)) {
336            $babelMin = "https://unpkg.com/babel-standalone@6/babel.min.js";
337            // a load of babel invoke it (be sure to not have it twice
338            if (!(array_key_exists($babelMin, $externalResources))) {
339                $externalResources[] = $babelMin;
340            }
341        }
342
343        // Add the external resources
344        foreach ($externalResources as $externalResource) {
345            $pathInfo = pathinfo($externalResource);
346            $fileExtension = $pathInfo['extension'];
347            switch ($fileExtension) {
348                case 'css':
349                    $headIFrame .= "<link rel=\"stylesheet\" type=\"text/css\" href=\"$externalResource\"/>";
350                    break;
351                case 'js':
352                    $headIFrame .= "<script type=\"text/javascript\" src=\"$externalResource\"></script>";
353                    break;
354            }
355        }
356
357        // WebConsole style sheet
358        $webcodeClass = WebCodeTag::getClass();
359        $cssUrl = FetcherRawLocalPath::createFromPath(WikiPath::createComboResource("webcode:webcode-iframe.css"))->getFetchUrl()->toHtmlString();
360        $headIFrame .= "<link class='$webcodeClass' rel=\"stylesheet\" type=\"text/css\" href=\"$cssUrl\"/>";
361
362        // A little margin to make it neater
363        // that can be overwritten via cascade
364        $headIFrame .= "<style class=\"$webcodeClass\">body { margin:10px } /* default margin */</style>";
365
366        // The css
367        $headIFrame .= self::getCss($codes);
368
369        // The javascript console script should be first to handle console.log in the content
370        $useConsole = $data[WebCodeTag::USE_CONSOLE_ATTRIBUTE];
371        if ($useConsole) {
372            $url = FetcherRawLocalPath::createFromPath(WikiPath::createComboResource("webcode:webcode-console.js"))->getFetchUrl()->toHtmlString();
373            $headIFrame .= <<<EOF
374<script class="$webcodeClass" type="text/javascript" src="$url"></script>
375EOF;
376        }
377        $body = self::getBodyHtmlAndJavascript($codes, $useConsole);
378        $iframeSrcValue = <<<EOF
379<html lang="en">
380<head>
381<title>Made by WebCode</title>
382$headIFrame
383</head>
384<body>
385$body
386</body>
387</html>
388EOF;
389        $tagAttributes->addOutputAttributeValue("srcdoc", $iframeSrcValue);
390
391        // Code bar with button
392        // Credits bar
393        $bar = '<div class="webcode-bar">';
394        $bar .= '<div class="webcode-bar-item">' . PluginUtility::getDocumentationHyperLink(WebCodeTag::TAG, "Rendered by WebCode", false) . '</div>';
395        $bar .= '<div class="webcode-bar-item">' . self::addJsFiddleButton($codes, $externalResources, $useConsole, $tagAttributes->getValue("name")) . '</div>';
396        $bar .= '</div>';
397
398        return self::finishIframe($tagAttributes, $bar);
399
400
401    }
402
403    /**
404     * @param array $codes the array containing the codes
405     * @param array $externalResources the attributes of a call (for now the externalResources)
406     * @param bool $useConsole
407     * @param null $snippetTitle
408     * @return string the HTML form code
409     *
410     * Specification, see http://doc.jsfiddle.net/api/post.html
411     */
412    public static function addJsFiddleButton($codes, $externalResources, $useConsole = false, $snippetTitle = null): string
413    {
414
415        $postURL = "https://jsfiddle.net/api/post/library/pure/"; //No Framework
416
417
418        if ($useConsole) {
419            // If their is a console.log function, add the Firebug Lite support of JsFiddle
420            // Seems to work only with the Edge version of jQuery
421            // $postURL .= "edge/dependencies/Lite/";
422            // The firebug logging is not working anymore because of 404
423
424            // Adding them here
425            // The firebug resources for the console.log features
426            try {
427                $externalResources[] = FetcherRawLocalPath::createFromPath(WikiPath::createComboResource(':firebug:firebug-lite.css'))->getFetchUrl()->toString();
428                $externalResources[] = FetcherRawLocalPath::createFromPath(WikiPath::createComboResource(':firebug:firebug-lite-1.2.js'))->getFetchUrl()->toString();
429            } catch (ExceptionNotFound $e) {
430                LogUtility::internalError("We were unable to add the firebug css and js. Error: {$e->getMessage()}", WebCodeTag::CANONICAL);
431            }
432
433        }
434
435        // The below code is to prevent this JsFiddle bug: https://github.com/jsfiddle/jsfiddle-issues/issues/726
436        // The order of the resources is not guaranteed
437        // We pass then the resources only if their is one resources
438        // Otherwise we pass them as a script element in the HTML.
439        if (count($externalResources) <= 1) {
440            $externalResourcesInput = '<input type="hidden" name="resources" value="' . implode(",", $externalResources) . '"/>';
441        } else {
442            $externalResourcesInput = '';
443            if (!array_key_exists('html', $codes)) {
444                $codes['html'] = '';
445            }
446            $codes['html'] .= "\n\n\n\n\n<!-- The resources -->\n";
447            $codes['html'] .= "<!-- They have been added here because their order is not guarantee through the API. -->\n";
448            $codes['html'] .= "<!-- See: https://github.com/jsfiddle/jsfiddle-issues/issues/726 -->\n";
449            foreach ($externalResources as $externalResource) {
450                if ($externalResource !== "") {
451                    $extension = pathinfo($externalResource)['extension'];
452                    switch ($extension) {
453                        case "css":
454                            $codes['html'] .= "<link href=\"$externalResource\" rel=\"stylesheet\"/>\n";
455                            break;
456                        case "js":
457                            $codes['html'] .= "<script src=\"$externalResource\"></script>\n";
458                            break;
459                        default:
460                            $codes['html'] .= "<!-- " . $externalResource . " -->\n";
461                    }
462                }
463            }
464        }
465
466        $jsCode = $codes['javascript'] ?? null;
467        $jsPanel = 0; // language for the js specific panel (0 = JavaScript)
468        if (array_key_exists('babel', $codes)) {
469            $jsCode = $codes['babel'];
470            $jsPanel = 3; // 3 = Babel
471        }
472
473        // Title and description
474        global $ID;
475        $pageTitle = tpl_pagetitle($ID, true);
476        if (!$snippetTitle) {
477
478            $snippetTitle = "Code from " . $pageTitle;
479        }
480        $description = "Code from the page '" . $pageTitle . "' \n" . wl($ID, $absolute = true);
481        return '<form  method="post" action="' . $postURL . '" target="_blank">' .
482            '<input type="hidden" name="title" value="' . htmlentities($snippetTitle) . '"/>' .
483            '<input type="hidden" name="description" value="' . htmlentities($description) . '"/>' .
484            '<input type="hidden" name="css" value="' . htmlentities($codes['css'] ?? '') . '"/>' .
485            '<input type="hidden" name="html" value="' . htmlentities("<!-- The HTML -->" . $codes['html'] ?? '') . '"/>' .
486            '<input type="hidden" name="js" value="' . htmlentities($jsCode) . '"/>' .
487            '<input type="hidden" name="panel_js" value="' . htmlentities($jsPanel) . '"/>' .
488            '<input type="hidden" name="wrap" value="b"/>' .  //javascript no wrap in body
489            $externalResourcesInput .
490            '<button>Try the code</button>' .
491            '</form>';
492
493    }
494
495    private static function finishIframe(TagAttributes $tagAttributes, string $bar = ""): string
496    {
497        /**
498         * The iframe does not have any width
499         * By default, we set it to 100% and it can be
500         * constraint with the `width` attributes that will
501         * set a a max-width
502         */
503        $tagAttributes->addStyleDeclarationIfNotSet("width", "100%");
504
505        /**
506         * FrameBorder
507         */
508        $frameBorder = $tagAttributes->getValueAndRemoveIfPresent(WebCodeTag::FRAMEBORDER_ATTRIBUTE);
509        if ($frameBorder !== null && $frameBorder == 0) {
510            $tagAttributes->addStyleDeclarationIfNotSet("border", "none");
511        }
512
513        $iFrameHtml = $tagAttributes->toHtmlEnterTag("iframe") . '</iframe>';
514        return "<div class=\"webcode-wrapper\">" . $iFrameHtml . $bar . '</div>';
515    }
516
517    /**
518     * Return the body
519     * @param $codes - the code to apply
520     * @param $useConsole - if the console area should be printed
521     * @return string - the html and javascript
522     */
523    private static function getBodyHtmlAndJavascript($codes, $useConsole): string
524    {
525
526        $body = "";
527        if (array_key_exists('html', $codes)) {
528            // The HTML code
529            $body .= $codes['html'];
530        }
531        // The javascript console area is based at the end of the HTML document
532        if ($useConsole) {
533
534            $body .= <<<EOF
535<!-- WebCode Console -->
536<div class="webcode-console-wrapper">
537    <p class="webConsoleTitle">Console Output:</p>
538    <div id="webCodeConsole"></div>
539</div>
540EOF;
541        }
542        // The javascript comes at the end because it may want to be applied on previous HTML element
543        // as the page load in the IO order, javascript must be placed at the end
544        if (array_key_exists('javascript', $codes)) {
545            /**
546             * The user should escapes the following character * <, >, ", ', \, and &.
547             * because they will interfere with the HTML parser
548             *
549             * The user should write `<\/script>` and note `</script>`
550             */
551            // The Javascript code
552            $body .= '<script class="webcode-javascript" type="text/javascript">' . $codes['javascript'] . '</script>';
553        }
554        if (array_key_exists('babel', $codes)) {
555            // The Babel code
556            $body .= '<script type="text/babel">' . $codes['babel'] . '</script>';
557        }
558        return $body;
559
560    }
561
562    private static function getCss($codes): string
563    {
564        if (array_key_exists('css', $codes)) {
565            return '<style>' . $codes['css'] . '</style>';
566        };
567        return "";
568    }
569}
570