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