Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 130
0.00% covered (danger)
0.00%
0 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
Bootstrapper
0.00% covered (danger)
0.00%
0 / 130
0.00% covered (danger)
0.00%
0 / 17
1482
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 bootstrap
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getDbService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initTestMode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 initSystemStatus
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 initTimeZone
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initContext
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 initViewModel
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 initUserLanguage
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 initTheme
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 initLoginTokenManager
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 initExceptionBasedHttpStatuses
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 initSearch
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 initErrorLogging
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 initRenderErrorEvent
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 initContentSecurityPolicy
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 initRateLimiter
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3/**
4 * VuFind Bootstrapper
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  Bootstrap
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 VuFind;
31
32use Laminas\Mvc\MvcEvent;
33use Laminas\Router\Http\RouteMatch;
34use Psr\Container\ContainerInterface;
35use VuFind\I18n\Locale\LocaleSettings;
36
37/**
38 * VuFind Bootstrapper
39 *
40 * @category VuFind
41 * @package  Bootstrap
42 * @author   Demian Katz <demian.katz@villanova.edu>
43 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
44 * @link     https://vufind.org Main Site
45 */
46class Bootstrapper
47{
48    /**
49     * Main VuFind configuration
50     *
51     * @var \Laminas\Config\Config
52     */
53    protected $config;
54
55    /**
56     * Service manager
57     *
58     * @var ContainerInterface
59     */
60    protected $container;
61
62    /**
63     * Current MVC event
64     *
65     * @var MvcEvent
66     */
67    protected $event;
68
69    /**
70     * Event manager
71     *
72     * @var \Laminas\EventManager\EventManagerInterface
73     */
74    protected $events;
75
76    /**
77     * Constructor
78     *
79     * @param MvcEvent $event Laminas MVC Event object
80     */
81    public function __construct(MvcEvent $event)
82    {
83        $this->event = $event;
84        $app = $event->getApplication();
85        $this->events = $app->getEventManager();
86        $this->container = $app->getServiceManager();
87        $this->config = $this->container->get(\VuFind\Config\PluginManager::class)
88            ->get('config');
89    }
90
91    /**
92     * Bootstrap all necessary resources.
93     *
94     * @return void
95     */
96    public function bootstrap(): void
97    {
98        // automatically call all methods starting with "init":
99        $methods = get_class_methods($this);
100        foreach ($methods as $method) {
101            if (str_starts_with($method, 'init')) {
102                $this->$method();
103            }
104        }
105    }
106
107    /**
108     * Get a database service object.
109     *
110     * @param class-string<T> $name Name of service to retrieve
111     *
112     * @template T
113     *
114     * @return T
115     */
116    public function getDbService(string $name): \VuFind\Db\Service\DbServiceInterface
117    {
118        return $this->container->get(\VuFind\Db\Service\PluginManager::class)->get($name);
119    }
120
121    /**
122     * Set up cookie to flag test mode.
123     *
124     * @return void
125     */
126    protected function initTestMode(): void
127    {
128        // If we're in test mode (as determined by the config.ini property installed
129        // by the build.xml startup process), set a cookie so the front-end code can
130        // act accordingly. (This is needed to work around a problem where opening
131        // print dialogs during testing stalls the automated test process).
132        if ($this->config->System->runningTestSuite ?? false) {
133            $cm = $this->container->get(\VuFind\Cookie\CookieManager::class);
134            $cm->set('VuFindTestSuiteRunning', '1', 0, false);
135        }
136    }
137
138    /**
139     * If the system is offline, set up a handler to override the routing output.
140     *
141     * @return void
142     */
143    protected function initSystemStatus(): void
144    {
145        // If the system is unavailable and we're not in the console, forward to the
146        // unavailable page.
147        if (PHP_SAPI !== 'cli' && !($this->config->System->available ?? true)) {
148            $callback = function ($e) {
149                $routeMatch = new RouteMatch(
150                    ['controller' => 'Error', 'action' => 'Unavailable'],
151                    1
152                );
153                $routeMatch->setMatchedRouteName('error-unavailable');
154                $e->setRouteMatch($routeMatch);
155            };
156            $this->events->attach('route', $callback);
157        }
158    }
159
160    /**
161     * Initializes timezone value
162     *
163     * @return void
164     */
165    protected function initTimeZone(): void
166    {
167        date_default_timezone_set($this->config->Site->timezone);
168    }
169
170    /**
171     * Set view variables representing the current context.
172     *
173     * @return void
174     */
175    protected function initContext(): void
176    {
177        $callback = function ($event) {
178            if (PHP_SAPI !== 'cli') {
179                $viewModel = $this->container->get('ViewManager')->getViewModel();
180
181                // Grab the template name from the first child -- we can use this to
182                // figure out the current template context.
183                $children = $viewModel->getChildren();
184                if (!empty($children)) {
185                    $parts = explode('/', $children[0]->getTemplate());
186                    $viewModel->setVariable('templateDir', $parts[0]);
187                    $viewModel->setVariable(
188                        'templateName',
189                        $parts[1] ?? null
190                    );
191                }
192            }
193        };
194        $this->events->attach('dispatch', $callback);
195    }
196
197    /**
198     * Set up the initial view model.
199     *
200     * @return void
201     */
202    protected function initViewModel(): void
203    {
204        $settings = $this->container->get(LocaleSettings::class);
205        $locale = $settings->getUserLocale();
206        $viewModel = $this->container->get('HttpViewManager')->getViewModel();
207        $viewModel->setVariable('userLang', $locale);
208        $viewModel->setVariable('allLangs', $settings->getEnabledLocales());
209        $viewModel->setVariable('rtl', $settings->isRightToLeftLocale($locale));
210    }
211
212    /**
213     * Update language in user account, as needed.
214     *
215     * @return void
216     */
217    protected function initUserLanguage(): void
218    {
219        $callback = function ($event) {
220            // Store last selected language in user account, if applicable:
221            $settings = $this->container->get(LocaleSettings::class);
222            $language = $settings->getUserLocale();
223            $authManager = $this->container->get(\VuFind\Auth\Manager::class);
224            if (
225                ($user = $authManager->getUserObject())
226                && $user->getLastLanguage() != $language
227            ) {
228                $user->setLastLanguage($language);
229                $this->getDbService(\VuFind\Db\Service\UserServiceInterface::class)->persistEntity($user);
230            }
231        };
232        $this->events->attach('dispatch.error', $callback);
233        $this->events->attach('dispatch', $callback);
234    }
235
236    /**
237     * Set up theme handling.
238     *
239     * @return void
240     */
241    protected function initTheme(): void
242    {
243        // Attach remaining theme configuration to the dispatch event at high
244        // priority (TODO: use priority constant once defined by framework):
245        $config = $this->config->Site;
246        $callback = function ($event) use ($config) {
247            $theme = new \VuFindTheme\Initializer($config, $event);
248            $theme->init();
249        };
250        $this->events->attach('dispatch.error', $callback, 9000);
251        $this->events->attach('dispatch', $callback, 9000);
252    }
253
254    /**
255     * The login token manager needs to be informed after the theme has been initialized,
256     * so that it can send warning emails if necessary.
257     *
258     * @return void
259     */
260    protected function initLoginTokenManager(): void
261    {
262        $dispatchCallback = function () {
263            $this->container->get(\VuFind\Auth\LoginTokenManager::class)->themeIsReady();
264        };
265        $finishCallback = function () {
266            $this->container->get(\VuFind\Auth\LoginTokenManager::class)->requestIsFinished();
267        };
268        $this->events->attach('dispatch.error', $dispatchCallback, 8000);
269        $this->events->attach('dispatch', $dispatchCallback, 8000);
270        $this->events->attach('finish', $finishCallback, 8000);
271    }
272
273    /**
274     * Set up custom HTTP status based on exception information.
275     *
276     * @return void
277     */
278    protected function initExceptionBasedHttpStatuses(): void
279    {
280        // HTTP statuses not needed in console mode:
281        if (PHP_SAPI == 'cli') {
282            return;
283        }
284
285        $callback = function ($e) {
286            $exception = $e->getParam('exception');
287            if ($exception instanceof \VuFind\Exception\HttpStatusInterface) {
288                $response = $e->getResponse();
289                if (!$response) {
290                    $response = new \Laminas\Http\Response();
291                    $e->setResponse($response);
292                }
293                $response->setStatusCode($exception->getHttpStatus());
294            }
295        };
296        $this->events->attach('dispatch.error', $callback);
297    }
298
299    /**
300     * Set up search subsystem.
301     *
302     * @return void
303     */
304    protected function initSearch(): void
305    {
306        $bm = $this->container->get(\VuFind\Search\BackendManager::class);
307        $events = $this->container->get('SharedEventManager');
308        $events->attach(
309            \VuFindSearch\Service::class,
310            \VuFindSearch\Service::EVENT_RESOLVE,
311            [$bm, 'onResolve']
312        );
313    }
314
315    /**
316     * Set up logging.
317     *
318     * @return void
319     */
320    protected function initErrorLogging(): void
321    {
322        $callback = function ($event) {
323            if ($this->container->has(\VuFind\Log\Logger::class)) {
324                $log = $this->container->get(\VuFind\Log\Logger::class);
325                if ($log instanceof \VuFind\Log\ExtendedLoggerInterface) {
326                    $exception = $event->getParam('exception');
327                    // Console request does not include server,
328                    // so use a dummy in that case.
329                    $server = (PHP_SAPI == 'cli')
330                        ? new \Laminas\Stdlib\Parameters(['env' => 'console'])
331                        : $event->getRequest()->getServer();
332                    if (!empty($exception)) {
333                        $log->logException($exception, $server);
334                    }
335                }
336            }
337        };
338        $this->events->attach('dispatch.error', $callback);
339        $this->events->attach('render.error', $callback);
340    }
341
342    /**
343     * Set up handling for rendering problems.
344     *
345     * @return void
346     */
347    protected function initRenderErrorEvent(): void
348    {
349        // When a render.error is triggered, as a high priority, set a flag in the
350        // layout that can be used to suppress actions in the layout templates that
351        // might trigger exceptions -- this will greatly increase the odds of showing
352        // a user-friendly message instead of a fatal error.
353        $callback = function ($event) {
354            $viewModel = $this->container->get('ViewManager')->getViewModel();
355            $viewModel->renderingError = true;
356        };
357        $this->events->attach('render.error', $callback, 10000);
358    }
359
360    /**
361     * Set up content security policy
362     *
363     * @return void
364     */
365    protected function initContentSecurityPolicy(): void
366    {
367        if (PHP_SAPI === 'cli') {
368            return;
369        }
370        $headers = $this->event->getResponse()->getHeaders();
371        $cspHeaderGenerator = $this->container
372            ->get(\VuFind\Security\CspHeaderGenerator::class);
373        foreach ($cspHeaderGenerator->getHeaders() as $cspHeader) {
374            $headers->addHeader($cspHeader);
375        }
376    }
377
378    /**
379     * Set up rate limiter
380     *
381     * @return void
382     */
383    protected function initRateLimiter(): void
384    {
385        if (PHP_SAPI === 'cli') {
386            return;
387        }
388        $callback = function ($event) {
389            // Create rate limiter manager here so that we don't e.g. initialize the session too early:
390            $rateLimiterManager = $this->container->get(\VuFind\RateLimiter\RateLimiterManager::class);
391            if (!$rateLimiterManager->isEnabled()) {
392                return;
393            }
394            $result = $rateLimiterManager->check($event);
395            if (!$result['allow']) {
396                $response = $event->getResponse();
397                $response->setStatusCode(429);
398                $response->setContent($result['message']);
399                $event->stopPropagation(true);
400                return $response;
401            }
402        };
403        $this->events->attach('dispatch', $callback, 11000);
404    }
405}