Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 134 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
Initializer | |
0.00% |
0 / 134 |
|
0.00% |
0 / 8 |
2862 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
init | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
30 | |||
pickTheme | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
420 | |||
sendThemeOptionsToView | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getThemeOptions | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
42 | |||
setUpThemeViewHelpers | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
setUpThemes | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
110 | |||
updateTranslator | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | |
3 | /** |
4 | * VuFind Theme Initializer |
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 Laminas\Config\Config; |
33 | use Laminas\Mvc\MvcEvent; |
34 | use Laminas\Stdlib\RequestInterface as Request; |
35 | use Laminas\View\Resolver\TemplatePathStack; |
36 | use Psr\Container\ContainerInterface; |
37 | |
38 | /** |
39 | * VuFind Theme Initializer |
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 | */ |
47 | class Initializer |
48 | { |
49 | /** |
50 | * Theme configuration object |
51 | * |
52 | * @var Config |
53 | */ |
54 | protected $config; |
55 | |
56 | /** |
57 | * Laminas MVC Event |
58 | * |
59 | * @var MvcEvent |
60 | */ |
61 | protected $event; |
62 | |
63 | /** |
64 | * Top-level service container |
65 | * |
66 | * @var \Psr\Container\ContainerInterface |
67 | */ |
68 | protected $serviceManager; |
69 | |
70 | /** |
71 | * Theme tools object |
72 | * |
73 | * @var \VuFindTheme\ThemeInfo |
74 | */ |
75 | protected $tools; |
76 | |
77 | /** |
78 | * Mobile interface detector |
79 | * |
80 | * @var \VuFindTheme\Mobile |
81 | */ |
82 | protected $mobile; |
83 | |
84 | /** |
85 | * Cookie manager |
86 | * |
87 | * @var \VuFind\Cookie\CookieManager |
88 | */ |
89 | protected $cookieManager; |
90 | |
91 | /** |
92 | * A static flag used to determine if the theme has been initialized |
93 | * |
94 | * @var bool |
95 | */ |
96 | protected static $themeInitialized = false; |
97 | |
98 | /** |
99 | * Constructor |
100 | * |
101 | * @param Config $config Configuration object |
102 | * containing these keys: |
103 | * <ul> |
104 | * <li>theme - the name of the default theme for non-mobile devices</li> |
105 | * <li>mobile_theme - the name of the default theme for mobile devices |
106 | * (omit to disable mobile support)</li> |
107 | * <li>alternate_themes - a comma-separated list of alternate themes that |
108 | * can be accessed via the ui GET parameter; each entry is a colon-separated |
109 | * parameter-value:theme-name pair.</li> |
110 | * <li>selectable_themes - a comma-separated list of themes that may be |
111 | * selected through the user interface; each entry is a colon-separated |
112 | * name:description pair, where name may be 'standard,' 'mobile,' or one of |
113 | * the parameter-values from the alternate_themes array.</li> |
114 | * <li>generator - a Generator value to display in the HTML header |
115 | * (optional)</li> |
116 | * </ul> |
117 | * @param MvcEvent|ContainerInterface $eventOrContainer Laminas MVC Event object |
118 | * OR service container object |
119 | */ |
120 | public function __construct(Config $config, $eventOrContainer) |
121 | { |
122 | // Store parameters: |
123 | $this->config = $config; |
124 | |
125 | if ($eventOrContainer instanceof MvcEvent) { |
126 | $this->event = $eventOrContainer; |
127 | $this->serviceManager = $this->event->getApplication() |
128 | ->getServiceManager(); |
129 | } elseif ($eventOrContainer instanceof ContainerInterface) { |
130 | $this->event = null; |
131 | $this->serviceManager = $eventOrContainer; |
132 | } else { |
133 | throw new \Exception( |
134 | 'Illegal type for $eventOrContainer: ' . $eventOrContainer::class |
135 | ); |
136 | } |
137 | |
138 | // Get the cookie manager from the service manager: |
139 | $this->cookieManager = $this->serviceManager |
140 | ->get(\VuFind\Cookie\CookieManager::class); |
141 | |
142 | // Get base directory from tools object: |
143 | $this->tools = $this->serviceManager->get(\VuFindTheme\ThemeInfo::class); |
144 | |
145 | // Set up mobile device detector: |
146 | $this->mobile = $this->serviceManager->get(\VuFindTheme\Mobile::class); |
147 | $this->mobile->enable(isset($this->config->mobile_theme)); |
148 | } |
149 | |
150 | /** |
151 | * Initialize the theme. This needs to be triggered as part of the dispatch |
152 | * event. |
153 | * |
154 | * @throws \Exception |
155 | * @return void |
156 | */ |
157 | public function init() |
158 | { |
159 | // Make sure to initialize the theme just once |
160 | if (self::$themeInitialized) { |
161 | return; |
162 | } |
163 | self::$themeInitialized = true; |
164 | |
165 | // Determine the current theme: |
166 | $currentTheme = $this->pickTheme( |
167 | isset($this->event) ? $this->event->getRequest() : null |
168 | ); |
169 | |
170 | // Determine theme options: |
171 | $this->sendThemeOptionsToView($currentTheme); |
172 | |
173 | // Make sure the current theme is set correctly in the tools object: |
174 | $error = null; |
175 | try { |
176 | $this->tools->setTheme($currentTheme); |
177 | } catch (\Exception $error) { |
178 | // If an illegal value is passed in, the setter may throw an exception. |
179 | // We should ignore it for now and throw it after we have set up the |
180 | // theme (the setter will use a safe value instead of the illegal one). |
181 | } |
182 | |
183 | // Using the settings we initialized above, actually configure the themes; we |
184 | // need to do this even if there is an error, since we need a theme in order |
185 | // to display an error message! |
186 | $this->setUpThemes(array_reverse($this->tools->getThemeInfo())); |
187 | |
188 | // If we encountered an error loading theme settings, fail now. |
189 | if (isset($error)) { |
190 | throw new \Exception($error->getMessage()); |
191 | } |
192 | } |
193 | |
194 | /** |
195 | * Support method for init() -- figure out which theme option is active. |
196 | * |
197 | * @param Request $request Request object (for obtaining user parameters); |
198 | * set to null if no request context is available. |
199 | * |
200 | * @return string |
201 | */ |
202 | protected function pickTheme(?Request $request) |
203 | { |
204 | // The admin theme should always be picked if |
205 | // - the Admin module is enabled AND |
206 | // - an admin theme is set AND |
207 | // - an admin route is requested (route configuration has an |
208 | // 'admin_route' => true default parameter). |
209 | if ( |
210 | isset($this->event) |
211 | && ($routeMatch = $this->event->getRouteMatch()) |
212 | && $routeMatch->getParam('admin_route') |
213 | && ($this->config->admin_enabled ?? false) |
214 | && ($adminTheme = ($this->config->admin_theme ?? false)) |
215 | ) { |
216 | return $adminTheme; |
217 | } |
218 | |
219 | // Load standard configuration options: |
220 | $standardTheme = $this->config->theme; |
221 | if (PHP_SAPI == 'cli') { |
222 | return $standardTheme; |
223 | } |
224 | $mobileTheme = $this->mobile->enabled() |
225 | ? $this->config->mobile_theme : false; |
226 | |
227 | // Find out if the user has a saved preference in the POST, URL or cookies: |
228 | $selectedUI = null; |
229 | if (isset($request)) { |
230 | $selectedUI = $request->getPost()->get( |
231 | 'ui', |
232 | $request->getQuery()->get( |
233 | 'ui', |
234 | $request->getCookie()->ui ?? null |
235 | ) |
236 | ); |
237 | } |
238 | if (empty($selectedUI)) { |
239 | $selectedUI = ($mobileTheme && $this->mobile->detect()) |
240 | ? 'mobile' : 'standard'; |
241 | } |
242 | |
243 | // Save the current setting to a cookie so it persists: |
244 | $this->cookieManager->set('ui', $selectedUI); |
245 | |
246 | // Do we have a valid mobile selection? |
247 | if ($mobileTheme && $selectedUI == 'mobile') { |
248 | return $mobileTheme; |
249 | } |
250 | |
251 | // Do we have a non-standard selection? |
252 | if ( |
253 | $selectedUI != 'standard' |
254 | && isset($this->config->alternate_themes) |
255 | ) { |
256 | // Check the alternate theme settings for a match: |
257 | $parts = explode(',', $this->config->alternate_themes); |
258 | foreach ($parts as $part) { |
259 | $subparts = explode(':', $part); |
260 | if ( |
261 | (trim($subparts[0]) == trim($selectedUI)) |
262 | && isset($subparts[1]) && !empty($subparts[1]) |
263 | ) { |
264 | return $subparts[1]; |
265 | } |
266 | } |
267 | } |
268 | |
269 | // If we got this far, we either have a standard option or the user chose |
270 | // an invalid non-standard option; either way, we need to default to the |
271 | // standard theme: |
272 | return $standardTheme; |
273 | } |
274 | |
275 | /** |
276 | * Make the theme options available to the view. |
277 | * |
278 | * @param string $currentTheme Active theme |
279 | * |
280 | * @return void |
281 | */ |
282 | protected function sendThemeOptionsToView($currentTheme) |
283 | { |
284 | // Get access to the view model: |
285 | if (PHP_SAPI !== 'cli') { |
286 | $viewModel = $this->serviceManager->get('ViewManager')->getViewModel(); |
287 | |
288 | // Send down the view options: |
289 | $viewModel->setVariable('themeOptions', $this->getThemeOptions($currentTheme)); |
290 | } |
291 | } |
292 | |
293 | /** |
294 | * Return an array of information about user-selectable themes. Each entry in |
295 | * the array is an associative array with 'name', 'desc' and 'selected' keys. |
296 | * |
297 | * @param string $currentTheme Active theme |
298 | * |
299 | * @return array |
300 | */ |
301 | protected function getThemeOptions($currentTheme) |
302 | { |
303 | $options = []; |
304 | if (isset($this->config->selectable_themes)) { |
305 | $parts = explode(',', $this->config->selectable_themes); |
306 | foreach ($parts as $part) { |
307 | $subparts = explode(':', $part); |
308 | $name = trim($subparts[0]); |
309 | $desc = isset($subparts[1]) ? trim($subparts[1]) : ''; |
310 | $desc = empty($desc) ? $name : $desc; |
311 | if (!empty($name)) { |
312 | $options[] = [ |
313 | 'name' => $name, 'desc' => $desc, |
314 | 'selected' => ($currentTheme == $name), |
315 | ]; |
316 | } |
317 | } |
318 | } |
319 | return $options; |
320 | } |
321 | |
322 | /** |
323 | * Support method for setUpThemes -- register view helpers. |
324 | * |
325 | * @param array $helpers Helper settings |
326 | * |
327 | * @return void |
328 | */ |
329 | protected function setUpThemeViewHelpers($helpers) |
330 | { |
331 | // Grab the helper loader from the view manager: |
332 | $loader = $this->serviceManager->get('ViewHelperManager'); |
333 | |
334 | // Register all the helpers: |
335 | $config = new \Laminas\ServiceManager\Config($helpers); |
336 | $config->configureServiceManager($loader); |
337 | } |
338 | |
339 | /** |
340 | * Support method for init() -- set up theme once current settings are known. |
341 | * |
342 | * @param array $themes Theme configuration information. |
343 | * |
344 | * @return void |
345 | */ |
346 | protected function setUpThemes($themes) |
347 | { |
348 | $templatePathStack = []; |
349 | |
350 | // Grab the resource manager for tracking CSS, JS, etc.: |
351 | $resources = $this->serviceManager |
352 | ->get(\VuFindTheme\ResourceContainer::class); |
353 | |
354 | // Set generator if necessary: |
355 | if (isset($this->config->generator)) { |
356 | $resources->setGenerator($this->config->generator); |
357 | } |
358 | |
359 | // Determine doctype and apply it: |
360 | $doctype = 'HTML5'; |
361 | foreach ($themes as $key => $currentThemeInfo) { |
362 | if (isset($currentThemeInfo['doctype'])) { |
363 | $doctype = $currentThemeInfo['doctype']; |
364 | break; |
365 | } |
366 | } |
367 | $loader = $this->serviceManager->get('ViewHelperManager'); |
368 | ($loader->get('doctype'))($doctype); |
369 | |
370 | // Apply the loaded theme settings in reverse for proper inheritance: |
371 | foreach ($themes as $key => $currentThemeInfo) { |
372 | if (isset($currentThemeInfo['helpers'])) { |
373 | $this->setUpThemeViewHelpers($currentThemeInfo['helpers']); |
374 | } |
375 | |
376 | // Add template path: |
377 | $templatePathStack[] = $this->tools->getBaseDir() . "/$key/templates"; |
378 | |
379 | // Add CSS and JS dependencies: |
380 | if (isset($currentThemeInfo['css'])) { |
381 | $resources->addCss($currentThemeInfo['css']); |
382 | } |
383 | if (isset($currentThemeInfo['js'])) { |
384 | $resources->addJs($currentThemeInfo['js']); |
385 | } |
386 | |
387 | // Select encoding: |
388 | if (isset($currentThemeInfo['encoding'])) { |
389 | $resources->setEncoding($currentThemeInfo['encoding']); |
390 | } |
391 | |
392 | // Select favicon: |
393 | if (isset($currentThemeInfo['favicon'])) { |
394 | $resources->setFavicon($currentThemeInfo['favicon']); |
395 | } |
396 | } |
397 | |
398 | // Inject the path stack generated above into the resolver: |
399 | $resolver = $this->serviceManager->get(TemplatePathStack::class); |
400 | $resolver->addPaths($templatePathStack); |
401 | |
402 | // Add theme specific language files for translation |
403 | $this->updateTranslator($themes); |
404 | } |
405 | |
406 | /** |
407 | * Support method for setUpThemes() - add theme specific language files for |
408 | * translation. |
409 | * |
410 | * @param array $themes Theme configuration information. |
411 | * |
412 | * @return void |
413 | */ |
414 | protected function updateTranslator($themes) |
415 | { |
416 | $theme = null; |
417 | $pathStack = []; |
418 | foreach (array_keys($themes) as $theme) { |
419 | $dir = APPLICATION_PATH . '/themes/' . $theme . '/languages'; |
420 | if (is_dir($dir)) { |
421 | $pathStack[] = $dir; |
422 | } |
423 | } |
424 | |
425 | if (!empty($pathStack)) { |
426 | try { |
427 | $translator = $this->serviceManager |
428 | ->get(\Laminas\Mvc\I18n\Translator::class); |
429 | $pm = $translator->getPluginManager(); |
430 | $pm->get('ExtendedIni')->addToPathStack($pathStack); |
431 | } catch (\Laminas\Mvc\I18n\Exception\BadMethodCallException $e) { |
432 | // This exception likely indicates that translation is disabled, |
433 | // so we can't proceed. |
434 | return; |
435 | } |
436 | |
437 | // Override the default cache with a theme-specific cache to avoid |
438 | // key collisions in a multi-theme environment. |
439 | try { |
440 | $cacheManager = $this->serviceManager |
441 | ->get(\VuFind\Cache\Manager::class); |
442 | $cacheName = $cacheManager->addLanguageCacheForTheme($theme); |
443 | $translator->setCache($cacheManager->getCache($cacheName)); |
444 | } catch (\Exception $e) { |
445 | // Don't let a cache failure kill the whole application, but make |
446 | // note of it: |
447 | $logger = $this->serviceManager->get(\VuFind\Log\Logger::class); |
448 | $logger->debug( |
449 | 'Problem loading cache: ' . $e::class . ' exception: ' |
450 | . $e->getMessage() |
451 | ); |
452 | } |
453 | } |
454 | } |
455 | } |