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 13namespace ComboStrap; 14 15require_once(__DIR__ . '/Snippet.php'); 16 17/** 18 * @package ComboStrap 19 * 20 * Public interface of {@link Snippet} 21 * 22 * All plugin/component should use the attach functions to add a internal or external 23 * stylesheet/javascript to a slot or request scoped 24 * 25 * Note: 26 * All function with the suffix 27 * * `ForSlot` are snippets for a bar (ie page, sidebar, ...) - cached 28 * * `ForRequests` are snippets added for the HTTP request - not cached. Example of request component: message, anchor 29 */ 30class SnippetManager 31{ 32 33 const COMBO_CLASS_SUFFIX = "combo"; 34 35 36 const CANONICAL = "snippet-manager"; 37 const SCRIPT_TAG = "script"; 38 const LINK_TAG = "link"; 39 const STYLE_TAG = "style"; 40 const DATA_DOKUWIKI_ATT = "_data"; 41 42 43 /** 44 * @var SnippetManager array that contains one element (one {@link SnippetManager} scoped to the requested id 45 */ 46 private static $globalSnippetManager; 47 48 /** 49 * Empty the snippets 50 * This is used to render the snippet only once 51 * The snippets renders first in the head 52 * and otherwise at the end of the document 53 * if the user are using another template or are in edit mode 54 */ 55 public static function reset() 56 { 57 self::$globalSnippetManager = null; 58 Snippet::reset(); 59 } 60 61 62 /** 63 * @param $tag 64 * @return string 65 * @deprecated create a {@link Snippet} instead and use the {@link Snippet::getClass()} function instead 66 */ 67 public static function getClassFromSnippetId($tag): string 68 { 69 $snippet = Snippet::createUnknownSnippet($tag); 70 return $snippet->getClass(); 71 } 72 73 74 /** 75 * @return SnippetManager - the global reference 76 * that is set for every run at the end of this file 77 * TODO: migrate the attach function to {@link Snippet} 78 * because Snippet has already a global variable {@link Snippet::getOrCreateSnippet()} 79 */ 80 public static function getOrCreate(): SnippetManager 81 { 82 $id = PluginUtility::getRequestedWikiId(); 83 if ($id === null) { 84 if (PluginUtility::isTest()) { 85 $id = "test_dynamic_script_execution"; 86 } else { 87 LogUtility::msg("The requested Id could not be found, the snippets may not be scoped properly"); 88 } 89 } 90 91 $snippetManager = self::$globalSnippetManager[$id]; 92 if ($snippetManager === null) { 93 self::$globalSnippetManager = null; // delete old snippet manager for other request 94 $snippetManager = new SnippetManager(); 95 self::$globalSnippetManager[$id] = $snippetManager; 96 } 97 return $snippetManager; 98 } 99 100 101 /** 102 * Transform in dokuwiki format 103 * 104 * @return array of node type and an array of array of html attributes 105 */ 106 public function getAllSnippetsToDokuwikiArray(): array 107 { 108 $snippets = Snippet::getSnippets(); 109 if ($snippets === null) { 110 return []; 111 } 112 /** 113 * The returned array in dokuwiki format 114 */ 115 $returnedDokuWikiFormat = array(); 116 117 /** 118 * Processing the external resources 119 * and collecting the internal one 120 * 121 * The order is the order where they were added/created. 122 * 123 * The internal script may be dependent on the external javascript 124 * and vice-versa (for instance, Math-Jax library is dependent 125 * on the config that is an internal script) 126 * 127 */ 128 foreach ($snippets as $snippet) { 129 130 $type = $snippet->getType(); 131 132 133 $extension = $snippet->getExtension(); 134 switch ($extension) { 135 case Snippet::EXTENSION_JS: 136 switch ($type) { 137 case Snippet::EXTERNAL_TYPE: 138 139 $jsDokuwiki = array( 140 "class" => $snippet->getClass(), 141 "src" => $snippet->getUrl(), 142 "crossorigin" => "anonymous" 143 ); 144 $integrity = $snippet->getIntegrity(); 145 if ($integrity !== null) { 146 $jsDokuwiki["integrity"] = $integrity; 147 } 148 $critical = $snippet->getCritical(); 149 if (!$critical) { 150 $jsDokuwiki["defer"] = null; 151 // not async: it will run as soon as possible 152 // the dom main not be loaded completely, the script may miss HTML dom element 153 } 154 $jsDokuwiki = $this->addExtraHtml($jsDokuwiki, $snippet); 155 ksort($jsDokuwiki); 156 $returnedDokuWikiFormat[self::SCRIPT_TAG][] = $jsDokuwiki; 157 break; 158 case Snippet::INTERNAL_TYPE: 159 $content = $snippet->getInternalInlineAndFileContent(); 160 if ($content === null) { 161 LogUtility::msg("The internal js snippet ($snippet) has no content. Skipped"); 162 continue 3; 163 } 164 $jsDokuwiki = array( 165 "class" => $snippet->getClass(), 166 self::DATA_DOKUWIKI_ATT => $content 167 ); 168 $jsDokuwiki = $this->addExtraHtml($jsDokuwiki, $snippet); 169 $returnedDokuWikiFormat[self::SCRIPT_TAG][] = $jsDokuwiki; 170 break; 171 default: 172 LogUtility::msg("Unknown javascript snippet type"); 173 } 174 break; 175 case Snippet::EXTENSION_CSS: 176 switch ($type) { 177 case Snippet::EXTERNAL_TYPE: 178 $cssDokuwiki = array( 179 "class" => $snippet->getClass(), 180 "rel" => "stylesheet", 181 "href" => $snippet->getUrl(), 182 "crossorigin" => "anonymous" 183 ); 184 $integrity = $snippet->getIntegrity(); 185 if ($integrity !== null) { 186 $cssDokuwiki["integrity"] = $integrity; 187 } 188 $critical = $snippet->getCritical(); 189 if (!$critical && Site::getTemplate() === Site::STRAP_TEMPLATE_NAME) { 190 $cssDokuwiki["rel"] = "preload"; 191 $cssDokuwiki['as'] = self::STYLE_TAG; 192 } 193 $cssDokuwiki = $this->addExtraHtml($cssDokuwiki, $snippet); 194 ksort($cssDokuwiki); 195 $returnedDokuWikiFormat[self::LINK_TAG][] = $cssDokuwiki; 196 break; 197 case Snippet::INTERNAL_TYPE: 198 /** 199 * CSS inline in script tag 200 * They are all critical 201 */ 202 $content = $snippet->getInternalInlineAndFileContent(); 203 if ($content === null) { 204 LogUtility::msg("The internal css snippet ($snippet) has no content. Skipped"); 205 continue 3; 206 } 207 $cssInternalArray = array( 208 "class" => $snippet->getClass(), 209 self::DATA_DOKUWIKI_ATT => $content 210 ); 211 $cssInternalArray = $this->addExtraHtml($cssInternalArray, $snippet); 212 $returnedDokuWikiFormat[self::STYLE_TAG][] = $cssInternalArray; 213 break; 214 default: 215 LogUtility::msg("Unknown css snippet type"); 216 } 217 break; 218 default: 219 LogUtility::msg("The extension ($extension) is unknown, the external snippet ($snippet) was not added"); 220 } 221 222 } 223 224 return $returnedDokuWikiFormat; 225 } 226 227 /** 228 * @deprecated see {@link SnippetManager::reset()} 229 * 230 */ 231 public 232 function close() 233 { 234 self::reset(); 235 } 236 237 238 public 239 function getJsonArrayFromSlotSnippets($slot): ?array 240 { 241 $snippets = Snippet::getSnippets(); 242 if ($snippets === null) { 243 return null; 244 } 245 $snippetsForSlot = array_filter($snippets, 246 function ($s) use ($slot) { 247 return $s->hasSlot($slot); 248 }); 249 $jsonSnippets = null; 250 foreach ($snippetsForSlot as $snippet) { 251 $jsonSnippets[] = $snippet->toJsonArray(); 252 } 253 return $jsonSnippets; 254 255 } 256 257 /** 258 * @param array $array 259 * @param string $slot 260 * @return null|Snippet[] 261 * @throws ExceptionCombo 262 */ 263 public 264 function getSlotSnippetsFromJsonArray(array $array, string $slot): ?array 265 { 266 $snippets = null; 267 foreach ($array as $element) { 268 $snippets[] = Snippet::createFromJson($element) 269 ->addSlot($slot); 270 } 271 return $snippets; 272 } 273 274 275 /** 276 * @param $snippetId 277 * @param string|null $script - the css snippet to add, otherwise it takes the file 278 * @return Snippet a snippet not in a slot 279 */ 280 public 281 function &attachCssInternalStyleSheetForSlot($snippetId, string $script = null): Snippet 282 { 283 $snippet = $this->attachSnippetFromSlot($snippetId, Snippet::EXTENSION_CSS, Snippet::INTERNAL_TYPE); 284 if ($script !== null) { 285 $snippet->setInlineContent($script); 286 } 287 return $snippet; 288 } 289 290 /** 291 * @param $snippetId 292 * @param string|null $script - the css if any, otherwise the css file will be taken 293 * @return Snippet a snippet scoped at the request scope (not in a slot) 294 */ 295 public 296 function &attachCssSnippetForRequest($snippetId, string $script = null): Snippet 297 { 298 $snippet = $this->attachSnippetFromRequest($snippetId, Snippet::EXTENSION_CSS, Snippet::INTERNAL_TYPE); 299 if ($script != null) { 300 $snippet->setInlineContent($script); 301 } 302 return $snippet; 303 } 304 305 /** 306 * @param $snippetId 307 * @param string|null $script 308 * @return Snippet a snippet in a slot 309 */ 310 public 311 function &attachInternalJavascriptForSlot($snippetId, string $script = null): Snippet 312 { 313 $snippet = &$this->attachSnippetFromSlot($snippetId, Snippet::EXTENSION_JS, Snippet::INTERNAL_TYPE); 314 if ($script !== null) { 315 $content = $snippet->getInternalDynamicContent(); 316 if ($content !== null) { 317 $content .= $script; 318 } else { 319 $content = $script; 320 } 321 $snippet->setInlineContent($content); 322 } 323 return $snippet; 324 } 325 326 /** 327 * @param $snippetId 328 * @return Snippet a snippet not in a slot 329 */ 330 public 331 function &attachJavascriptSnippetForRequest($snippetId): Snippet 332 { 333 return $this->attachSnippetFromRequest($snippetId, Snippet::EXTENSION_JS, Snippet::INTERNAL_TYPE); 334 } 335 336 /** 337 * @param string $componentId 338 * @param string $type 339 * @param string $identifier 340 * @return Snippet 341 */ 342 private 343 function &attachSnippetFromSlot(string $componentId, string $type, string $identifier): Snippet 344 { 345 $slot = PluginUtility::getCurrentSlotId(); 346 $snippet = Snippet::getOrCreateSnippet($identifier, $type, $componentId) 347 ->addSlot($slot); 348 return $snippet; 349 } 350 351 private 352 function &attachSnippetFromRequest($componentName, $type, $internalOrUrlIdentifier): Snippet 353 { 354 $snippet = Snippet::getOrCreateSnippet($internalOrUrlIdentifier, $type, $componentName) 355 ->addSlot(Snippet::REQUEST_SLOT); 356 return $snippet; 357 } 358 359 360 /** 361 * Add a local javascript script as tag 362 * (ie same as {@link SnippetManager::attachJavascriptLibraryForSlot()}) 363 * but for local resource combo file (library) 364 * 365 * For instance: 366 * * library:combo:combo.js 367 * * for a file located at dokuwiki_home\lib\plugins\combo\resources\library\combo\combo.js 368 * @param string $snippetId - the snippet id 369 * @param string $relativeId - the relative id from the resources directory 370 */ 371 public 372 function attachJavascriptScriptForRequest(string $snippetId, string $relativeId) 373 { 374 $javascriptMedia = JavascriptLibrary::createJavascriptLibraryFromDokuwikiId($relativeId); 375 $url = $javascriptMedia->getUrl(); 376 return $this->attachSnippetFromRequest($snippetId, Snippet::EXTENSION_JS, $url); 377 378 } 379 380 /** 381 * @param string $snippetId 382 * @param string $relativeId 383 * @param string|null $integrity 384 * @return Snippet 385 */ 386 public 387 function attachJavascriptComboResourceForSlot(string $snippetId, string $relativeId, string $integrity = null): Snippet 388 { 389 $javascriptMedia = JavascriptLibrary::createJavascriptLibraryFromDokuwikiId($relativeId); 390 $url = $javascriptMedia->getUrl(); 391 return $this->attachJavascriptLibraryForSlot( 392 $snippetId, 393 $url, 394 $integrity 395 ); 396 397 } 398 399 public 400 function attachJavascriptComboLibrary() 401 { 402 return $this->attachJavascriptScriptForRequest("combo", "library:combo:dist:combo.min.js"); 403 } 404 405 public 406 function attachJavascriptLibraryForSlot(string $snippetId, string $url, string $integrity = null): Snippet 407 { 408 return $this 409 ->attachSnippetFromSlot( 410 $snippetId, 411 Snippet::EXTENSION_JS, 412 $url) 413 ->setIntegrity($integrity); 414 } 415 416 public 417 function attachCssExternalStyleSheetForSlot(string $snippetId, string $url, string $integrity = null): Snippet 418 { 419 return $this 420 ->attachSnippetFromSlot( 421 $snippetId, 422 Snippet::EXTENSION_CSS, 423 $url) 424 ->setIntegrity($integrity); 425 } 426 427 428 public function attachJavascriptLibraryForRequest(string $componentName, string $url, string $integrity): Snippet 429 { 430 return $this 431 ->attachSnippetFromRequest( 432 $componentName, 433 Snippet::EXTENSION_JS, 434 $url) 435 ->setIntegrity($integrity); 436 437 } 438 439 private function addExtraHtml(array $attributesArray, Snippet $snippet): array 440 { 441 $htmlAttributes = $snippet->getHtmlAttributes(); 442 if ($htmlAttributes !== null) { 443 foreach ($htmlAttributes as $name => $value) { 444 $attributesArray[$name] = $value; 445 } 446 } 447 return $attributesArray; 448 } 449 450 451} 452