Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
83.06% |
103 / 124 |
|
70.00% |
14 / 20 |
CRAP | |
0.00% |
0 / 1 |
ResourceContainer | |
83.06% |
103 / 124 |
|
70.00% |
14 / 20 |
90.46 | |
0.00% |
0 / 1 |
addCss | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
7.10 | |||
addJs | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
7.10 | |||
addCssEntry | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
addCssStringEntry | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
addCssArrayEntry | |
76.47% |
13 / 17 |
|
0.00% |
0 / 1 |
7.64 | |||
addJsEntry | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
addJsStringEntry | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
addJsArrayEntry | |
68.42% |
13 / 19 |
|
0.00% |
0 / 1 |
10.02 | |||
removeEntry | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
3 | |||
insertEntry | |
76.47% |
13 / 17 |
|
0.00% |
0 / 1 |
11.30 | |||
getCss | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getJs | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
parseSetting | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
setEncoding | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getEncoding | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setFavicon | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFavicon | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setGenerator | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getGenerator | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
removeCSS | |
0.00% |
0 / 5 |
|
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 | |
30 | namespace VuFindTheme; |
31 | |
32 | use function count; |
33 | use function is_array; |
34 | |
35 | /** |
36 | * VuFind Theme Public Resource Handler (for CSS, JS, etc.) |
37 | * |
38 | * @category VuFind |
39 | * @package Theme |
40 | * @author Demian Katz <demian.katz@villanova.edu> |
41 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
42 | * @link https://vufind.org Main Site |
43 | */ |
44 | class ResourceContainer |
45 | { |
46 | use \VuFind\Log\VarDumperTrait; |
47 | |
48 | /** |
49 | * CSS files |
50 | * |
51 | * @var array |
52 | */ |
53 | protected $css = []; |
54 | |
55 | /** |
56 | * Javascript files |
57 | * |
58 | * @var array |
59 | */ |
60 | protected $js = []; |
61 | |
62 | /** |
63 | * Favicon |
64 | * |
65 | * @var string|array|null |
66 | */ |
67 | protected $favicon = null; |
68 | |
69 | /** |
70 | * Encoding type |
71 | * |
72 | * @var string |
73 | */ |
74 | protected $encoding = 'UTF-8'; |
75 | |
76 | /** |
77 | * Generator value for <meta> tag |
78 | * |
79 | * @var string |
80 | */ |
81 | protected $generator = ''; |
82 | |
83 | /** |
84 | * Add a CSS file. |
85 | * |
86 | * @param array|string $css CSS file (or array of CSS files) to add (possibly |
87 | * with extra settings from theme config appended to each filename string). |
88 | * |
89 | * @return void |
90 | */ |
91 | public function addCss($css) |
92 | { |
93 | if ((!is_array($css) && !is_a($css, 'Traversable')) || isset($css['file'])) { |
94 | $this->addCssEntry($css); |
95 | } elseif (isset($css[0])) { |
96 | foreach ($css as $current) { |
97 | $this->addCssEntry($current); |
98 | } |
99 | } elseif ($css === []) { |
100 | return; |
101 | } else { |
102 | throw new \Exception('Invalid CSS entry format: ' . $this->varDump($css)); |
103 | } |
104 | } |
105 | |
106 | /** |
107 | * Add a Javascript file. |
108 | * |
109 | * @param array|string $js Javascript file (or array of files) to add (possibly |
110 | * with extra settings from theme config appended to each filename string). |
111 | * |
112 | * @return void |
113 | */ |
114 | public function addJs($js) |
115 | { |
116 | if ((!is_array($js) && !is_a($js, 'Traversable')) || isset($js['file'])) { |
117 | $this->addJsEntry($js); |
118 | } elseif (isset($js[0])) { |
119 | foreach ($js as $current) { |
120 | $this->addJsEntry($current); |
121 | } |
122 | } elseif ($js === []) { |
123 | return; |
124 | } else { |
125 | throw new \Exception('Invalid JS entry format: ' . $this->varDump($js)); |
126 | } |
127 | } |
128 | |
129 | /** |
130 | * Helper function for adding a CSS file. |
131 | * |
132 | * @param string|array $cssEntry Entry to add, either as string with path |
133 | * or array with additional properties. |
134 | * |
135 | * @return void |
136 | */ |
137 | protected function addCssEntry($cssEntry) |
138 | { |
139 | if (!is_array($cssEntry)) { |
140 | $this->addCssStringEntry($cssEntry); |
141 | } else { |
142 | $this->addCssArrayEntry($cssEntry); |
143 | } |
144 | } |
145 | |
146 | /** |
147 | * Helper function for adding a CSS file which is described as string. |
148 | * |
149 | * @param string $cssEntry Entry to add as string. |
150 | * |
151 | * @return void |
152 | */ |
153 | protected function addCssStringEntry($cssEntry) |
154 | { |
155 | $parts = $this->parseSetting($cssEntry); |
156 | // Special case for media with parentheses |
157 | // ie. (min-width: 768px) |
158 | if (count($parts) > 1 && str_starts_with($parts[1], '(')) { |
159 | $parts[1] .= ':' . $parts[2]; |
160 | array_splice($parts, 2, 1); |
161 | } |
162 | $cssArray = [ |
163 | 'file' => trim($parts[0]), |
164 | ]; |
165 | if (isset($parts[1])) { |
166 | $cssArray['media'] = trim($parts[1]); |
167 | } |
168 | if (isset($parts[2])) { |
169 | $cssArray['conditional'] = trim($parts[2]); |
170 | } |
171 | $this->addCssArrayEntry($cssArray); |
172 | } |
173 | |
174 | /** |
175 | * Helper function for adding a CSS file which is described as array. |
176 | * |
177 | * @param array $cssEntry Entry to add as array. |
178 | * |
179 | * @return void |
180 | */ |
181 | protected function addCssArrayEntry($cssEntry) |
182 | { |
183 | if (isset($cssEntry['priority']) && isset($cssEntry['load_after'])) { |
184 | throw new \Exception( |
185 | 'Using "priority" as well as "load_after" in the same entry ' |
186 | . 'is not supported: "' . $cssEntry['file'] . '"' |
187 | ); |
188 | } |
189 | |
190 | // If we are disabling the dependency, remove it now. |
191 | if ($cssEntry['disabled'] ?? false) { |
192 | $this->removeEntry($cssEntry, $this->css); |
193 | return; |
194 | } |
195 | |
196 | foreach ($this->css as $existingEntry) { |
197 | if ($existingEntry['file'] == $cssEntry['file']) { |
198 | // If we have the same settings as before, just skip this entry. |
199 | if ($existingEntry == $cssEntry) { |
200 | return; |
201 | } |
202 | |
203 | throw new \Exception( |
204 | 'Overriding an existing dependency is not supported: ' |
205 | . '"' . $cssEntry['file'] . '"' |
206 | ); |
207 | } |
208 | } |
209 | |
210 | $this->insertEntry($cssEntry, $this->css); |
211 | } |
212 | |
213 | /** |
214 | * Helper function for adding a Javascript file. |
215 | * |
216 | * @param string|array $jsEntry Entry to add, either as string with path |
217 | * or array with additional properties. |
218 | * |
219 | * @return void |
220 | */ |
221 | protected function addJsEntry($jsEntry) |
222 | { |
223 | if (!is_array($jsEntry)) { |
224 | $this->addJsStringEntry($jsEntry); |
225 | } else { |
226 | $this->addJsArrayEntry($jsEntry); |
227 | } |
228 | } |
229 | |
230 | /** |
231 | * Helper function for adding a Javascript file which is described as string. |
232 | * |
233 | * @param string $jsEntry Entry to add as string. |
234 | * |
235 | * @return void |
236 | */ |
237 | protected function addJsStringEntry($jsEntry) |
238 | { |
239 | $parts = $this->parseSetting($jsEntry); |
240 | if (count($parts) == 1) { |
241 | $jsEntry = ['file' => $jsEntry]; |
242 | } else { |
243 | $jsEntry = [ |
244 | 'file' => $parts[0], |
245 | 'attributes' => ['conditional' => trim($parts[1])], |
246 | ]; |
247 | } |
248 | $this->addJsArrayEntry($jsEntry); |
249 | } |
250 | |
251 | /** |
252 | * Helper function for adding a Javascript file which is described as array. |
253 | * |
254 | * @param array $jsEntry Entry to add as array. |
255 | * |
256 | * @return void |
257 | */ |
258 | protected function addJsArrayEntry($jsEntry) |
259 | { |
260 | if (!isset($jsEntry['position'])) { |
261 | $jsEntry['position'] = 'header'; |
262 | } |
263 | |
264 | if (isset($jsEntry['priority']) && isset($jsEntry['load_after'])) { |
265 | throw new \Exception( |
266 | 'Using "priority" as well as "load_after" in the same entry ' |
267 | . 'is not supported: "' . $jsEntry['file'] . '"' |
268 | ); |
269 | } |
270 | |
271 | // If we are disabling the dependency, remove it now. |
272 | if ($jsEntry['disabled'] ?? false) { |
273 | $this->removeEntry($jsEntry, $this->js); |
274 | return; |
275 | } |
276 | |
277 | foreach ($this->js as $existingEntry) { |
278 | if ($existingEntry['file'] == $jsEntry['file']) { |
279 | // If we have the same settings as before, just skip this entry. |
280 | if ($existingEntry == $jsEntry) { |
281 | return; |
282 | } |
283 | |
284 | throw new \Exception( |
285 | 'Overriding an existing dependency is not supported: ' |
286 | . '"' . $jsEntry['file'] . '"' |
287 | ); |
288 | } |
289 | } |
290 | |
291 | $this->insertEntry($jsEntry, $this->js); |
292 | } |
293 | |
294 | /** |
295 | * Helper function to remove an entry from an array based on filename. |
296 | * |
297 | * @param array $entry The entry to remove. |
298 | * @param array $array The array from which the entry shall be removed. |
299 | * |
300 | * @return void |
301 | */ |
302 | protected function removeEntry($entry, &$array) |
303 | { |
304 | foreach (array_keys($array) as $i) { |
305 | if (($array[$i]['file'] ?? '') === ($entry['file'] ?? null)) { |
306 | unset($array[$i]); |
307 | return; |
308 | } |
309 | } |
310 | } |
311 | |
312 | /** |
313 | * Helper function to insert an entry to an array, |
314 | * also considering priority and dependency, if existing. |
315 | * |
316 | * @param array $entry The entry to insert. |
317 | * @param array $array The array into which the entry shall be inserted. |
318 | * |
319 | * @return void |
320 | */ |
321 | protected function insertEntry($entry, &$array) |
322 | { |
323 | if (isset($entry['priority']) || isset($entry['load_after'])) { |
324 | foreach (array_keys($array) as $i) { |
325 | if (isset($entry['priority'])) { |
326 | $currentPriority = $array[$i]['priority'] ?? null; |
327 | if ( |
328 | !isset($currentPriority) |
329 | || $currentPriority > $entry['priority'] |
330 | ) { |
331 | array_splice($array, $i, 0, [$entry]); |
332 | return; |
333 | } |
334 | } elseif (isset($entry['load_after'])) { |
335 | if ($entry['load_after'] === $array[$i]['file']) { |
336 | array_splice($array, $i + 1, 0, [$entry]); |
337 | return; |
338 | } |
339 | } |
340 | } |
341 | |
342 | if (isset($entry['load_after'])) { |
343 | throw new \Exception( |
344 | 'Dependency not found: ' . $entry['load_after'] |
345 | ); |
346 | } |
347 | } |
348 | |
349 | // Insert at end if either no priority/dependency is given |
350 | // or no other element has been found |
351 | $array[] = $entry; |
352 | } |
353 | |
354 | /** |
355 | * Get CSS files. |
356 | * |
357 | * @return array |
358 | */ |
359 | public function getCss() |
360 | { |
361 | return $this->css; |
362 | } |
363 | |
364 | /** |
365 | * Get Javascript files. |
366 | * |
367 | * @param string $position Position where the files should be inserted |
368 | * (allowed values are 'header' or 'footer'). |
369 | * |
370 | * @return array |
371 | */ |
372 | public function getJs(string $position = null) |
373 | { |
374 | if (!isset($position)) { |
375 | return $this->js; |
376 | } else { |
377 | return array_filter( |
378 | $this->js, |
379 | function ($jsFile) use ($position) { |
380 | return $jsFile['position'] == $position; |
381 | } |
382 | ); |
383 | } |
384 | } |
385 | |
386 | /** |
387 | * Given a colon-delimited configuration string, break it apart, making sure |
388 | * that URLs in the first position are not inappropriately split. |
389 | * |
390 | * @param string $current Setting to parse |
391 | * |
392 | * @return array |
393 | */ |
394 | public function parseSetting($current) |
395 | { |
396 | // TODO: replace this method with a deprecation warning when all configs |
397 | // have been converted to arrays |
398 | $parts = explode(':', $current); |
399 | // Special case: don't explode URLs: |
400 | if ( |
401 | ($parts[0] === 'http' || $parts[0] === 'https') |
402 | && str_starts_with($parts[1], '//') |
403 | ) { |
404 | $protocol = array_shift($parts); |
405 | $parts[0] = $protocol . ':' . $parts[0]; |
406 | } |
407 | return $parts; |
408 | } |
409 | |
410 | /** |
411 | * Set the encoding. |
412 | * |
413 | * @param string $e New encoding |
414 | * |
415 | * @return void |
416 | */ |
417 | public function setEncoding($e) |
418 | { |
419 | $this->encoding = $e; |
420 | } |
421 | |
422 | /** |
423 | * Get the encoding. |
424 | * |
425 | * @return void |
426 | */ |
427 | public function getEncoding() |
428 | { |
429 | return $this->encoding; |
430 | } |
431 | |
432 | /** |
433 | * Set the favicon. |
434 | * |
435 | * @param string|array $favicon New favicon path. |
436 | * |
437 | * @return void |
438 | */ |
439 | public function setFavicon($favicon) |
440 | { |
441 | $this->favicon = $favicon; |
442 | } |
443 | |
444 | /** |
445 | * Get the favicon (null for none). |
446 | * |
447 | * @return string|array|null |
448 | */ |
449 | public function getFavicon() |
450 | { |
451 | return $this->favicon; |
452 | } |
453 | |
454 | /** |
455 | * Set the generator. |
456 | * |
457 | * @param string $generator New generator. |
458 | * |
459 | * @return void |
460 | */ |
461 | public function setGenerator($generator) |
462 | { |
463 | $this->generator = $generator; |
464 | } |
465 | |
466 | /** |
467 | * Get the generator. |
468 | * |
469 | * @return string |
470 | */ |
471 | public function getGenerator() |
472 | { |
473 | return $this->generator; |
474 | } |
475 | |
476 | /** |
477 | * Remove a CSS file if it matches another file's name |
478 | * |
479 | * @param string $file Filename to remove |
480 | * |
481 | * @return void |
482 | */ |
483 | protected function removeCSS($file) |
484 | { |
485 | [$name, ] = explode('.', $file); |
486 | $name .= '.css'; |
487 | $index = array_search($name, $this->css); |
488 | if (false !== $index) { |
489 | unset($this->css[$index]); |
490 | } |
491 | } |
492 | } |