xref: /plugin/combo/action/snippetsbootstrap.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
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