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