Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 128 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
Initializer | |
0.00% |
0 / 128 |
|
0.00% |
0 / 8 |
2352 | |
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 / 29 |
|
0.00% |
0 / 1 |
240 | |||
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 | // Load standard configuration options: |
205 | $standardTheme = $this->config->theme; |
206 | if (PHP_SAPI == 'cli') { |
207 | return $standardTheme; |
208 | } |
209 | $mobileTheme = $this->mobile->enabled() |
210 | ? $this->config->mobile_theme : false; |
211 | |
212 | // Find out if the user has a saved preference in the POST, URL or cookies: |
213 | $selectedUI = null; |
214 | if (isset($request)) { |
215 | $selectedUI = $request->getPost()->get( |
216 | 'ui', |
217 | $request->getQuery()->get( |
218 | 'ui', |
219 | $request->getCookie()->ui ?? null |
220 | ) |
221 | ); |
222 | } |
223 | if (empty($selectedUI)) { |
224 | $selectedUI = ($mobileTheme && $this->mobile->detect()) |
225 | ? 'mobile' : 'standard'; |
226 | } |
227 | |
228 | // Save the current setting to a cookie so it persists: |
229 | $this->cookieManager->set('ui', $selectedUI); |
230 | |
231 | // Do we have a valid mobile selection? |
232 | if ($mobileTheme && $selectedUI == 'mobile') { |
233 | return $mobileTheme; |
234 | } |
235 | |
236 | // Do we have a non-standard selection? |
237 | if ( |
238 | $selectedUI != 'standard' |
239 | && isset($this->config->alternate_themes) |
240 | ) { |
241 | // Check the alternate theme settings for a match: |
242 | $parts = explode(',', $this->config->alternate_themes); |
243 | foreach ($parts as $part) { |
244 | $subparts = explode(':', $part); |
245 | if ( |
246 | (trim($subparts[0]) == trim($selectedUI)) |
247 | && isset($subparts[1]) && !empty($subparts[1]) |
248 | ) { |
249 | return $subparts[1]; |
250 | } |
251 | } |
252 | } |
253 | |
254 | // If we got this far, we either have a standard option or the user chose |
255 | // an invalid non-standard option; either way, we need to default to the |
256 | // standard theme: |
257 | return $standardTheme; |
258 | } |
259 | |
260 | /** |
261 | * Make the theme options available to the view. |
262 | * |
263 | * @param string $currentTheme Active theme |
264 | * |
265 | * @return void |
266 | */ |
267 | protected function sendThemeOptionsToView($currentTheme) |
268 | { |
269 | // Get access to the view model: |
270 | if (PHP_SAPI !== 'cli') { |
271 | $viewModel = $this->serviceManager->get('ViewManager')->getViewModel(); |
272 | |
273 | // Send down the view options: |
274 | $viewModel->setVariable('themeOptions', $this->getThemeOptions($currentTheme)); |
275 | } |
276 | } |
277 | |
278 | /** |
279 | * Return an array of information about user-selectable themes. Each entry in |
280 | * the array is an associative array with 'name', 'desc' and 'selected' keys. |
281 | * |
282 | * @param string $currentTheme Active theme |
283 | * |
284 | * @return array |
285 | */ |
286 | protected function getThemeOptions($currentTheme) |
287 | { |
288 | $options = []; |
289 | if (isset($this->config->selectable_themes)) { |
290 | $parts = explode(',', $this->config->selectable_themes); |
291 | foreach ($parts as $part) { |
292 | $subparts = explode(':', $part); |
293 | $name = trim($subparts[0]); |
294 | $desc = isset($subparts[1]) ? trim($subparts[1]) : ''; |
295 | $desc = empty($desc) ? $name : $desc; |
296 | if (!empty($name)) { |
297 | $options[] = [ |
298 | 'name' => $name, 'desc' => $desc, |
299 | 'selected' => ($currentTheme == $name), |
300 | ]; |
301 | } |
302 | } |
303 | } |
304 | return $options; |
305 | } |
306 | |
307 | /** |
308 | * Support method for setUpThemes -- register view helpers. |
309 | * |
310 | * @param array $helpers Helper settings |
311 | * |
312 | * @return void |
313 | */ |
314 | protected function setUpThemeViewHelpers($helpers) |
315 | { |
316 | // Grab the helper loader from the view manager: |
317 | $loader = $this->serviceManager->get('ViewHelperManager'); |
318 | |
319 | // Register all the helpers: |
320 | $config = new \Laminas\ServiceManager\Config($helpers); |
321 | $config->configureServiceManager($loader); |
322 | } |
323 | |
324 | /** |
325 | * Support method for init() -- set up theme once current settings are known. |
326 | * |
327 | * @param array $themes Theme configuration information. |
328 | * |
329 | * @return void |
330 | */ |
331 | protected function setUpThemes($themes) |
332 | { |
333 | $templatePathStack = []; |
334 | |
335 | // Grab the resource manager for tracking CSS, JS, etc.: |
336 | $resources = $this->serviceManager |
337 | ->get(\VuFindTheme\ResourceContainer::class); |
338 | |
339 | // Set generator if necessary: |
340 | if (isset($this->config->generator)) { |
341 | $resources->setGenerator($this->config->generator); |
342 | } |
343 | |
344 | // Determine doctype and apply it: |
345 | $doctype = 'HTML5'; |
346 | foreach ($themes as $key => $currentThemeInfo) { |
347 | if (isset($currentThemeInfo['doctype'])) { |
348 | $doctype = $currentThemeInfo['doctype']; |
349 | break; |
350 | } |
351 | } |
352 | $loader = $this->serviceManager->get('ViewHelperManager'); |
353 | ($loader->get('doctype'))($doctype); |
354 | |
355 | // Apply the loaded theme settings in reverse for proper inheritance: |
356 | foreach ($themes as $key => $currentThemeInfo) { |
357 | if (isset($currentThemeInfo['helpers'])) { |
358 | $this->setUpThemeViewHelpers($currentThemeInfo['helpers']); |
359 | } |
360 | |
361 | // Add template path: |
362 | $templatePathStack[] = $this->tools->getBaseDir() . "/$key/templates"; |
363 | |
364 | // Add CSS and JS dependencies: |
365 | if (isset($currentThemeInfo['css'])) { |
366 | $resources->addCss($currentThemeInfo['css']); |
367 | } |
368 | if (isset($currentThemeInfo['js'])) { |
369 | $resources->addJs($currentThemeInfo['js']); |
370 | } |
371 | |
372 | // Select encoding: |
373 | if (isset($currentThemeInfo['encoding'])) { |
374 | $resources->setEncoding($currentThemeInfo['encoding']); |
375 | } |
376 | |
377 | // Select favicon: |
378 | if (isset($currentThemeInfo['favicon'])) { |
379 | $resources->setFavicon($currentThemeInfo['favicon']); |
380 | } |
381 | } |
382 | |
383 | // Inject the path stack generated above into the resolver: |
384 | $resolver = $this->serviceManager->get(TemplatePathStack::class); |
385 | $resolver->addPaths($templatePathStack); |
386 | |
387 | // Add theme specific language files for translation |
388 | $this->updateTranslator($themes); |
389 | } |
390 | |
391 | /** |
392 | * Support method for setUpThemes() - add theme specific language files for |
393 | * translation. |
394 | * |
395 | * @param array $themes Theme configuration information. |
396 | * |
397 | * @return void |
398 | */ |
399 | protected function updateTranslator($themes) |
400 | { |
401 | $theme = null; |
402 | $pathStack = []; |
403 | foreach (array_keys($themes) as $theme) { |
404 | $dir = APPLICATION_PATH . '/themes/' . $theme . '/languages'; |
405 | if (is_dir($dir)) { |
406 | $pathStack[] = $dir; |
407 | } |
408 | } |
409 | |
410 | if (!empty($pathStack)) { |
411 | try { |
412 | $translator = $this->serviceManager |
413 | ->get(\Laminas\Mvc\I18n\Translator::class); |
414 | $pm = $translator->getPluginManager(); |
415 | $pm->get('ExtendedIni')->addToPathStack($pathStack); |
416 | } catch (\Laminas\Mvc\I18n\Exception\BadMethodCallException $e) { |
417 | // This exception likely indicates that translation is disabled, |
418 | // so we can't proceed. |
419 | return; |
420 | } |
421 | |
422 | // Override the default cache with a theme-specific cache to avoid |
423 | // key collisions in a multi-theme environment. |
424 | try { |
425 | $cacheManager = $this->serviceManager |
426 | ->get(\VuFind\Cache\Manager::class); |
427 | $cacheName = $cacheManager->addLanguageCacheForTheme($theme); |
428 | $translator->setCache($cacheManager->getCache($cacheName)); |
429 | } catch (\Exception $e) { |
430 | // Don't let a cache failure kill the whole application, but make |
431 | // note of it: |
432 | $logger = $this->serviceManager->get(\VuFind\Log\Logger::class); |
433 | $logger->debug( |
434 | 'Problem loading cache: ' . $e::class . ' exception: ' |
435 | . $e->getMessage() |
436 | ); |
437 | } |
438 | } |
439 | } |
440 | } |