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