Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.95% covered (success)
98.95%
94 / 95
91.67% covered (success)
91.67%
11 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ThemeInfo
98.95% covered (success)
98.95%
94 / 95
91.67% covered (success)
91.67%
11 / 12
38
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 setCache
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBaseDir
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMixinConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getThemeConfig
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setTheme
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getTheme
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadThemeConfig
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getThemeInfo
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getMergedConfig
100.00% covered (success)
100.00%
24 / 24
100.00% covered (success)
100.00%
1 / 1
10
 findContainingTheme
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
8
 findInThemes
96.43% covered (success)
96.43%
27 / 28
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3/**
4 * Class to represent currently-selected theme and related information.
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010-2023.
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 Laminas\Cache\Storage\StorageInterface;
33use Webmozart\Glob\Glob;
34
35use function is_array;
36use function strlen;
37
38/**
39 * Class to represent currently-selected theme and related information.
40 *
41 * @category VuFind
42 * @package  Theme
43 * @author   Demian Katz <demian.katz@villanova.edu>
44 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
45 * @link     https://vufind.org Main Site
46 */
47class ThemeInfo
48{
49    use \VuFind\Feature\MergeRecursiveTrait;
50
51    /**
52     * Base directory for theme files
53     *
54     * @var string
55     */
56    protected $baseDir;
57
58    /**
59     * Current selected theme
60     *
61     * @var string
62     */
63    protected $currentTheme;
64
65    /**
66     * A safe theme (guaranteed to exist) that can be loaded if an invalid
67     * configuration is passed in
68     *
69     * @var string
70     */
71    protected $safeTheme;
72
73    /**
74     * Theme configuration cache
75     *
76     * @var array
77     */
78    protected $allThemeInfo = null;
79
80    /**
81     * Cache for merged configs
82     *
83     * @var StorageInterface
84     */
85    protected $cache = null;
86
87    // Constant for use with findContainingTheme:
88    public const RETURN_ALL_DETAILS = 'all';
89
90    /**
91     * Constructor
92     *
93     * @param string $baseDir   Base directory for theme files.
94     * @param string $safeTheme Theme that should be guaranteed to exist.
95     */
96    public function __construct($baseDir, $safeTheme)
97    {
98        $this->baseDir = $baseDir;
99        $this->currentTheme = $this->safeTheme = $safeTheme;
100    }
101
102    /**
103     * Provide cache and activate info caching
104     *
105     * @param StorageInterface $cache cache object
106     *
107     * @return void
108     */
109    public function setCache(StorageInterface $cache)
110    {
111        $this->cache = $cache;
112    }
113
114    /**
115     * Get the base directory for themes.
116     *
117     * @return string
118     */
119    public function getBaseDir()
120    {
121        return $this->baseDir;
122    }
123
124    /**
125     * Get the configuration file for the specified mixin.
126     *
127     * @param string $mixin Mixin name
128     *
129     * @return string
130     */
131    protected function getMixinConfig($mixin)
132    {
133        return $this->baseDir . "/$mixin/mixin.config.php";
134    }
135
136    /**
137     * Get the configuration file for the specified theme.
138     *
139     * @param string $theme Theme name
140     *
141     * @return string
142     */
143    protected function getThemeConfig($theme)
144    {
145        return $this->baseDir . "/$theme/theme.config.php";
146    }
147
148    /**
149     * Set the current theme.
150     *
151     * @param string $theme Theme to set.
152     *
153     * @return void
154     * @throws \Exception
155     */
156    public function setTheme($theme)
157    {
158        // If the configured theme setting is illegal, throw an exception without
159        // making any changes.
160        if (!file_exists($this->getThemeConfig($theme))) {
161            throw new \Exception('Cannot load theme: ' . $theme);
162        }
163        if ($theme != $this->currentTheme) {
164            // Clear any cached theme information when we change themes:
165            $this->allThemeInfo = null;
166            $this->currentTheme = $theme;
167        }
168    }
169
170    /**
171     * Get the current theme.
172     *
173     * @return string
174     */
175    public function getTheme()
176    {
177        return $this->currentTheme;
178    }
179
180    /**
181     * Load configuration for the specified theme (and its mixins, if any) into the
182     * allThemeInfo property.
183     *
184     * @param string $theme Name of theme to load
185     *
186     * @return void
187     */
188    protected function loadThemeConfig($theme)
189    {
190        // Load theme configuration...
191        $this->allThemeInfo[$theme] = include $this->getThemeConfig($theme);
192        // ..and if there are mixins, load those too!
193        if (isset($this->allThemeInfo[$theme]['mixins'])) {
194            foreach ($this->allThemeInfo[$theme]['mixins'] as $mix) {
195                $this->allThemeInfo[$mix] = include $this->getMixinConfig($mix);
196            }
197        }
198    }
199
200    /**
201     * Get all the configuration details related to the current theme.
202     *
203     * @return array
204     */
205    public function getThemeInfo()
206    {
207        // Fill in the theme info cache if it is not already populated:
208        if ($this->allThemeInfo === null) {
209            // Build an array of theme information by inheriting up the theme tree:
210            $this->allThemeInfo = [];
211            $currentTheme = $this->getTheme();
212            do {
213                $this->loadThemeConfig($currentTheme);
214                $currentTheme = $this->allThemeInfo[$currentTheme]['extends'];
215            } while ($currentTheme);
216        }
217
218        return $this->allThemeInfo;
219    }
220
221    /**
222     * Get a configuration element, merged to reflect theme inheritance.
223     *
224     * @param string $key Configuration key to retrieve (or empty string to
225     * retrieve full configuration)
226     *
227     * @return array|string
228     */
229    public function getMergedConfig(string $key = '')
230    {
231        $currentTheme = $this->getTheme();
232        $allThemeInfo = $this->getThemeInfo();
233
234        $cacheKey = $currentTheme . '_' . $key;
235
236        if ($this->cache !== null) {
237            $cached = $this->cache->getItem($cacheKey);
238
239            if ($cached !== null) {
240                return $cached;
241            }
242        }
243
244        $merged = [];
245
246        while (!empty($currentTheme)) {
247            $currentThemeSet = array_merge(
248                (array)$currentTheme,
249                $allThemeInfo[$currentTheme]['mixins'] ?? [],
250            );
251
252            // from child to parent
253            foreach ($currentThemeSet as $theme) {
254                if (
255                    isset($allThemeInfo[$theme])
256                    && (empty($key) || isset($allThemeInfo[$theme][$key]))
257                ) {
258                    $current = empty($key)
259                        ? $allThemeInfo[$theme]
260                        : $allThemeInfo[$theme][$key];
261
262                    $merged = $this->mergeRecursive($current, $merged);
263                }
264            }
265
266            $currentTheme = $allThemeInfo[$currentTheme]['extends'];
267        }
268
269        if ($this->cache !== null) {
270            $this->cache->setItem($cacheKey, $merged);
271        }
272
273        return $merged;
274    }
275
276    /**
277     * Search the themes for a particular file. If it exists, return the
278     * first matching theme name; otherwise, return null.
279     *
280     * @param string|array $relativePath Relative path (or array of paths) to
281     * search within themes
282     * @param string|bool  $returnType   If boolean true, return full file path;
283     * if boolean false, return containing theme name; if self::RETURN_ALL_DETAILS,
284     * return an array containing both values (keyed with 'path', 'theme' and
285     * 'relativePath').
286     *
287     * @return string|array|null
288     */
289    public function findContainingTheme($relativePath, $returnType = false)
290    {
291        $basePath = $this->getBaseDir();
292        $allPaths = is_array($relativePath)
293            ? $relativePath : [$relativePath];
294
295        $currentTheme = $this->getTheme();
296        $allThemeInfo = $this->getThemeInfo();
297
298        while (!empty($currentTheme)) {
299            $currentThemeSet = array_merge(
300                (array)$currentTheme,
301                $allThemeInfo[$currentTheme]['mixins'] ?? []
302            );
303            foreach ($currentThemeSet as $theme) {
304                foreach ($allPaths as $currentPath) {
305                    $path = "$basePath/$theme/$currentPath";
306                    if (file_exists($path)) {
307                        // Depending on return type, send back the requested data:
308                        if (self::RETURN_ALL_DETAILS === $returnType) {
309                            $relativePath = $currentPath;
310                            return compact('path', 'theme', 'relativePath');
311                        }
312                        return $returnType ? $path : $theme;
313                    }
314                }
315            }
316            $currentTheme = $allThemeInfo[$currentTheme]['extends'];
317        }
318
319        return null;
320    }
321
322    /**
323     * Search the themes for a file pattern. Returns all matching files.
324     *
325     * Note that for any matching file the last match in the theme hierarchy is
326     * returned.
327     *
328     * @param string|array $relativePathPattern Relative path pattern (or array of
329     * patterns) to search within themes
330     *
331     * @return array
332     */
333    public function findInThemes($relativePathPattern)
334    {
335        $basePath = $this->getBaseDir();
336        $allPaths = (array)$relativePathPattern;
337
338        $currentTheme = $this->getTheme();
339        $allThemeInfo = $this->getThemeInfo();
340
341        $allThemes = [];
342        while (!empty($currentTheme)) {
343            $allThemes = array_merge(
344                $allThemes,
345                (array)$currentTheme,
346                $allThemeInfo[$currentTheme]['mixins'] ?? []
347            );
348            $currentTheme = $allThemeInfo[$currentTheme]['extends'];
349        }
350
351        // Start from the base theme so that we can find any overrides properly
352        $allThemes = array_reverse($allThemes);
353        $results = [];
354        foreach ($allThemes as $theme) {
355            $themePath = "$basePath/$theme/";
356            foreach ($allPaths as $currentPath) {
357                $path = $themePath . $currentPath;
358                foreach (Glob::glob($path) as $file) {
359                    if (filetype($file) === 'dir') {
360                        continue;
361                    }
362                    $relativeFile = substr($file, strlen($themePath));
363                    $results[$relativeFile] = [
364                        'theme' => $theme,
365                        'file' => $file,
366                        'relativeFile' => $relativeFile,
367                    ];
368                }
369            }
370        }
371        return array_values($results);
372    }
373}