1<?php 2/** 3 * DokuWiki Plugin Js Action 4 * 5 */ 6 7use ComboStrap\ArrayUtility; 8use ComboStrap\Bootstrap; 9use ComboStrap\ExceptionNotFound; 10use ComboStrap\ExecutionContext; 11use ComboStrap\Identity; 12use ComboStrap\LogUtility; 13use ComboStrap\TemplateForWebPage; 14use ComboStrap\SiteConfig; 15 16 17/** 18 * 19 * 20 */ 21class action_plugin_combo_snippetsbootstrap extends DokuWiki_Action_Plugin 22{ 23 24 25 public const CONF_PRELOAD_CSS = "preloadCss"; 26 /** 27 * Use the Jquery of Dokuwiki and not of Bootstrap 28 */ 29 public const CONF_JQUERY_DOKU = 'jQueryDoku'; 30 public const CONF_JQUERY_DOKU_DEFAULT = 0; 31 32 /** 33 * Disable the javascript of Dokuwiki 34 * if public 35 * https://combostrap.com/frontend/optimization 36 */ 37 public const CONF_DISABLE_BACKEND_JAVASCRIPT = "disableBackendJavascript"; 38 39 /** 40 * This is so a bad practice, default to no 41 * but fun to watch 42 */ 43 const CONF_PRELOAD_CSS_DEFAULT = 0; 44 const JQUERY_CANONICAL = "jquery"; 45 const FRONT_END_OPTIMIZATION_CANONICAL = 'frontend:optimization'; 46 47 48 /** 49 * @param Doku_Event $event 50 * @param $param 51 * Function that handle the META HEADER event 52 * * It will add the Bootstrap Js and CSS 53 * * Make all script and resources defer 54 * @throws Exception 55 * @noinspection PhpUnused 56 */ 57 public static function handle_bootstrap(Doku_Event &$event, $param) 58 { 59 60 61 $newHeaderTypes = array(); 62 63 $bootstrap = Bootstrap::getFromContext(); 64 $bootStrapMajorVersion = $bootstrap->getMajorVersion(); 65 $eventHeaderTypes = $event->data; 66 $executionContextConfig = ExecutionContext::getActualOrCreateFromEnv()->getConfig(); 67 foreach ($eventHeaderTypes as $headTagName => $headTagsAsArray) { 68 switch ($headTagName) { 69 70 case "link": 71 /** 72 * Link tag processing 73 * ie index, rss, manifest, search, alternate, stylesheet 74 */ 75 $headTagsAsArray[] = $bootstrap->getCssSnippet()->toDokuWikiArray(); 76 77 // preload all CSS is an heresy as it creates a FOUC (Flash of non-styled element) 78 // but we know it only now and this is fun to experience for the user 79 $cssPreloadConf = $executionContextConfig->getValue(self::CONF_PRELOAD_CSS, self::CONF_PRELOAD_CSS_DEFAULT); 80 $newLinkData = array(); 81 foreach ($headTagsAsArray as $linkData) { 82 $rel = $linkData['rel'] ?? null; 83 switch ($rel) { 84 case 'edit': 85 break; 86 case 'preload': 87 /** 88 * Preload can be set at the array level with the critical attribute 89 * If the preload attribute is present 90 * We get that for instance for css animation style sheet 91 * that are not needed for rendering 92 */ 93 if (isset($linkData["as"])) { 94 if ($linkData["as"] === "style") { 95 $newLinkData[] = self::captureStylePreloadingAndTransformToPreloadCssTag($linkData); 96 continue 2; 97 } 98 } 99 $newLinkData[] = $linkData; 100 break; 101 case 'stylesheet': 102 if ($cssPreloadConf) { 103 $newLinkData[] = self::captureStylePreloadingAndTransformToPreloadCssTag($linkData); 104 continue 2; 105 } 106 $newLinkData[] = $linkData; 107 break; 108 default: 109 $newLinkData[] = $linkData; 110 break; 111 } 112 } 113 114 $newHeaderTypes[$headTagName] = $newLinkData; 115 break; 116 117 case "script": 118 119 /** 120 * Script processing 121 * 122 * Do we delete the dokuwiki javascript ? 123 */ 124 $scriptToDeletes = []; 125 $disableBackend = SiteConfig::getConfValue(self::CONF_DISABLE_BACKEND_JAVASCRIPT, 0); 126 if (!Identity::isLoggedIn() && $disableBackend) { 127 $scriptToDeletes = [ 128 //'JSINFO', Don't delete Jsinfo !! It contains metadata information (that is used to get context) 129 'js.php' 130 ]; 131 if ($bootStrapMajorVersion == "5") { 132 // bs 5 does not depends on jquery 133 $scriptToDeletes[] = "jquery.php"; 134 } 135 } 136 137 /** 138 * The new script array 139 * that will replace the actual 140 */ 141 $newScriptTagAsArray = array(); 142 /** 143 * Scan: 144 * * Capture the Dokuwiki Jquery Tags 145 * * Delete for optimization if needed 146 * 147 * @var array A variable to hold the Jquery scripts 148 * jquery-migrate, jquery, jquery-ui ou jquery.php 149 * see https://www.dokuwiki.org/config:jquerycdn 150 */ 151 $jqueryDokuScriptsTagsAsArray = array(); 152 foreach ($headTagsAsArray as $scriptData) { 153 154 foreach ($scriptToDeletes as $scriptToDelete) { 155 if (isset($scriptData["_data"]) && !empty($scriptData["_data"])) { 156 $haystack = $scriptData["_data"]; 157 } else { 158 $haystack = $scriptData["src"]; 159 } 160 if (preg_match("/$scriptToDelete/i", $haystack)) { 161 continue 2; 162 } 163 } 164 165 $critical = false; 166 if (isset($scriptData["critical"])) { 167 $critical = $scriptData["critical"]; 168 unset($scriptData["critical"]); 169 } 170 171 // defer is only for external resource 172 // if this is not, this is illegal 173 if (isset($scriptData["src"])) { 174 if (!$critical) { 175 $scriptData['defer'] = null; 176 } 177 } 178 179 if (isset($scriptData["type"])) { 180 $type = strtolower($scriptData["type"]); 181 if ($type == "text/javascript") { 182 unset($scriptData["type"]); 183 } 184 } 185 186 // The charset attribute on the script element is obsolete. 187 if (isset($scriptData["charset"])) { 188 unset($scriptData["charset"]); 189 } 190 191 // Jquery ? 192 $jqueryFound = false; 193 // script may also be just an online script without the src attribute 194 if (array_key_exists('src', $scriptData)) { 195 $jqueryFound = strpos($scriptData['src'], 'jquery'); 196 } 197 if ($jqueryFound === false) { 198 $newScriptTagAsArray[] = $scriptData; 199 } else { 200 $jqueryDokuScriptsTagsAsArray[] = $scriptData; 201 } 202 203 } 204 205 /** 206 * Add Bootstrap scripts 207 * At the top of the queue 208 */ 209 if ($bootStrapMajorVersion === 4) { 210 $useJqueryDoku = ExecutionContext::getActualOrCreateFromEnv()->getConfig()->getBooleanValue(self::CONF_JQUERY_DOKU, self::CONF_JQUERY_DOKU_DEFAULT); 211 if ( 212 !Identity::isLoggedIn() 213 && !$useJqueryDoku 214 ) { 215 /** 216 * We take the Javascript of Bootstrap 217 * (Jquery and others) 218 */ 219 $boostrapSnippetsAsArray = []; 220 foreach ($bootstrap->getJsSnippets() as $snippet) { 221 $boostrapSnippetsAsArray[] = $snippet->toDokuWikiArray(); 222 } 223 /** 224 * At the top of the queue 225 */ 226 $newScriptTagAsArray = array_merge($boostrapSnippetsAsArray, $newScriptTagAsArray); 227 } else { 228 // Logged in 229 // We take the Jqueries of doku and we add Bootstrap 230 $newScriptTagAsArray = array_merge($jqueryDokuScriptsTagsAsArray, $newScriptTagAsArray); // js 231 // We had popper of Bootstrap 232 $newScriptTagAsArray[] = $bootstrap->getPopperSnippet()->toDokuWikiArray(); 233 // We had the js of Bootstrap 234 $newScriptTagAsArray[] = $bootstrap->getBootstrapJsSnippet()->toDokuWikiArray(); 235 } 236 } else { 237 238 // There is no JQuery in 5 239 // We had the js of Bootstrap and popper 240 // Add Jquery before the js.php 241 $newScriptTagAsArray = array_merge($jqueryDokuScriptsTagsAsArray, $newScriptTagAsArray); // js 242 // Then add at the top of the top (first of the first) bootstrap 243 // Why ? Because Jquery should be last to be able to see the missing icon 244 // https://stackoverflow.com/questions/17367736/jquery-ui-dialog-missing-close-icon 245 $bootstrapTagArray[] = $bootstrap->getPopperSnippet()->toDokuWikiArray(); 246 $bootstrapTagArray[] = $bootstrap->getBootstrapJsSnippet()->toDokuWikiArray(); 247 $newScriptTagAsArray = array_merge($bootstrapTagArray, $newScriptTagAsArray); 248 249 } 250 251 $newHeaderTypes[$headTagName] = $newScriptTagAsArray; 252 break; 253 case "meta": 254 $newHeaderData = array(); 255 foreach ($headTagsAsArray as $metaData) { 256 // Content should never be null 257 // Name may change 258 // https://www.w3.org/TR/html4/struct/global.html#edef-META 259 if (!key_exists("content", $metaData)) { 260 $message = "The head meta (" . print_r($metaData, true) . ") does not have a content property"; 261 LogUtility::error($message, self::FRONT_END_OPTIMIZATION_CANONICAL); 262 } else { 263 $content = $metaData["content"]; 264 if (empty($content)) { 265 $messageEmpty = "the below head meta has an empty content property (" . ArrayUtility::formatAsString($metaData) . ")"; 266 LogUtility::error($messageEmpty, self::FRONT_END_OPTIMIZATION_CANONICAL); 267 } else { 268 $newHeaderData[] = $metaData; 269 } 270 } 271 } 272 $newHeaderTypes[$headTagName] = $newHeaderData; 273 break; 274 case "noscript": // https://github.com/ComboStrap/dokuwiki-plugin-gtm/blob/master/action.php#L32 275 case "style": 276 $newHeaderTypes[$headTagName] = $headTagsAsArray; 277 break; 278 default: 279 $message = "The header type ($headTagName) is unknown and was not controlled."; 280 $newHeaderTypes[$headTagName] = $headTagsAsArray; 281 LogUtility::error($message, self::FRONT_END_OPTIMIZATION_CANONICAL); 282 283 } 284 } 285 286 $event->data = $newHeaderTypes; 287 288 289 } 290 291 /** 292 * @param $linkData - an array of link style sheet data 293 * @return array - the array with the preload attributes 294 */ 295 public static function captureStylePreloadingAndTransformToPreloadCssTag($linkData): array 296 { 297 /** 298 * Save the stylesheet to load it at the end 299 */ 300 $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 301 try { 302 $preloadedCss = &$executionContext->getRuntimeObject(TemplateForWebPage::PRELOAD_TAG); 303 } catch (ExceptionNotFound $e) { 304 $preloadedCss = []; 305 $executionContext->setRuntimeObject(TemplateForWebPage::PRELOAD_TAG, $preloadedCss); 306 } 307 $preloadedCss[] = $linkData; 308 309 /** 310 * Modify the actual tag data 311 * Change the loading mechanism to preload 312 */ 313 $linkData['rel'] = 'preload'; 314 $linkData['as'] = 'style'; 315 return $linkData; 316 } 317 318 319 /** 320 * Registers our handler for the MANIFEST_SEND event 321 * https://www.dokuwiki.org/devel:event:js_script_list 322 * manipulate the list of JavaScripts that will be concatenated 323 * @param Doku_Event_Handler $controller 324 */ 325 public function register(Doku_Event_Handler $controller) 326 { 327 328 $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'handle_bootstrap'); 329 330 } 331 332 333} 334 335