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 switch ($linkData['rel']) { 83 case 'edit': 84 break; 85 case 'preload': 86 /** 87 * Preload can be set at the array level with the critical attribute 88 * If the preload attribute is present 89 * We get that for instance for css animation style sheet 90 * that are not needed for rendering 91 */ 92 if (isset($linkData["as"])) { 93 if ($linkData["as"] === "style") { 94 $newLinkData[] = self::captureStylePreloadingAndTransformToPreloadCssTag($linkData); 95 continue 2; 96 } 97 } 98 $newLinkData[] = $linkData; 99 break; 100 case 'stylesheet': 101 if ($cssPreloadConf) { 102 $newLinkData[] = self::captureStylePreloadingAndTransformToPreloadCssTag($linkData); 103 continue 2; 104 } 105 $newLinkData[] = $linkData; 106 break; 107 default: 108 $newLinkData[] = $linkData; 109 break; 110 } 111 } 112 113 $newHeaderTypes[$headTagName] = $newLinkData; 114 break; 115 116 case "script": 117 118 /** 119 * Script processing 120 * 121 * Do we delete the dokuwiki javascript ? 122 */ 123 $scriptToDeletes = []; 124 $disableBackend = SiteConfig::getConfValue(self::CONF_DISABLE_BACKEND_JAVASCRIPT, 0); 125 if (!Identity::isLoggedIn() && $disableBackend) { 126 $scriptToDeletes = [ 127 //'JSINFO', Don't delete Jsinfo !! It contains metadata information (that is used to get context) 128 'js.php' 129 ]; 130 if ($bootStrapMajorVersion == "5") { 131 // bs 5 does not depends on jquery 132 $scriptToDeletes[] = "jquery.php"; 133 } 134 } 135 136 /** 137 * The new script array 138 * that will replace the actual 139 */ 140 $newScriptTagAsArray = array(); 141 /** 142 * Scan: 143 * * Capture the Dokuwiki Jquery Tags 144 * * Delete for optimization if needed 145 * 146 * @var array A variable to hold the Jquery scripts 147 * jquery-migrate, jquery, jquery-ui ou jquery.php 148 * see https://www.dokuwiki.org/config:jquerycdn 149 */ 150 $jqueryDokuScriptsTagsAsArray = array(); 151 foreach ($headTagsAsArray as $scriptData) { 152 153 foreach ($scriptToDeletes as $scriptToDelete) { 154 if (isset($scriptData["_data"]) && !empty($scriptData["_data"])) { 155 $haystack = $scriptData["_data"]; 156 } else { 157 $haystack = $scriptData["src"]; 158 } 159 if (preg_match("/$scriptToDelete/i", $haystack)) { 160 continue 2; 161 } 162 } 163 164 $critical = false; 165 if (isset($scriptData["critical"])) { 166 $critical = $scriptData["critical"]; 167 unset($scriptData["critical"]); 168 } 169 170 // defer is only for external resource 171 // if this is not, this is illegal 172 if (isset($scriptData["src"])) { 173 if (!$critical) { 174 $scriptData['defer'] = null; 175 } 176 } 177 178 if (isset($scriptData["type"])) { 179 $type = strtolower($scriptData["type"]); 180 if ($type == "text/javascript") { 181 unset($scriptData["type"]); 182 } 183 } 184 185 // The charset attribute on the script element is obsolete. 186 if (isset($scriptData["charset"])) { 187 unset($scriptData["charset"]); 188 } 189 190 // Jquery ? 191 $jqueryFound = false; 192 // script may also be just an online script without the src attribute 193 if (array_key_exists('src', $scriptData)) { 194 $jqueryFound = strpos($scriptData['src'], 'jquery'); 195 } 196 if ($jqueryFound === false) { 197 $newScriptTagAsArray[] = $scriptData; 198 } else { 199 $jqueryDokuScriptsTagsAsArray[] = $scriptData; 200 } 201 202 } 203 204 /** 205 * Add Bootstrap scripts 206 * At the top of the queue 207 */ 208 if ($bootStrapMajorVersion === 4) { 209 $useJqueryDoku = ExecutionContext::getActualOrCreateFromEnv()->getConfig()->getBooleanValue(self::CONF_JQUERY_DOKU, self::CONF_JQUERY_DOKU_DEFAULT); 210 if ( 211 !Identity::isLoggedIn() 212 && !$useJqueryDoku 213 ) { 214 /** 215 * We take the Javascript of Bootstrap 216 * (Jquery and others) 217 */ 218 $boostrapSnippetsAsArray = []; 219 foreach ($bootstrap->getJsSnippets() as $snippet) { 220 $boostrapSnippetsAsArray[] = $snippet->toDokuWikiArray(); 221 } 222 /** 223 * At the top of the queue 224 */ 225 $newScriptTagAsArray = array_merge($boostrapSnippetsAsArray, $newScriptTagAsArray); 226 } else { 227 // Logged in 228 // We take the Jqueries of doku and we add Bootstrap 229 $newScriptTagAsArray = array_merge($jqueryDokuScriptsTagsAsArray, $newScriptTagAsArray); // js 230 // We had popper of Bootstrap 231 $newScriptTagAsArray[] = $bootstrap->getPopperSnippet()->toDokuWikiArray(); 232 // We had the js of Bootstrap 233 $newScriptTagAsArray[] = $bootstrap->getBootstrapJsSnippet()->toDokuWikiArray(); 234 } 235 } else { 236 237 // There is no JQuery in 5 238 // We had the js of Bootstrap and popper 239 // Add Jquery before the js.php 240 $newScriptTagAsArray = array_merge($jqueryDokuScriptsTagsAsArray, $newScriptTagAsArray); // js 241 // Then add at the top of the top (first of the first) bootstrap 242 // Why ? Because Jquery should be last to be able to see the missing icon 243 // https://stackoverflow.com/questions/17367736/jquery-ui-dialog-missing-close-icon 244 $bootstrapTagArray[] = $bootstrap->getPopperSnippet()->toDokuWikiArray(); 245 $bootstrapTagArray[] = $bootstrap->getBootstrapJsSnippet()->toDokuWikiArray(); 246 $newScriptTagAsArray = array_merge($bootstrapTagArray, $newScriptTagAsArray); 247 248 } 249 250 $newHeaderTypes[$headTagName] = $newScriptTagAsArray; 251 break; 252 case "meta": 253 $newHeaderData = array(); 254 foreach ($headTagsAsArray as $metaData) { 255 // Content should never be null 256 // Name may change 257 // https://www.w3.org/TR/html4/struct/global.html#edef-META 258 if (!key_exists("content", $metaData)) { 259 $message = "The head meta (" . print_r($metaData, true) . ") does not have a content property"; 260 LogUtility::error($message, self::FRONT_END_OPTIMIZATION_CANONICAL); 261 } else { 262 $content = $metaData["content"]; 263 if (empty($content)) { 264 $messageEmpty = "the below head meta has an empty content property (" . ArrayUtility::formatAsString($metaData) . ")"; 265 LogUtility::error($messageEmpty, self::FRONT_END_OPTIMIZATION_CANONICAL); 266 } else { 267 $newHeaderData[] = $metaData; 268 } 269 } 270 } 271 $newHeaderTypes[$headTagName] = $newHeaderData; 272 break; 273 case "noscript": // https://github.com/ComboStrap/dokuwiki-plugin-gtm/blob/master/action.php#L32 274 case "style": 275 $newHeaderTypes[$headTagName] = $headTagsAsArray; 276 break; 277 default: 278 $message = "The header type ($headTagName) is unknown and was not controlled."; 279 $newHeaderTypes[$headTagName] = $headTagsAsArray; 280 LogUtility::error($message, self::FRONT_END_OPTIMIZATION_CANONICAL); 281 282 } 283 } 284 285 $event->data = $newHeaderTypes; 286 287 288 } 289 290 /** 291 * @param $linkData - an array of link style sheet data 292 * @return array - the array with the preload attributes 293 */ 294 public static function captureStylePreloadingAndTransformToPreloadCssTag($linkData): array 295 { 296 /** 297 * Save the stylesheet to load it at the end 298 */ 299 $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 300 try { 301 $preloadedCss = &$executionContext->getRuntimeObject(TemplateForWebPage::PRELOAD_TAG); 302 } catch (ExceptionNotFound $e) { 303 $preloadedCss = []; 304 $executionContext->setRuntimeObject(TemplateForWebPage::PRELOAD_TAG,$preloadedCss); 305 } 306 $preloadedCss[] = $linkData; 307 308 /** 309 * Modify the actual tag data 310 * Change the loading mechanism to preload 311 */ 312 $linkData['rel'] = 'preload'; 313 $linkData['as'] = 'style'; 314 return $linkData; 315 } 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