1<?php
2/**
3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved.
4 *
5 * This source code is licensed under the GPL license found in the
6 * COPYING  file in the root directory of this source tree.
7 *
8 * @license  GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html)
9 * @author   ComboStrap <support@combostrap.com>
10 *
11 */
12
13namespace ComboStrap;
14
15require_once(__DIR__ . '/Snippet.php');
16
17/**
18 * @package ComboStrap
19 *
20 * Public interface of {@link Snippet}
21 *
22 * All plugin/component should use the attach functions to add a internal or external
23 * stylesheet/javascript to a slot or request scoped
24 *
25 * Note:
26 * All function with the suffix
27 *   * `ForSlot` are snippets for a bar (ie page, sidebar, ...) - cached
28 *   * `ForRequests` are snippets added for the HTTP request - not cached. Example of request component: message, anchor
29 */
30class SnippetManager
31{
32
33    const COMBO_CLASS_SUFFIX = "combo";
34
35
36    const CANONICAL = "snippet-manager";
37    const SCRIPT_TAG = "script";
38    const LINK_TAG = "link";
39    const STYLE_TAG = "style";
40    const DATA_DOKUWIKI_ATT = "_data";
41
42
43    /**
44     * @var SnippetManager array that contains one element (one {@link SnippetManager} scoped to the requested id
45     */
46    private static $globalSnippetManager;
47
48    /**
49     * Empty the snippets
50     * This is used to render the snippet only once
51     * The snippets renders first in the head
52     * and otherwise at the end of the document
53     * if the user are using another template or are in edit mode
54     */
55    public static function reset()
56    {
57        self::$globalSnippetManager = null;
58        Snippet::reset();
59    }
60
61
62    /**
63     * @param $tag
64     * @return string
65     * @deprecated create a {@link Snippet} instead and use the {@link Snippet::getClass()} function instead
66     */
67    public static function getClassFromSnippetId($tag): string
68    {
69        $snippet = Snippet::createUnknownSnippet($tag);
70        return $snippet->getClass();
71    }
72
73
74    /**
75     * @return SnippetManager - the global reference
76     * that is set for every run at the end of this file
77     * TODO: migrate the attach function to {@link Snippet}
78     *   because Snippet has already a global variable {@link Snippet::getOrCreateSnippet()}
79     */
80    public static function getOrCreate(): SnippetManager
81    {
82        $id = PluginUtility::getRequestedWikiId();
83        if ($id === null) {
84            if (PluginUtility::isTest()) {
85                $id = "test_dynamic_script_execution";
86            } else {
87                LogUtility::msg("The requested Id could not be found, the snippets may not be scoped properly");
88            }
89        }
90
91        $snippetManager = self::$globalSnippetManager[$id];
92        if ($snippetManager === null) {
93            self::$globalSnippetManager = null; // delete old snippet manager for other request
94            $snippetManager = new SnippetManager();
95            self::$globalSnippetManager[$id] = $snippetManager;
96        }
97        return $snippetManager;
98    }
99
100
101    /**
102     * Transform in dokuwiki format
103     *
104     * @return array of node type and an array of array of html attributes
105     */
106    public function getAllSnippetsToDokuwikiArray(): array
107    {
108        $snippets = Snippet::getSnippets();
109        if ($snippets === null) {
110            return [];
111        }
112        /**
113         * The returned array in dokuwiki format
114         */
115        $returnedDokuWikiFormat = array();
116
117        /**
118         * Processing the external resources
119         * and collecting the internal one
120         *
121         * The order is the order where they were added/created.
122         *
123         * The internal script may be dependent on the external javascript
124         * and vice-versa (for instance, Math-Jax library is dependent
125         * on the config that is an internal script)
126         *
127         */
128        foreach ($snippets as $snippet) {
129
130            $type = $snippet->getType();
131
132
133            $extension = $snippet->getExtension();
134            switch ($extension) {
135                case Snippet::EXTENSION_JS:
136                    switch ($type) {
137                        case Snippet::EXTERNAL_TYPE:
138
139                            $jsDokuwiki = array(
140                                "class" => $snippet->getClass(),
141                                "src" => $snippet->getUrl(),
142                                "crossorigin" => "anonymous"
143                            );
144                            $integrity = $snippet->getIntegrity();
145                            if ($integrity !== null) {
146                                $jsDokuwiki["integrity"] = $integrity;
147                            }
148                            $critical = $snippet->getCritical();
149                            if (!$critical) {
150                                $jsDokuwiki["defer"] = null;
151                                // not async: it will run as soon as possible
152                                // the dom main not be loaded completely, the script may miss HTML dom element
153                            }
154                            $jsDokuwiki = $this->addExtraHtml($jsDokuwiki, $snippet);
155                            ksort($jsDokuwiki);
156                            $returnedDokuWikiFormat[self::SCRIPT_TAG][] = $jsDokuwiki;
157                            break;
158                        case Snippet::INTERNAL_TYPE:
159                            $content = $snippet->getInternalInlineAndFileContent();
160                            if ($content === null) {
161                                LogUtility::msg("The internal js snippet ($snippet) has no content. Skipped");
162                                continue 3;
163                            }
164                            $jsDokuwiki = array(
165                                "class" => $snippet->getClass(),
166                                self::DATA_DOKUWIKI_ATT => $content
167                            );
168                            $jsDokuwiki = $this->addExtraHtml($jsDokuwiki, $snippet);
169                            $returnedDokuWikiFormat[self::SCRIPT_TAG][] = $jsDokuwiki;
170                            break;
171                        default:
172                            LogUtility::msg("Unknown javascript snippet type");
173                    }
174                    break;
175                case Snippet::EXTENSION_CSS:
176                    switch ($type) {
177                        case Snippet::EXTERNAL_TYPE:
178                            $cssDokuwiki = array(
179                                "class" => $snippet->getClass(),
180                                "rel" => "stylesheet",
181                                "href" => $snippet->getUrl(),
182                                "crossorigin" => "anonymous"
183                            );
184                            $integrity = $snippet->getIntegrity();
185                            if ($integrity !== null) {
186                                $cssDokuwiki["integrity"] = $integrity;
187                            }
188                            $critical = $snippet->getCritical();
189                            if (!$critical && Site::getTemplate() === Site::STRAP_TEMPLATE_NAME) {
190                                $cssDokuwiki["rel"] = "preload";
191                                $cssDokuwiki['as'] = self::STYLE_TAG;
192                            }
193                            $cssDokuwiki = $this->addExtraHtml($cssDokuwiki, $snippet);
194                            ksort($cssDokuwiki);
195                            $returnedDokuWikiFormat[self::LINK_TAG][] = $cssDokuwiki;
196                            break;
197                        case Snippet::INTERNAL_TYPE:
198                            /**
199                             * CSS inline in script tag
200                             * They are all critical
201                             */
202                            $content = $snippet->getInternalInlineAndFileContent();
203                            if ($content === null) {
204                                LogUtility::msg("The internal css snippet ($snippet) has no content. Skipped");
205                                continue 3;
206                            }
207                            $cssInternalArray = array(
208                                "class" => $snippet->getClass(),
209                                self::DATA_DOKUWIKI_ATT => $content
210                            );
211                            $cssInternalArray = $this->addExtraHtml($cssInternalArray, $snippet);
212                            $returnedDokuWikiFormat[self::STYLE_TAG][] = $cssInternalArray;
213                            break;
214                        default:
215                            LogUtility::msg("Unknown css snippet type");
216                    }
217                    break;
218                default:
219                    LogUtility::msg("The extension ($extension) is unknown, the external snippet ($snippet) was not added");
220            }
221
222        }
223
224        return $returnedDokuWikiFormat;
225    }
226
227    /**
228     * @deprecated see {@link SnippetManager::reset()}
229     *
230     */
231    public
232    function close()
233    {
234        self::reset();
235    }
236
237
238    public
239    function getJsonArrayFromSlotSnippets($slot): ?array
240    {
241        $snippets = Snippet::getSnippets();
242        if ($snippets === null) {
243            return null;
244        }
245        $snippetsForSlot = array_filter($snippets,
246            function ($s) use ($slot) {
247                return $s->hasSlot($slot);
248            });
249        $jsonSnippets = null;
250        foreach ($snippetsForSlot as $snippet) {
251            $jsonSnippets[] = $snippet->toJsonArray();
252        }
253        return $jsonSnippets;
254
255    }
256
257    /**
258     * @param array $array
259     * @param string $slot
260     * @return null|Snippet[]
261     * @throws ExceptionCombo
262     */
263    public
264    function getSlotSnippetsFromJsonArray(array $array, string $slot): ?array
265    {
266        $snippets = null;
267        foreach ($array as $element) {
268            $snippets[] = Snippet::createFromJson($element)
269                ->addSlot($slot);
270        }
271        return $snippets;
272    }
273
274
275    /**
276     * @param $snippetId
277     * @param string|null $script - the css snippet to add, otherwise it takes the file
278     * @return Snippet a snippet not in a slot
279     */
280    public
281    function &attachCssInternalStyleSheetForSlot($snippetId, string $script = null): Snippet
282    {
283        $snippet = $this->attachSnippetFromSlot($snippetId, Snippet::EXTENSION_CSS, Snippet::INTERNAL_TYPE);
284        if ($script !== null) {
285            $snippet->setInlineContent($script);
286        }
287        return $snippet;
288    }
289
290    /**
291     * @param $snippetId
292     * @param string|null $script -  the css if any, otherwise the css file will be taken
293     * @return Snippet a snippet scoped at the request scope (not in a slot)
294     */
295    public
296    function &attachCssSnippetForRequest($snippetId, string $script = null): Snippet
297    {
298        $snippet = $this->attachSnippetFromRequest($snippetId, Snippet::EXTENSION_CSS, Snippet::INTERNAL_TYPE);
299        if ($script != null) {
300            $snippet->setInlineContent($script);
301        }
302        return $snippet;
303    }
304
305    /**
306     * @param $snippetId
307     * @param string|null $script
308     * @return Snippet a snippet in a slot
309     */
310    public
311    function &attachInternalJavascriptForSlot($snippetId, string $script = null): Snippet
312    {
313        $snippet = &$this->attachSnippetFromSlot($snippetId, Snippet::EXTENSION_JS, Snippet::INTERNAL_TYPE);
314        if ($script !== null) {
315            $content = $snippet->getInternalDynamicContent();
316            if ($content !== null) {
317                $content .= $script;
318            } else {
319                $content = $script;
320            }
321            $snippet->setInlineContent($content);
322        }
323        return $snippet;
324    }
325
326    /**
327     * @param $snippetId
328     * @return Snippet a snippet not in a slot
329     */
330    public
331    function &attachJavascriptSnippetForRequest($snippetId): Snippet
332    {
333        return $this->attachSnippetFromRequest($snippetId, Snippet::EXTENSION_JS, Snippet::INTERNAL_TYPE);
334    }
335
336    /**
337     * @param string $componentId
338     * @param string $type
339     * @param string $identifier
340     * @return Snippet
341     */
342    private
343    function &attachSnippetFromSlot(string $componentId, string $type, string $identifier): Snippet
344    {
345        $slot = PluginUtility::getCurrentSlotId();
346        $snippet = Snippet::getOrCreateSnippet($identifier, $type, $componentId)
347            ->addSlot($slot);
348        return $snippet;
349    }
350
351    private
352    function &attachSnippetFromRequest($componentName, $type, $internalOrUrlIdentifier): Snippet
353    {
354        $snippet = Snippet::getOrCreateSnippet($internalOrUrlIdentifier, $type, $componentName)
355            ->addSlot(Snippet::REQUEST_SLOT);
356        return $snippet;
357    }
358
359
360    /**
361     * Add a local javascript script as tag
362     * (ie same as {@link SnippetManager::attachJavascriptLibraryForSlot()})
363     * but for local resource combo file (library)
364     *
365     * For instance:
366     *   * library:combo:combo.js
367     *   * for a file located at dokuwiki_home\lib\plugins\combo\resources\library\combo\combo.js
368     * @param string $snippetId - the snippet id
369     * @param string $relativeId - the relative id from the resources directory
370     */
371    public
372    function attachJavascriptScriptForRequest(string $snippetId, string $relativeId)
373    {
374        $javascriptMedia = JavascriptLibrary::createJavascriptLibraryFromDokuwikiId($relativeId);
375        $url = $javascriptMedia->getUrl();
376        return $this->attachSnippetFromRequest($snippetId, Snippet::EXTENSION_JS, $url);
377
378    }
379
380    /**
381     * @param string $snippetId
382     * @param string $relativeId
383     * @param string|null $integrity
384     * @return Snippet
385     */
386    public
387    function attachJavascriptComboResourceForSlot(string $snippetId, string $relativeId, string $integrity = null): Snippet
388    {
389        $javascriptMedia = JavascriptLibrary::createJavascriptLibraryFromDokuwikiId($relativeId);
390        $url = $javascriptMedia->getUrl();
391        return $this->attachJavascriptLibraryForSlot(
392            $snippetId,
393            $url,
394            $integrity
395        );
396
397    }
398
399    public
400    function attachJavascriptComboLibrary()
401    {
402        return $this->attachJavascriptScriptForRequest("combo", "library:combo:dist:combo.min.js");
403    }
404
405    public
406    function attachJavascriptLibraryForSlot(string $snippetId, string $url, string $integrity = null): Snippet
407    {
408        return $this
409            ->attachSnippetFromSlot(
410                $snippetId,
411                Snippet::EXTENSION_JS,
412                $url)
413            ->setIntegrity($integrity);
414    }
415
416    public
417    function attachCssExternalStyleSheetForSlot(string $snippetId, string $url, string $integrity = null): Snippet
418    {
419        return $this
420            ->attachSnippetFromSlot(
421                $snippetId,
422                Snippet::EXTENSION_CSS,
423                $url)
424            ->setIntegrity($integrity);
425    }
426
427
428    public function attachJavascriptLibraryForRequest(string $componentName, string $url, string $integrity): Snippet
429    {
430        return $this
431            ->attachSnippetFromRequest(
432                $componentName,
433                Snippet::EXTENSION_JS,
434                $url)
435            ->setIntegrity($integrity);
436
437    }
438
439    private function addExtraHtml(array $attributesArray, Snippet $snippet): array
440    {
441        $htmlAttributes = $snippet->getHtmlAttributes();
442        if ($htmlAttributes !== null) {
443            foreach ($htmlAttributes as $name => $value) {
444                $attributesArray[$name] = $value;
445            }
446        }
447        return $attributesArray;
448    }
449
450
451}
452