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 * A component to manage the extra HTML that
20 * comes from components and that should come in the head HTML node
21 *
22 * The snippet manager handles two scope of snippet
23 * All function with the suffix
24 *   * `ForBar` are snippets for a bar (ie page, sidebar, ...) - cached
25 *   * `ForRequests` are snippets added for the HTTP request - not cached. Example of request component: message, anchor
26 *
27 */
28class SnippetManager
29{
30
31    const COMBO_CLASS_SUFFIX = "combo";
32
33    /**
34     * If a snippet is critical, it should not be deferred
35     *
36     * By default:
37     *   * all css are critical (except animation or background stylesheet)
38     *   * all javascript are not critical
39     *
40     * This attribute is passed in the dokuwiki array
41     * The value is stored in the {@link Snippet::getCritical()}
42     */
43    const CRITICAL_ATTRIBUTE = "critical";
44
45
46    /**
47     *
48     * The scope is used for snippet that are not added
49     * by the syntax plugin but by the actions plugin
50     * It's also used in the cache because not all bars
51     * may render at the same time due to the other been cached.
52     *
53     * There is two scope:
54     *   * {@link SnippetManager::$snippetsByBarScope}
55     *   * or {@link SnippetManager::$snippetsByRequestScope}
56     */
57
58    /**
59     * @var array all snippets scope to the bar level
60     */
61    private $snippetsByBarScope = array();
62
63    /**
64     * @var array heads that are unique on a request scope
65     *
66     * TlDR: The snippet does not depends to a Page and cannot therefore be cached along.
67     *
68     * The code that adds this snippet is not created by the parsing of content
69     * or depends on the page.
70     *
71     * It's always called and add the snippet whatsoever.
72     * Generally, this is an action plugin with a `TPL_METAHEADER_OUTPUT` hook
73     * such as {@link Bootstrap}, {@link HistoricalBreadcrumbMenuItem},
74     * ,...
75     */
76    private $snippetsByRequestScope = array();
77
78
79
80    public static function init()
81    {
82        global $componentScript;
83        $componentScript = new SnippetManager();
84    }
85
86
87    /**
88     * @param $tag
89     * @return string
90     * @deprecated create a {@link Snippet} instead and use the {@link Snippet::getClass()} function instead
91     */
92    public static function getClassFromSnippetId($tag)
93    {
94        $snippet = Snippet::createUnknownSnippet($tag);
95        return $snippet->getClass();
96    }
97
98
99    /**
100     * @param $snippetId
101     * @param string|null $css - the css
102     *   if null, the file $snippetId.css is searched in the `style` directory
103     * @return Snippet
104     * @deprecated use {@link SnippetManager::attachCssSnippetForBar()} instead
105     */
106    public function &upsertCssSnippetForBar($snippetId, $css = null)
107    {
108        $snippet = &$this->attachCssSnippetForBar($snippetId);
109        if ($css != null) {
110            $snippet->setContent($css);
111        }
112        return $snippet;
113
114    }
115
116    /**
117     * @param $snippetId
118     * @param $script - javascript code if null, it will search in the js directory
119     * @return Snippet
120     * @deprecated use {@link SnippetManager::attachJavascriptSnippetForBar()} instead
121     */
122    public function &upsertJavascriptForBar($snippetId, $script = null)
123    {
124        $snippet = &$this->attachJavascriptSnippetForBar($snippetId);
125        if ($script != null) {
126            $snippet->setContent($script);
127        }
128        return $snippet;
129    }
130
131    /**
132     * @param $snippetId
133     * @param array $tags - an array of tags without content where the key is the node type and the value a array of attributes array
134     * @return Snippet
135     */
136    public function &upsertTagsForBar($snippetId, $tags)
137    {
138        $snippet = &$this->attachTagsForBar($snippetId);
139        $snippet->setTags($tags);
140        return $snippet;
141    }
142
143
144    /**
145     * @return SnippetManager - the global reference
146     * that is set for every run at the end of this fille
147     */
148    public static function get()
149    {
150        global $componentScript;
151        if (empty($componentScript)) {
152            SnippetManager::init();
153        }
154        return $componentScript;
155    }
156
157
158    /**
159     * @return array of node type and an array of array of html attributes
160     */
161    public function getSnippets()
162    {
163        /**
164         * Distinct Snippet
165         */
166        $distinctSnippetIdByType = [];
167        if (sizeof($this->snippetsByRequestScope) == 1) {
168            /**
169             * There is only 0 or 1 value
170             * because this is still scoped to the actual request (by the requested id)
171             */
172            $distinctSnippetIdByType = array_shift($this->snippetsByRequestScope);
173        }
174        foreach ($this->snippetsByBarScope as $snippet) {
175            $distinctSnippetIdByType = $this->mergeSnippetArray($distinctSnippetIdByType, $snippet);
176        }
177
178
179        /**
180         * Transform in dokuwiki format
181         * We collect the separately head that have content
182         * from the head that refers to external resources
183         * because the content will depends on the resources
184         * and should then come in the last position
185         */
186        $dokuWikiHeadsFormatContent = array();
187        $dokuWikiHeadsSrc = array();
188        foreach ($distinctSnippetIdByType as $snippetType => $snippetBySnippetId) {
189            switch ($snippetType) {
190                case Snippet::TYPE_JS:
191                    foreach ($snippetBySnippetId as $snippetId => $snippet) {
192                        /**
193                         * Bug (Quick fix)
194                         */
195                        if (is_string($snippet)) {
196                            LogUtility::msg("The snippet ($snippetId) is a string ($snippet) and not a snippet object", LogUtility::LVL_MSG_ERROR);
197                            $content = $snippet;
198                        } else {
199                            $content = $snippet->getContent();
200                        }
201                        /** @var Snippet $snippet */
202                        $dokuWikiHeadsFormatContent["script"][] = array(
203                            "class" => $snippet->getClass(),
204                            "_data" => $content
205                        );
206                    }
207                    break;
208                case Snippet::TYPE_CSS:
209                    /**
210                     * CSS inline in script tag
211                     * They are all critical
212                     */
213                    foreach ($snippetBySnippetId as $snippetId => $snippet) {
214                        /**
215                         * Bug (Quick fix)
216                         */
217                        if (is_string($snippet)) {
218                            LogUtility::msg("The snippet ($snippetId) is a string ($snippet) and not a snippet object", LogUtility::LVL_MSG_ERROR);
219                            $content = $snippet;
220                        } else {
221                            /**
222                             * @var Snippet $snippet
223                             */
224                            $content = $snippet->getContent();
225                        }
226                        $snippetArray = array(
227                            "class" => $snippet->getClass(),
228                            "_data" => $content
229                        );
230                        /** @var Snippet $snippet */
231                        $dokuWikiHeadsFormatContent["style"][] = $snippetArray;
232                    }
233                    break;
234                case Snippet::TAG_TYPE:
235                    foreach ($snippetBySnippetId as $snippetId => $tagsSnippet) {
236                        /** @var Snippet $tagsSnippet */
237                        foreach ($tagsSnippet->getTags() as $snippetType => $heads) {
238                            $classFromSnippetId = self::getClassFromSnippetId($snippetId);
239                            foreach ($heads as $head) {
240                                if (isset($head["class"])) {
241                                    $head["class"] = $head["class"] . " " . $classFromSnippetId;
242                                } else {
243                                    $head["class"] = $classFromSnippetId;
244                                }
245                                /**
246                                 * Critical is only treated by strap
247                                 */
248                                if (Site::isStrapTemplate()) {
249                                    $head[self::CRITICAL_ATTRIBUTE] = $tagsSnippet->getCritical();
250                                }
251                                $dokuWikiHeadsSrc[$snippetType][] = $head;
252                            }
253                        }
254                    }
255                    break;
256            }
257        }
258
259        /**
260         * Merge the content head node at the last position of the head ref node
261         */
262        foreach ($dokuWikiHeadsFormatContent as $headsNodeType => $headsData) {
263            foreach ($headsData as $heads) {
264                $dokuWikiHeadsSrc[$headsNodeType][] = $heads;
265            }
266        }
267        return $dokuWikiHeadsSrc;
268    }
269
270    /**
271     * Empty the snippets
272     * This is used to render the snippet only once
273     * The snippets renders first in the head
274     * and otherwise at the end of the document
275     * if the user are using another template or are in edit mode
276     */
277    public function close()
278    {
279        $this->snippetsByBarScope = array();
280        $this->snippetsByRequestScope = array();
281        $this->barsProcessed = array();
282    }
283
284
285    /**
286     * @param $snippetId
287     * @param array $tags - upsert a tag each time that this function is called
288     */
289    public function upsertHeadTagForRequest($snippetId, array $tags)
290    {
291        $id = PluginUtility::getPageId();
292        $snippet = &$this->snippetsByRequestScope[$id][Snippet::TAG_TYPE][$snippetId];
293        if (!isset($snippet)) {
294            $snippet = new Snippet($snippetId, Snippet::TAG_TYPE);
295        }
296        $snippet->setTags($tags);
297    }
298
299
300
301    /**
302     * A function to be able to add snippets from the snippets cache
303     * when a bar was served from the cache
304     * @param $bar
305     * @param $snippets
306     */
307    public function addSnippetsFromCacheForBar($bar, $snippets)
308    {
309
310        /**
311         * It may happens that this snippetsByBarScope is not empty
312         * when the snippet is added with the bad scope
313         *
314         * For instance, due to the {@link HistoricalBreadcrumbMenuItem},
315         * A protected link can be used in a slot but also added on a page level (ie
316         * that has a {@link PageProtection::addPageProtectionSnippet() page protection}
317         *
318         * Therefore we just merge.
319         */
320        if (!isset($this->snippetsByBarScope[$bar])) {
321
322            $this->snippetsByBarScope[$bar] = $snippets;
323
324        } else {
325
326            $this->snippetsByBarScope[$bar] = $this->mergeSnippetArray($this->snippetsByBarScope[$bar], $snippets);
327
328        }
329    }
330
331    public function getSnippetsForBar($bar)
332    {
333        if (isset($this->snippetsByBarScope[$bar])) {
334            return $this->snippetsByBarScope[$bar];
335        } else {
336            return null;
337        }
338
339    }
340
341    /**
342     * Add a javascript snippet at a request level
343     * (Meaning that it should never be cached)
344     * @param $snippetId
345     * @param $script
346     * @return Snippet
347     */
348    public function &upsertJavascriptSnippetForRequest($snippetId, $script = null)
349    {
350        $snippet = &$this->attachJavascriptSnippetForRequest($snippetId);
351        if ($script != null) {
352            $snippet->setContent($script);
353        }
354        return $snippet;
355
356    }
357
358    /**
359     * @param $snippetId
360     * @param null $script
361     * @return Snippet
362     */
363    public function &upsertCssSnippetForRequest($snippetId, $script = null)
364    {
365        $snippet = &$this->attachCssSnippetForRequest($snippetId, $script);
366        return $snippet;
367    }
368
369    /**
370     * @param $snippetId
371     * @param string $script - the css snippet to add, otherwise it takes the file
372     * @return Snippet a snippet scoped at the bar level
373     */
374    public function &attachCssSnippetForBar($snippetId, $script = null)
375    {
376        $snippet = $this->attachSnippetFromBar($snippetId, Snippet::TYPE_CSS);
377        if ($script != null) {
378            $snippet->setContent($script);
379        }
380        return $snippet;
381    }
382
383    /**
384     * @param $snippetId
385     * @param string $script -  the css if any, otherwise the css file will be taken
386     * @return Snippet a snippet scoped at the request scope
387     */
388    public function &attachCssSnippetForRequest($snippetId, $script = null)
389    {
390        $snippet = $this->attachSnippetFromRequest($snippetId, Snippet::TYPE_CSS);
391        if ($script != null) {
392            $snippet->setContent($script);
393        }
394        return $snippet;
395    }
396
397    /**
398     * @param $snippetId
399     * @param null $script
400     * @return Snippet a snippet scoped at the bar level
401     */
402    public function &attachJavascriptSnippetForBar($snippetId, $script = null)
403    {
404        $snippet = $this->attachSnippetFromBar($snippetId, Snippet::TYPE_JS);
405        if ($script != null) {
406            $snippet->setContent($script);
407        }
408        return $snippet;
409    }
410
411    /**
412     * @param $snippetId
413     * @return Snippet a snippet scoped at the request level
414     */
415    public function &attachJavascriptSnippetForRequest($snippetId)
416    {
417        return $this->attachSnippetFromRequest($snippetId, Snippet::TYPE_JS);
418    }
419
420    private function &attachSnippetFromBar($snippetId, $type)
421    {
422        global $ID;
423        $bar = $ID;
424        $snippetFromArray = &$this->snippetsByBarScope[$bar][$type][$snippetId];
425        if (!isset($snippetFromArray)) {
426            $snippet = new Snippet($snippetId, $type);
427            $snippetFromArray = $snippet;
428        }
429        return $snippetFromArray;
430    }
431
432    private function &attachSnippetFromRequest($snippetId, $type)
433    {
434        global $ID;
435        $bar = $ID;
436        $snippetFromArray = &$this->snippetsByRequestScope[$bar][$type][$snippetId];
437        if (!isset($snippetFromArray)) {
438            $snippet = new Snippet($snippetId, $type);
439            $snippetFromArray = $snippet;
440        }
441        return $snippetFromArray;
442    }
443
444    public function &attachTagsForBar($snippetId)
445    {
446        global $ID;
447        $bar = $ID;
448        $heads = &$this->snippetsByBarScope[$bar][Snippet::TAG_TYPE][$snippetId];
449        if (!isset($heads)) {
450            $heads = new Snippet($snippetId, Snippet::TAG_TYPE);
451        }
452        return $heads;
453    }
454
455    public function getCssSnippetContent($string)
456    {
457
458    }
459
460    private function mergeSnippetArray($left, $right)
461    {
462
463        $distinctSnippetIdByType = $left;
464        foreach (array_keys($right) as $snippetContentType) {
465            /**
466             * @var $snippetObject Snippet
467             */
468            foreach ($right[$snippetContentType] as $snippetObject) {
469                /**
470                 * Snippet is an object
471                 */
472                if (isset($distinctSnippetIdByType[$snippetContentType])) {
473                    if (!array_key_exists($snippetObject->getId(), $distinctSnippetIdByType[$snippetContentType])) {
474                        $distinctSnippetIdByType[$snippetContentType][$snippetObject->getId()] = $snippetObject;
475                    }
476                } else {
477                    $distinctSnippetIdByType[$snippetContentType][$snippetObject->getId()] = $snippetObject;
478                }
479            }
480        }
481
482        return $distinctSnippetIdByType;
483
484    }
485
486
487}
488
489
490