Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
79.79% covered (warning)
79.79%
75 / 94
72.22% covered (warning)
72.22%
13 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResourceContainer
79.79% covered (warning)
79.79%
75 / 94
72.22% covered (warning)
72.22%
13 / 18
78.08
0.00% covered (danger)
0.00%
0 / 1
 addCss
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
5
 addJs
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
7.10
 addJsEntry
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 addJsStringEntry
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 addJsArrayEntry
68.42% covered (warning)
68.42%
13 / 19
0.00% covered (danger)
0.00%
0 / 1
10.02
 removeEntry
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 insertEntry
76.47% covered (warning)
76.47%
13 / 17
0.00% covered (danger)
0.00%
0 / 1
11.30
 getCss
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getJs
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 parseSetting
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 setEncoding
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getEncoding
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFavicon
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFavicon
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setGenerator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getGenerator
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 dynamicallyParsed
40.00% covered (danger)
40.00%
2 / 5
0.00% covered (danger)
0.00%
0 / 1
2.86
 removeCSS
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 * VuFind Theme Public Resource Handler (for CSS, JS, etc.)
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  Theme
25 * @author   Demian Katz <demian.katz@villanova.edu>
26 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
27 * @link     https://vufind.org Main Site
28 */
29
30namespace VuFindTheme;
31
32use function count;
33use function in_array;
34use function is_array;
35
36/**
37 * VuFind Theme Public Resource Handler (for CSS, JS, etc.)
38 *
39 * @category VuFind
40 * @package  Theme
41 * @author   Demian Katz <demian.katz@villanova.edu>
42 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
43 * @link     https://vufind.org Main Site
44 */
45class ResourceContainer
46{
47    use \VuFind\Log\VarDumperTrait;
48
49    /**
50     * Less CSS files
51     *
52     * @var array
53     */
54    protected $less = [];
55
56    /**
57     * CSS files
58     *
59     * @var array
60     */
61    protected $css = [];
62
63    /**
64     * Javascript files
65     *
66     * @var array
67     */
68    protected $js = [];
69
70    /**
71     * Favicon
72     *
73     * @var string|array|null
74     */
75    protected $favicon = null;
76
77    /**
78     * Encoding type
79     *
80     * @var string
81     */
82    protected $encoding = 'UTF-8';
83
84    /**
85     * Generator value for <meta> tag
86     *
87     * @var string
88     */
89    protected $generator = '';
90
91    /**
92     * Add a CSS file.
93     *
94     * @param array|string $css CSS file (or array of CSS files) to add (possibly
95     * with extra settings from theme config appended to each filename string).
96     *
97     * @return void
98     */
99    public function addCss($css)
100    {
101        if (!is_array($css) && !is_a($css, 'Traversable')) {
102            $css = [$css];
103        }
104        foreach ($css as $current) {
105            if (!$this->dynamicallyParsed($current)) {
106                $this->css[] = $current;
107            }
108        }
109    }
110
111    /**
112     * Add a Javascript file.
113     *
114     * @param array|string $js Javascript file (or array of files) to add (possibly
115     * with extra settings from theme config appended to each filename string).
116     *
117     * @return void
118     */
119    public function addJs($js)
120    {
121        if ((!is_array($js) && !is_a($js, 'Traversable')) || isset($js['file'])) {
122            $this->addJsEntry($js);
123        } elseif (isset($js[0])) {
124            foreach ($js as $current) {
125                $this->addJsEntry($current);
126            }
127        } elseif ($js === []) {
128            return;
129        } else {
130            throw new \Exception('Invalid JS entry format: ' . $this->varDump($js));
131        }
132    }
133
134    /**
135     * Helper function for adding a Javascript file.
136     *
137     * @param string|array $jsEntry Entry to add, either as string with path
138     * or array with additional properties.
139     *
140     * @return void
141     */
142    protected function addJsEntry($jsEntry)
143    {
144        if (!is_array($jsEntry)) {
145            $this->addJsStringEntry($jsEntry);
146        } else {
147            $this->addJsArrayEntry($jsEntry);
148        }
149    }
150
151    /**
152     * Helper function for adding a Javascript file which is described as string.
153     *
154     * @param string $jsEntry Entry to add as string.
155     *
156     * @return void
157     */
158    protected function addJsStringEntry($jsEntry)
159    {
160        $parts = $this->parseSetting($jsEntry);
161        if (count($parts) == 1) {
162            $jsEntry = ['file' => $jsEntry];
163        } else {
164            $jsEntry = [
165                'file' => $parts[0],
166                'attributes' => ['conditional' => trim($parts[1])],
167            ];
168        }
169        $this->addJsArrayEntry($jsEntry);
170    }
171
172    /**
173     * Helper function for adding a Javascript file which is described as array.
174     *
175     * @param string $jsEntry Entry to add as string.
176     *
177     * @return void
178     */
179    protected function addJsArrayEntry($jsEntry)
180    {
181        if (!isset($jsEntry['position'])) {
182            $jsEntry['position'] = 'header';
183        }
184
185        if (isset($jsEntry['priority']) && isset($jsEntry['load_after'])) {
186            throw new \Exception(
187                'Using "priority" as well as "load_after" in the same entry '
188                . 'is not supported: "' . $jsEntry['file'] . '"'
189            );
190        }
191
192        // If we are disabling the dependency, remove it now.
193        if ($jsEntry['disabled'] ?? false) {
194            $this->removeEntry($jsEntry, $this->js);
195            return;
196        }
197
198        foreach ($this->js as $existingEntry) {
199            if ($existingEntry['file'] == $jsEntry['file']) {
200                // If we have the same settings as before, just skip this entry.
201                if ($existingEntry == $jsEntry) {
202                    return;
203                }
204
205                throw new \Exception(
206                    'Overriding an existing dependency is not supported: '
207                    . '"' . $jsEntry['file'] . '"'
208                );
209            }
210        }
211
212        $this->insertEntry($jsEntry, $this->js);
213    }
214
215    /**
216     * Helper function to remove an entry from an array based on filename.
217     *
218     * @param array $entry The entry to remove.
219     * @param array $array The array from which the entry shall be removed.
220     *
221     * @return void
222     */
223    protected function removeEntry($entry, &$array)
224    {
225        foreach (array_keys($array) as $i) {
226            if (($array[$i]['file'] ?? '') === ($entry['file'] ?? null)) {
227                unset($array[$i]);
228                return;
229            }
230        }
231    }
232
233    /**
234     * Helper function to insert an entry to an array,
235     * also considering priority and dependency, if existing.
236     *
237     * @param array $entry The entry to insert.
238     * @param array $array The array into which the entry shall be inserted.
239     *
240     * @return void
241     */
242    protected function insertEntry($entry, &$array)
243    {
244        if (isset($entry['priority']) || isset($entry['load_after'])) {
245            foreach (array_keys($array) as $i) {
246                if (isset($entry['priority'])) {
247                    $currentPriority = $array[$i]['priority'] ?? null;
248                    if (
249                        !isset($currentPriority)
250                        || $currentPriority > $entry['priority']
251                    ) {
252                        array_splice($array, $i, 0, [$entry]);
253                        return;
254                    }
255                } elseif (isset($entry['load_after'])) {
256                    if ($entry['load_after'] === $array[$i]['file']) {
257                        array_splice($array, $i + 1, 0, [$entry]);
258                        return;
259                    }
260                }
261            }
262
263            if (isset($entry['load_after'])) {
264                throw new \Exception(
265                    'Dependency not found: ' . $entry['load_after']
266                );
267            }
268        }
269
270        // Insert at end if either no priority/dependency is given
271        // or no other element has been found
272        $array[] = $entry;
273    }
274
275    /**
276     * Get CSS files.
277     *
278     * @return array
279     */
280    public function getCss()
281    {
282        return array_unique($this->css);
283    }
284
285    /**
286     * Get Javascript files.
287     *
288     * @param string $position Position where the files should be inserted
289     * (allowed values are 'header' or 'footer').
290     *
291     * @return array
292     */
293    public function getJs(string $position = null)
294    {
295        if (!isset($position)) {
296            return $this->js;
297        } else {
298            return array_filter(
299                $this->js,
300                function ($jsFile) use ($position) {
301                    return $jsFile['position'] == $position;
302                }
303            );
304        }
305    }
306
307    /**
308     * Given a colon-delimited configuration string, break it apart, making sure
309     * that URLs in the first position are not inappropriately split.
310     *
311     * @param string $current Setting to parse
312     *
313     * @return array
314     */
315    public function parseSetting($current)
316    {
317        // TODO: replace this method with a deprecation warning when all configs
318        // have been converted to arrays
319        $parts = explode(':', $current);
320        // Special case: don't explode URLs:
321        if (
322            ($parts[0] === 'http' || $parts[0] === 'https')
323            && str_starts_with($parts[1], '//')
324        ) {
325            $protocol = array_shift($parts);
326            $parts[0] = $protocol . ':' . $parts[0];
327        }
328        return $parts;
329    }
330
331    /**
332     * Set the encoding.
333     *
334     * @param string $e New encoding
335     *
336     * @return void
337     */
338    public function setEncoding($e)
339    {
340        $this->encoding = $e;
341    }
342
343    /**
344     * Get the encoding.
345     *
346     * @return void
347     */
348    public function getEncoding()
349    {
350        return $this->encoding;
351    }
352
353    /**
354     * Set the favicon.
355     *
356     * @param string|array $favicon New favicon path.
357     *
358     * @return void
359     */
360    public function setFavicon($favicon)
361    {
362        $this->favicon = $favicon;
363    }
364
365    /**
366     * Get the favicon (null for none).
367     *
368     * @return string|array|null
369     */
370    public function getFavicon()
371    {
372        return $this->favicon;
373    }
374
375    /**
376     * Set the generator.
377     *
378     * @param string $generator New generator.
379     *
380     * @return void
381     */
382    public function setGenerator($generator)
383    {
384        $this->generator = $generator;
385    }
386
387    /**
388     * Get the generator.
389     *
390     * @return string
391     */
392    public function getGenerator()
393    {
394        return $this->generator;
395    }
396
397    /**
398     * Check if a CSS file is being dynamically compiled in LESS
399     *
400     * @param string $file Filename to check
401     *
402     * @return bool
403     */
404    protected function dynamicallyParsed($file)
405    {
406        if (empty($this->less)) {
407            return false;
408        }
409        [$fileName, ] = explode('.', $file);
410        $lessFile = $fileName . '.less';
411        return in_array($lessFile, $this->less, true);
412    }
413
414    /**
415     * Remove a CSS file if it matches another file's name
416     *
417     * @param string $file Filename to remove
418     *
419     * @return void
420     */
421    protected function removeCSS($file)
422    {
423        [$name, ] = explode('.', $file);
424        $name .= '.css';
425        $index = array_search($name, $this->css);
426        if (false !== $index) {
427            unset($this->css[$index]);
428        }
429    }
430}