Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
11.06% covered (danger)
11.06%
25 / 226
14.29% covered (danger)
14.29%
6 / 42
CRAP
0.00% covered (danger)
0.00%
0 / 1
AbstractBase
11.06% covered (danger)
11.06%
25 / 226
14.29% covered (danger)
14.29%
6 / 42
7566.41
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateAccessPermission
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getAccessPermission
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setAccessPermission
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getRequest
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 attachDefaultListeners
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 createViewModel
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
3.18
 createEmailViewModel
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
342
 getAuthManager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAuthorizationService
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getILSAuthenticator
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getViewRenderer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 forceLogin
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 catalogLogin
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
272
 getConfig
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getILS
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRecordLoader
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRecordCache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRecordRouter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTable
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDbService
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getServerUrl
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 forwardTo
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 formWasSubmitted
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
30
 confirm
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 disableSessionWrites
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSearchMemory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 commentsEnabled
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 listsEnabled
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 tagsEnabled
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 setFollowupUrlToReferer
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 normalizeUrlForComparison
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 hasFollowupUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAndClearFollowupUrl
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 clearFollowupUrl
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getRecordTabManager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 inLightbox
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getILSLoginMethod
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getILSLoginSettings
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 getRefreshResponse
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isLocalUrl
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * VuFind controller base class (defines some methods that can be shared by other
5 * controllers).
6 *
7 * PHP version 8
8 *
9 * Copyright (C) Villanova University 2010.
10 *
11 * This program is free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License version 2,
13 * as published by the Free Software Foundation.
14 *
15 * This program is distributed in the hope that it will be useful,
16 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 * GNU General Public License for more details.
19 *
20 * You should have received a copy of the GNU General Public License
21 * along with this program; if not, write to the Free Software
22 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
23 *
24 * @category VuFind
25 * @package  Controller
26 * @author   Demian Katz <demian.katz@villanova.edu>
27 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
28 * @link     https://vufind.org/wiki/development:plugins:controllers Wiki
29 */
30
31namespace VuFind\Controller;
32
33use Laminas\Mvc\Controller\AbstractActionController;
34use Laminas\Mvc\MvcEvent;
35use Laminas\Mvc\Plugin\FlashMessenger\FlashMessenger;
36use Laminas\ServiceManager\ServiceLocatorInterface;
37use Laminas\Uri\Http;
38use Laminas\View\Model\ViewModel;
39use VuFind\Controller\Feature\AccessPermissionInterface;
40use VuFind\Db\Entity\UserEntityInterface;
41use VuFind\Exception\Auth as AuthException;
42use VuFind\Exception\ILS as ILSException;
43use VuFind\Http\PhpEnvironment\Request as HttpRequest;
44use VuFind\I18n\Translator\TranslatorAwareInterface;
45use VuFind\I18n\Translator\TranslatorAwareTrait;
46
47use function intval;
48use function is_object;
49
50/**
51 * VuFind controller base class (defines some methods that can be shared by other
52 * controllers).
53 *
54 * @category VuFind
55 * @package  Controller
56 * @author   Chris Hallberg <challber@villanova.edu>
57 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
58 * @link     https://vufind.org/wiki/development:plugins:controllers Wiki
59 *
60 * @method Plugin\Captcha captcha() Captcha plugin
61 * @method Plugin\DbUpgrade dbUpgrade() DbUpgrade plugin
62 * @method FlashMessenger flashMessenger() FlashMessenger plugin
63 * @method Plugin\Followup followup() Followup plugin
64 * @method Plugin\Holds holds() Holds plugin
65 * @method Plugin\ILLRequests ILLRequests() ILLRequests plugin
66 * @method Plugin\IlsRecords ilsRecords() IlsRecords plugin
67 * @method Plugin\NewItems newItems() NewItems plugin
68 * @method Plugin\Permission permission() Permission plugin
69 * @method Plugin\Renewals renewals() Renewals plugin
70 * @method Plugin\Reserves reserves() Reserves plugin
71 * @method Plugin\ResultScroller resultScroller() ResultScroller plugin
72 * @method Plugin\StorageRetrievalRequests storageRetrievalRequests()
73 * StorageRetrievalRequests plugin
74 *
75 * @SuppressWarnings(PHPMD.NumberOfChildren)
76 */
77class AbstractBase extends AbstractActionController implements AccessPermissionInterface, TranslatorAwareInterface
78{
79    use TranslatorAwareTrait;
80
81    /**
82     * Permission that must be granted to access this module (false for no
83     * restriction, null to use configured default (which is usually the same
84     * as false)).
85     *
86     * @var string|bool|null
87     */
88    protected $accessPermission = null;
89
90    /**
91     * Behavior when access is denied (used unless overridden through
92     * permissionBehavior.ini). Valid values are 'promptLogin' and 'exception'.
93     * Leave at null to use the defaultDeniedControllerBehavior set in
94     * permissionBehavior.ini (normally 'promptLogin' unless changed).
95     *
96     * @var string
97     */
98    protected $accessDeniedBehavior = null;
99
100    /**
101     * Service manager
102     *
103     * @var ServiceLocatorInterface
104     */
105    protected $serviceLocator;
106
107    /**
108     * Constructor
109     *
110     * @param ServiceLocatorInterface $sm Service locator
111     */
112    public function __construct(ServiceLocatorInterface $sm)
113    {
114        $this->serviceLocator = $sm;
115    }
116
117    /**
118     * Use preDispatch event to block access when appropriate.
119     *
120     * @param MvcEvent $e Event object
121     *
122     * @return void
123     */
124    public function validateAccessPermission(MvcEvent $e)
125    {
126        // If there is an access permission set for this controller, pass it
127        // through the permission helper, and if the helper returns a custom
128        // response, use that instead of the normal behavior.
129        if ($this->accessPermission) {
130            $response = $this->permission()
131                ->check($this->accessPermission, $this->accessDeniedBehavior);
132            if (is_object($response)) {
133                $e->setResponse($response);
134            }
135        }
136    }
137
138    /**
139     * Getter for access permission (string for required permission name, false
140     * for no permission required, null to use default permission).
141     *
142     * @return string|bool|null
143     */
144    public function getAccessPermission()
145    {
146        return $this->accessPermission;
147    }
148
149    /**
150     * Getter for access permission.
151     *
152     * @param string|false $ap Permission to require for access to the controller (false
153     * for no requirement)
154     *
155     * @return void
156     */
157    public function setAccessPermission($ap)
158    {
159        $this->accessPermission = empty($ap) ? false : $ap;
160    }
161
162    /**
163     * Get request object
164     *
165     * @return HttpRequest
166     */
167    public function getRequest()
168    {
169        if (!$this->request) {
170            $this->request = new HttpRequest();
171        }
172
173        return $this->request;
174    }
175
176    /**
177     * Register the default events for this controller
178     *
179     * @return void
180     */
181    protected function attachDefaultListeners()
182    {
183        parent::attachDefaultListeners();
184
185        // Attach preDispatch event if we need to check permissions.
186        if ($this->accessPermission) {
187            $events = $this->getEventManager();
188            $events->attach(
189                MvcEvent::EVENT_DISPATCH,
190                [$this, 'validateAccessPermission'],
191                1000
192            );
193        }
194    }
195
196    /**
197     * Create a new ViewModel.
198     *
199     * @param array $params Parameters to pass to ViewModel constructor.
200     *
201     * @return ViewModel
202     */
203    protected function createViewModel($params = null)
204    {
205        if ($this->inLightbox()) {
206            $this->layout()->setTemplate('layout/lightbox');
207            $params['inLightbox'] = true;
208        }
209        $lightboxParentUrl = new Http($this->getServerUrl());
210        $query = $lightboxParentUrl->getQueryAsArray();
211        unset($query['lightboxChild']);
212        $lightboxParentUrl->setQuery($query);
213        $this->layout()->lightboxParent = $lightboxParentUrl->toString();
214        if ($lightboxChild = $this->getRequest()->getQuery('lightboxChild')) {
215            $this->layout()->lightboxChild = $lightboxChild;
216        }
217        return new ViewModel($params);
218    }
219
220    /**
221     * Create a new ViewModel to use as an email form.
222     *
223     * @param array  $params         Parameters to pass to ViewModel constructor.
224     * @param string $defaultSubject Default subject line to use.
225     *
226     * @return ViewModel
227     */
228    protected function createEmailViewModel($params = null, $defaultSubject = null)
229    {
230        // Build view:
231        $view = $this->createViewModel($params);
232
233        // Load configuration and current user for convenience:
234        $config = $this->getConfig();
235        $view->disableFrom
236            = (isset($config->Mail->disable_from) && $config->Mail->disable_from);
237        $view->editableSubject = isset($config->Mail->user_editable_subjects)
238            && $config->Mail->user_editable_subjects;
239        $view->maxRecipients = isset($config->Mail->maximum_recipients)
240            ? intval($config->Mail->maximum_recipients) : 1;
241        $user = $this->getUser();
242
243        // Send parameters back to view so form can be re-populated:
244        if ($this->getRequest()->isPost()) {
245            $view->to = $this->params()->fromPost('to');
246            if (!$view->disableFrom) {
247                $view->from = $this->params()->fromPost('from');
248            }
249            if ($view->editableSubject) {
250                $view->subject = $this->params()->fromPost('subject');
251            }
252            $view->message = $this->params()->fromPost('message');
253        }
254
255        // Set default values if applicable:
256        if (empty($view->to) && $user && ($config->Mail->user_email_in_to ?? false)) {
257            $view->to = $user->getEmail();
258        }
259        if (empty($view->from)) {
260            if ($user && ($config->Mail->user_email_in_from ?? false)) {
261                $view->userEmailInFrom = true;
262                $view->from = $user->getEmail();
263            } elseif ($config->Mail->default_from ?? false) {
264                $view->from = $config->Mail->default_from;
265            }
266        }
267        if (empty($view->subject)) {
268            $view->subject = $defaultSubject;
269        }
270
271        // Fail if we're missing a from and the form element is disabled:
272        if ($view->disableFrom) {
273            if (empty($view->from)) {
274                $view->from = $config->Site->email;
275            }
276            if (empty($view->from)) {
277                throw new \Exception('Unable to determine email from address');
278            }
279        }
280
281        return $view;
282    }
283
284    /**
285     * Get the account manager object.
286     *
287     * @return \VuFind\Auth\Manager
288     */
289    protected function getAuthManager()
290    {
291        return $this->serviceLocator->get(\VuFind\Auth\Manager::class);
292    }
293
294    /**
295     * Get the authorization service (note that we're doing this on-demand
296     * rather than through injection with the AuthorizationServiceAwareInterface
297     * to minimize expensive initialization when authorization is not needed.
298     *
299     * @return \LmcRbacMvc\Service\AuthorizationService
300     */
301    protected function getAuthorizationService()
302    {
303        return $this->serviceLocator
304            ->get(\LmcRbacMvc\Service\AuthorizationService::class);
305    }
306
307    /**
308     * Get the ILS authenticator.
309     *
310     * @return \VuFind\Auth\ILSAuthenticator
311     */
312    protected function getILSAuthenticator()
313    {
314        return $this->serviceLocator->get(\VuFind\Auth\ILSAuthenticator::class);
315    }
316
317    /**
318     * Get the user object if logged in, false otherwise.
319     *
320     * @return ?UserEntityInterface
321     */
322    protected function getUser(): ?UserEntityInterface
323    {
324        return $this->getAuthManager()->getUserObject();
325    }
326
327    /**
328     * Get the view renderer
329     *
330     * @return \Laminas\View\Renderer\RendererInterface
331     */
332    protected function getViewRenderer()
333    {
334        return $this->serviceLocator->get('ViewRenderer');
335    }
336
337    /**
338     * Redirect the user to the login screen.
339     *
340     * @param string $msg     Flash message to display on login screen
341     * @param array  $extras  Associative array of extra fields to store
342     * @param bool   $forward True to forward, false to redirect
343     *
344     * @return mixed
345     */
346    public function forceLogin($msg = null, $extras = [], $forward = true)
347    {
348        // Set default message if necessary.
349        if (null === $msg) {
350            $msg = 'You must be logged in first';
351        }
352
353        // store parent url of lightboxes
354        $extras['lightboxParent'] = $this->getRequest()->getQuery('lightboxParent');
355
356        // Store the current URL as a login followup action
357        $this->followup()->store($extras);
358        if (!empty($msg)) {
359            $this->flashMessenger()->addMessage($msg, 'error');
360        }
361
362        // Set a flag indicating that we are forcing login:
363        $this->getRequest()->getPost()->set('forcingLogin', true);
364
365        if ($forward) {
366            return $this->forwardTo('MyResearch', 'Login');
367        }
368        return $this->redirect()->toRoute('myresearch-home');
369    }
370
371    /**
372     * Does the user have catalog credentials available?  Returns associative array
373     * of patron data if so, otherwise forwards to appropriate login prompt and
374     * returns false. If there is an ILS exception, a flash message is added and
375     * a newly created ViewModel is returned.
376     *
377     * @return bool|array|ViewModel
378     */
379    protected function catalogLogin()
380    {
381        // First make sure user is logged in to VuFind:
382        $account = $this->getAuthManager();
383        if (!$account->getIdentity()) {
384            return $this->forceLogin();
385        }
386
387        // Now check if the user has provided credentials with which to log in:
388        $ilsAuth = $this->getILSAuthenticator();
389        $patron = null;
390        if (
391            ($username = $this->params()->fromPost('cat_username', false))
392            && ($password = $this->params()->fromPost('cat_password', false))
393        ) {
394            // If somebody is POSTing credentials but that logic is disabled, we
395            // should throw an exception!
396            if (!$account->allowsUserIlsLogin()) {
397                throw new \Exception('Unexpected ILS credential submission.');
398            }
399            // Check for multiple ILS target selection
400            $target = $this->params()->fromPost('target', false);
401            if ($target) {
402                $username = "$target.$username";
403            }
404            try {
405                if ('email' === $this->getILSLoginMethod($target)) {
406                    $routeMatch = $this->getEvent()->getRouteMatch();
407                    $routeName = $routeMatch ? $routeMatch->getMatchedRouteName()
408                        : 'myresearch-profile';
409                    $routeParams = $routeMatch ? $routeMatch->getParams() : [];
410                    $ilsAuth->sendEmailLoginLink($username, $routeName, $routeParams, ['catalogLogin' => 'true']);
411                    $this->flashMessenger()
412                        ->addSuccessMessage('email_login_link_sent');
413                } else {
414                    $patron = $ilsAuth->newCatalogLogin($username, $password);
415
416                    // If login failed, store a warning message:
417                    if (!$patron) {
418                        $this->flashMessenger()
419                            ->addErrorMessage('Invalid Patron Login');
420                    }
421                }
422            } catch (ILSException $e) {
423                $this->flashMessenger()->addErrorMessage('ils_connection_failed');
424            }
425        } elseif (
426            'ILS' === $this->params()->fromQuery('auth_method', false)
427            && ($hash = $this->params()->fromQuery('hash', false))
428        ) {
429            try {
430                $patron = $ilsAuth->processEmailLoginHash($hash);
431            } catch (AuthException $e) {
432                $this->flashMessenger()->addErrorMessage($e->getMessage());
433            }
434        } else {
435            try {
436                // If no credentials were provided, try the stored values:
437                $patron = $ilsAuth->storedCatalogLogin();
438            } catch (ILSException $e) {
439                $this->flashMessenger()->addErrorMessage('ils_connection_failed');
440                return $this->createViewModel();
441            }
442        }
443
444        // If catalog login failed, send the user to the right page:
445        if (!$patron) {
446            return $this->forwardTo('MyResearch', 'CatalogLogin');
447        }
448
449        // Send value (either false or patron array) back to caller:
450        return $patron;
451    }
452
453    /**
454     * Get a VuFind configuration.
455     *
456     * @param string $id Configuration identifier (default = main VuFind config)
457     *
458     * @return \Laminas\Config\Config
459     */
460    public function getConfig($id = 'config')
461    {
462        return $this->serviceLocator->get(\VuFind\Config\PluginManager::class)
463            ->get($id);
464    }
465
466    /**
467     * Get the ILS connection.
468     *
469     * @return \VuFind\ILS\Connection
470     */
471    public function getILS()
472    {
473        return $this->serviceLocator->get(\VuFind\ILS\Connection::class);
474    }
475
476    /**
477     * Get the record loader
478     *
479     * @return \VuFind\Record\Loader
480     */
481    public function getRecordLoader()
482    {
483        return $this->serviceLocator->get(\VuFind\Record\Loader::class);
484    }
485
486    /**
487     * Get the record cache
488     *
489     * @return \VuFind\Record\Cache
490     */
491    public function getRecordCache()
492    {
493        return $this->serviceLocator->get(\VuFind\Record\Cache::class);
494    }
495
496    /**
497     * Get the record router.
498     *
499     * @return \VuFind\Record\Router
500     */
501    public function getRecordRouter()
502    {
503        return $this->serviceLocator->get(\VuFind\Record\Router::class);
504    }
505
506    /**
507     * Get a database table object.
508     *
509     * @param string $table Name of table to retrieve
510     *
511     * @return \VuFind\Db\Table\Gateway
512     */
513    public function getTable($table)
514    {
515        return $this->serviceLocator->get(\VuFind\Db\Table\PluginManager::class)
516            ->get($table);
517    }
518
519    /**
520     * Get a database service object.
521     *
522     * @param class-string<T> $name Name of service to retrieve
523     *
524     * @template T
525     *
526     * @return T
527     */
528    public function getDbService(string $name): \VuFind\Db\Service\DbServiceInterface
529    {
530        return $this->serviceLocator->get(\VuFind\Db\Service\PluginManager::class)
531            ->get($name);
532    }
533
534    /**
535     * Get the full URL to one of VuFind's routes.
536     *
537     * @param bool|string $route Boolean true for current URL, otherwise name of
538     * route to render as URL
539     *
540     * @return string
541     */
542    public function getServerUrl($route = true)
543    {
544        $serverHelper = $this->getViewRenderer()->plugin('serverurl');
545        return $serverHelper(
546            $route === true ? true : $this->url()->fromRoute($route)
547        );
548    }
549
550    /**
551     * Convenience method to make invocation of forward() helper less verbose.
552     *
553     * @param string $controller Controller to invoke
554     * @param string $action     Action to invoke
555     * @param array  $params     Extra parameters for the RouteMatch object (no
556     * need to provide action here, since $action takes care of that)
557     *
558     * @return mixed
559     */
560    public function forwardTo($controller, $action, $params = [])
561    {
562        // Inject action into the RouteMatch parameters
563        $params['action'] = $action;
564
565        // Dispatch the requested controller/action:
566        return $this->forward()->dispatch($controller, $params);
567    }
568
569    /**
570     * Check to see if a form was submitted from its post value
571     * Also validate the Captcha, if it's activated
572     *
573     * @param string|string[]|null $submitElements Name of the post field(s) to check
574     * to indicate a form submission (or null for default)
575     * @param bool                 $useCaptcha     Are we using captcha in this situation?
576     *
577     * @return bool
578     */
579    protected function formWasSubmitted(
580        $submitElements = null,
581        $useCaptcha = false
582    ) {
583        $buttonFound = false;
584        // Use of 'submit' as an input name was deprecated in release 10.0, but the
585        // check is retained for backward compatibility with custom templates.
586        $defaultSubmitElements = ['submitButton', 'submit'];
587        foreach ((array)($submitElements ?? $defaultSubmitElements) as $submitElement) {
588            if ($this->params()->fromPost($submitElement, false)) {
589                $buttonFound = true;
590                break;
591            }
592        }
593        // Fail if all expected submission elements were missing from the POST or
594        // if the form was submitted but expected CAPTCHA does not validate.
595        return $buttonFound && (!$useCaptcha || $this->captcha()->verify());
596    }
597
598    /**
599     * Confirm an action.
600     *
601     * @param string       $title     Title of confirm dialog
602     * @param string       $yesTarget Form target for "confirm" action
603     * @param string       $noTarget  Form target for "cancel" action
604     * @param string|array $messages  Info messages for confirm dialog
605     * @param array        $extras    Extra details to include in form
606     *
607     * @return mixed
608     */
609    public function confirm(
610        $title,
611        $yesTarget,
612        $noTarget,
613        $messages = [],
614        $extras = []
615    ) {
616        return $this->forwardTo(
617            'Confirm',
618            'Confirm',
619            [
620                'data' => [
621                    'title' => $title,
622                    'confirm' => $yesTarget,
623                    'cancel' => $noTarget,
624                    'messages' => (array)$messages,
625                    'extras' => $extras,
626                ],
627            ]
628        );
629    }
630
631    /**
632     * Prevent session writes -- this is designed to be called prior to time-
633     * consuming AJAX operations to help reduce the odds of a timing-related bug
634     * that causes the wrong version of session data to be written to disk (see
635     * VUFIND-716 for more details).
636     *
637     * @return void
638     */
639    protected function disableSessionWrites()
640    {
641        $this->serviceLocator->get(\VuFind\Session\Settings::class)->disableWrite();
642    }
643
644    /**
645     * Get the search memory
646     *
647     * @return \VuFind\Search\Memory
648     */
649    public function getSearchMemory()
650    {
651        return $this->serviceLocator->get(\VuFind\Search\Memory::class);
652    }
653
654    /**
655     * Are comments enabled?
656     *
657     * @return bool
658     */
659    protected function commentsEnabled()
660    {
661        $check = $this->serviceLocator
662            ->get(\VuFind\Config\AccountCapabilities::class);
663        return $check->getCommentSetting() !== 'disabled';
664    }
665
666    /**
667     * Are lists enabled?
668     *
669     * @return bool
670     */
671    protected function listsEnabled()
672    {
673        $check = $this->serviceLocator
674            ->get(\VuFind\Config\AccountCapabilities::class);
675        return $check->getListSetting() !== 'disabled';
676    }
677
678    /**
679     * Are tags enabled?
680     *
681     * @return bool
682     */
683    protected function tagsEnabled()
684    {
685        $check = $this->serviceLocator
686            ->get(\VuFind\Config\AccountCapabilities::class);
687        return $check->getTagSetting() !== 'disabled';
688    }
689
690    /**
691     * Store a referer (if appropriate) to keep post-login redirect pointing
692     * to an appropriate location. This is used when the user clicks the
693     * log in link from an arbitrary page or when a password is mistyped;
694     * separate logic is used for storing followup information when VuFind
695     * forces the user to log in from another context.
696     *
697     * @param bool  $allowCurrentUrl Whether the current URL is valid for followup
698     * @param array $extras          Extra data for the followup
699     *
700     * @return void
701     */
702    protected function setFollowupUrlToReferer(bool $allowCurrentUrl = true, array $extras = [])
703    {
704        // lbreferer is the stored current url of the lightbox
705        // which overrides the url from the server request when present
706        $referer = $this->getRequest()->getQuery()->get(
707            'lbreferer',
708            $this->getRequest()->getServer()->get('HTTP_REFERER', null)
709        );
710        // Get the referer -- if it's empty, there's nothing to store! Also,
711        // if the referer lives outside of VuFind, don't store it! We only
712        // want internal post-login redirects.
713        if (empty($referer) || !$this->isLocalUrl($referer)) {
714            return;
715        }
716        // If the referer is the MyResearch/Home action, it probably means
717        // that the user is repeatedly mistyping their password. We should
718        // ignore this and instead rely on any previously stored referer.
719        $refererNorm = $this->normalizeUrlForComparison($referer);
720        $myResearchHomeUrl = $this->getServerUrl('myresearch-home');
721        $mrhuNorm = $this->normalizeUrlForComparison($myResearchHomeUrl);
722        if ($mrhuNorm === $refererNorm) {
723            return;
724        }
725
726        // If the referer is the MyResearch/UserLogin action, it probably means
727        // that the user is repeatedly mistyping their password. We should
728        // ignore this and instead rely on any previously stored referer.
729        $myUserLogin = $this->getServerUrl('myresearch-userlogin');
730        $mulNorm = $this->normalizeUrlForComparison($myUserLogin);
731        if (str_starts_with($refererNorm, $mulNorm)) {
732            return;
733        }
734
735        // Check that the referer is not current URL if not allowed:
736        if (!$allowCurrentUrl && $this->getRequest()->getUriString() === $referer) {
737            return;
738        }
739
740        // Clear previously stored lightboxParent.
741        $this->followup()->clear('lightboxParent');
742
743        // If we got this far, we want to store the referer:
744        $this->followup()->store($extras, $referer);
745    }
746
747    /**
748     * Normalize the referer URL so that inconsistencies in protocol and trailing
749     * slashes do not break comparisons.
750     *
751     * @param string $url URL to normalize
752     *
753     * @return string
754     */
755    protected function normalizeUrlForComparison($url)
756    {
757        $parts = explode('://', $url, 2);
758        return trim(end($parts), '/');
759    }
760
761    /**
762     * Checks if a followup url is set
763     *
764     * @return bool
765     */
766    protected function hasFollowupUrl()
767    {
768        return null !== $this->followup()->retrieve('url');
769    }
770
771    /**
772     * Retrieve a referer to keep post-login redirect pointing
773     * to an appropriate location.
774     * Unset the followup before returning.
775     *
776     * @param bool $checkRedirect Whether the query should be checked for param 'redirect'
777     *
778     * @return string
779     */
780    protected function getAndClearFollowupUrl($checkRedirect = false)
781    {
782        if ($url = $this->followup()->retrieveAndClear('url')) {
783            $lightboxParent = $this->followup()->retrieveAndClear('lightboxParent');
784            // If a user clicks on the "Your Account" link, we want to be sure
785            // they get to their account rather than being redirected to an old
786            // followup URL. We'll use a redirect=0 GET flag to indicate this:
787            if (!$checkRedirect || $this->params()->fromQuery('redirect', true)) {
788                if (null !== $lightboxParent && !$this->inLightbox()) {
789                    $parentUrl = new \Laminas\Uri\Uri($lightboxParent);
790                    $params = $parentUrl->getQueryAsArray();
791                    $params['lightboxChild'] = $url;
792                    $parentUrl->setQuery($params);
793                    return $parentUrl;
794                }
795                return $url;
796            }
797        }
798        return null;
799    }
800
801    /**
802     * Sometimes we need to unset the followup to trigger default behaviors
803     *
804     * @return void
805     */
806    protected function clearFollowupUrl()
807    {
808        $this->followup()->clear('isReferrer');
809        $this->followup()->clear('lightboxParent');
810        $this->followup()->clear('url');
811    }
812
813    /**
814     * Get the tab configuration for this controller.
815     *
816     * @return \VuFind\RecordTab\TabManager
817     */
818    protected function getRecordTabManager()
819    {
820        return $this->serviceLocator->get(\VuFind\RecordTab\TabManager::class);
821    }
822
823    /**
824     * Are we currently in a lightbox context?
825     *
826     * @return bool
827     */
828    protected function inLightbox()
829    {
830        return
831            $this->params()->fromPost(
832                'layout',
833                $this->params()->fromQuery('layout', false)
834            ) === 'lightbox'
835            || 'layout/lightbox' == $this->layout()->getTemplate();
836    }
837
838    /**
839     * What login method does the ILS use (password, email, vufind)
840     *
841     * @param string $target Login target (MultiILS only)
842     *
843     * @return string
844     */
845    protected function getILSLoginMethod($target = '')
846    {
847        $config = $this->getILS()->checkFunction(
848            'patronLogin',
849            ['patron' => ['cat_username' => "$target.login"]]
850        );
851        return $config['loginMethod'] ?? 'password';
852    }
853
854    /**
855     * Get settings required for displaying the catalog login form
856     *
857     * @return array
858     */
859    protected function getILSLoginSettings()
860    {
861        $targets = null;
862        $defaultTarget = null;
863        $loginMethod = null;
864        $loginMethods = [];
865        // Connect to the ILS and check if multiple target support is available:
866        $catalog = $this->getILS();
867        if ($catalog->checkCapability('getLoginDrivers')) {
868            $targets = $catalog->getLoginDrivers();
869            $defaultTarget = $catalog->getDefaultLoginDriver();
870            foreach ($targets as $t) {
871                $loginMethods[$t] = $this->getILSLoginMethod($t);
872            }
873        } else {
874            $loginMethod = $this->getILSLoginMethod();
875        }
876        return compact('targets', 'defaultTarget', 'loginMethod', 'loginMethods');
877    }
878
879    /**
880     * Construct an HTTP 205 (refresh) response. Useful for reporting success
881     * in the lightbox without actually rendering content.
882     *
883     * @return \Laminas\Http\Response
884     */
885    protected function getRefreshResponse()
886    {
887        $response = $this->getResponse();
888        $response->setStatusCode(205);
889        return $response;
890    }
891
892    /**
893     * Is the provided URL local to this instance?
894     *
895     * @param string $url URL to check
896     *
897     * @return bool
898     */
899    protected function isLocalUrl(string $url): bool
900    {
901        $baseUrlNorm = $this->normalizeUrlForComparison($this->getServerUrl('home'));
902        return str_starts_with($this->normalizeUrlForComparison($url), $baseUrlNorm);
903    }
904}