Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 1088 |
|
0.00% |
0 / 57 |
CRAP | |
0.00% |
0 / 1 |
MyResearchController | |
0.00% |
0 / 1088 |
|
0.00% |
0 / 57 |
90902 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
processAuthenticationException | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
56 | |||
storeRefererForPostLoginRedirect | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
homeAction | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
342 | |||
accountAction | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
56 | |||
loginAction | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
userloginAction | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
completeLoginAction | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
logoutAction | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
30 | |||
getSearchRowSecurely | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
setSavedFlagSecurely | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getUserVerificationContainer | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
scheduleSearch | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
30 | |||
schedulesearchAction | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
56 | |||
isDuplicateOfSavedSearch | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
42 | |||
savesearchAction | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
90 | |||
profileAction | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
210 | |||
addAccountBlocksToFlashMessenger | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
catalogloginAction | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
favoritesAction | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
deleteAction | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
132 | |||
performDeleteFavorite | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
processEditSubmit | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
30 | |||
editAction | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
110 | |||
confirmDeleteFavorite | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
mylistAction | |
0.00% |
0 / 50 |
|
0.00% |
0 / 1 |
110 | |||
processEditList | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
56 | |||
editlistAction | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
110 | |||
emailNotVerifiedAction | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
deletelistAction | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
30 | |||
holdsAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
storageRetrievalRequestsAction | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
90 | |||
illRequestsAction | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
72 | |||
checkedoutAction | |
0.00% |
0 / 71 |
|
0.00% |
0 / 1 |
240 | |||
historicloansAction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
finesAction | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
72 | |||
getSessionInitiator | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
recoverAction | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
56 | |||
sendRecoveryEmail | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
20 | |||
sendFirstVerificationEmail | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
sendChangeNotificationEmail | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
6 | |||
sendVerificationEmail | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
72 | |||
verifyAction | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
20 | |||
verifyEmailAction | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
30 | |||
resetNewPasswordForm | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
newPasswordAction | |
0.00% |
0 / 41 |
|
0.00% |
0 / 1 |
110 | |||
changeEmailAction | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
90 | |||
changePasswordAction | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
deleteLoginTokenAction | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
deleteUserLoginTokensAction | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getHashAge | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
setUpAuthenticationFromRequest | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 | |||
deleteAccountAction | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
42 | |||
unsubscribeAction | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
56 | |||
getPaginationHelper | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
listTagsEnabled | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
addPendingEmailChangeMessage | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * MyResearch Controller |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010. |
9 | * Copyright (C) The National Library of Finland 2023. |
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 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
28 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
29 | * @link https://vufind.org Main Site |
30 | */ |
31 | |
32 | namespace VuFind\Controller; |
33 | |
34 | use DateTime; |
35 | use Exception; |
36 | use Laminas\ServiceManager\ServiceLocatorInterface; |
37 | use Laminas\Session\Container; |
38 | use Laminas\View\Model\ViewModel; |
39 | use VuFind\Account\UserAccountService; |
40 | use VuFind\Auth\ILSAuthenticator; |
41 | use VuFind\Controller\Feature\ListItemSelectionTrait; |
42 | use VuFind\Crypt\SecretCalculator; |
43 | use VuFind\Db\Entity\SearchEntityInterface; |
44 | use VuFind\Db\Entity\UserEntityInterface; |
45 | use VuFind\Db\Service\SearchServiceInterface; |
46 | use VuFind\Db\Service\UserListServiceInterface; |
47 | use VuFind\Db\Service\UserResourceServiceInterface; |
48 | use VuFind\Db\Service\UserServiceInterface; |
49 | use VuFind\Exception\Auth as AuthException; |
50 | use VuFind\Exception\AuthEmailNotVerified as AuthEmailNotVerifiedException; |
51 | use VuFind\Exception\AuthInProgress as AuthInProgressException; |
52 | use VuFind\Exception\BadRequest as BadRequestException; |
53 | use VuFind\Exception\Forbidden as ForbiddenException; |
54 | use VuFind\Exception\ListPermission as ListPermissionException; |
55 | use VuFind\Exception\LoginRequired as LoginRequiredException; |
56 | use VuFind\Exception\Mail as MailException; |
57 | use VuFind\Exception\MissingField as MissingFieldException; |
58 | use VuFind\Favorites\FavoritesService; |
59 | use VuFind\ILS\PaginationHelper; |
60 | use VuFind\Mailer\Mailer; |
61 | use VuFind\Search\RecommendListener; |
62 | use VuFind\Tags\TagsService; |
63 | use VuFind\Validator\CsrfInterface; |
64 | |
65 | use function count; |
66 | use function in_array; |
67 | use function intval; |
68 | use function is_array; |
69 | use function is_object; |
70 | |
71 | /** |
72 | * Controller for the user account area. |
73 | * |
74 | * @category VuFind |
75 | * @package Controller |
76 | * @author Demian Katz <demian.katz@villanova.edu> |
77 | * @author Ere Maijala <ere.maijala@helsinki.fi> |
78 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
79 | * @link https://vufind.org Main Site |
80 | */ |
81 | class MyResearchController extends AbstractBase |
82 | { |
83 | use Feature\BulkActionControllerTrait; |
84 | use Feature\CatchIlsExceptionsTrait; |
85 | use \VuFind\ILS\Logic\SummaryTrait; |
86 | use ListItemSelectionTrait; |
87 | |
88 | /** |
89 | * Configuration loader |
90 | * |
91 | * @var \VuFind\Config\PluginManager |
92 | */ |
93 | protected $configLoader; |
94 | |
95 | /** |
96 | * Permission that must be granted to access this module (false for no |
97 | * restriction, null to use configured default (which is usually the same |
98 | * as false)). |
99 | * |
100 | * For this controller, we default to false rather than null because |
101 | * we don't want a default setting to override the controller's accessibility |
102 | * and break the login process! |
103 | * |
104 | * @var string|bool |
105 | */ |
106 | protected $accessPermission = false; |
107 | |
108 | /** |
109 | * Export support class |
110 | * |
111 | * @var \VuFind\Export |
112 | */ |
113 | protected $export; |
114 | |
115 | /** |
116 | * ILS Pagination Helper |
117 | * |
118 | * @var PaginationHelper |
119 | */ |
120 | protected $paginationHelper = null; |
121 | |
122 | /** |
123 | * Session container |
124 | * |
125 | * @var Container |
126 | */ |
127 | protected $session; |
128 | |
129 | /** |
130 | * Constructor |
131 | * |
132 | * @param ServiceLocatorInterface $sm Service locator |
133 | * @param Container $container Session container |
134 | * @param \VuFind\Config\PluginManager $configLoader Configuration loader |
135 | * @param \VuFind\Export $export Export support class |
136 | */ |
137 | public function __construct( |
138 | ServiceLocatorInterface $sm, |
139 | Container $container, |
140 | \VuFind\Config\PluginManager $configLoader, |
141 | \VuFind\Export $export |
142 | ) { |
143 | parent::__construct($sm); |
144 | $this->session = $container; |
145 | $this->configLoader = $configLoader; |
146 | $this->export = $export; |
147 | } |
148 | |
149 | /** |
150 | * Process an authentication error. |
151 | * |
152 | * @param AuthException $e Exception to process. |
153 | * |
154 | * @return void |
155 | */ |
156 | protected function processAuthenticationException(AuthException $e) |
157 | { |
158 | $msg = $e->getMessage(); |
159 | if ($e instanceof AuthInProgressException) { |
160 | $this->flashMessenger()->addSuccessMessage($msg); |
161 | return; |
162 | } |
163 | if ($e instanceof AuthEmailNotVerifiedException) { |
164 | $this->sendFirstVerificationEmail($e->getUser()); |
165 | if ($msg == 'authentication_error_email_not_verified_html') { |
166 | $this->getUserVerificationContainer()->user = $e->getUser()->getUsername(); |
167 | $url = $this->url()->fromRoute('myresearch-emailnotverified') |
168 | . '?reverify=true'; |
169 | $msg = [ |
170 | 'html' => true, |
171 | 'msg' => $msg, |
172 | 'tokens' => ['%%url%%' => $url], |
173 | ]; |
174 | } |
175 | } |
176 | // If a Shibboleth-style login has failed and the user just logged |
177 | // out, we need to override the error message with a more relevant |
178 | // one: |
179 | if ( |
180 | $msg == 'authentication_error_admin' |
181 | && $this->getAuthManager()->userHasLoggedOut() |
182 | && $this->getSessionInitiator() |
183 | ) { |
184 | $msg = 'authentication_error_loggedout'; |
185 | } |
186 | $this->flashMessenger()->addMessage($msg, 'error'); |
187 | } |
188 | |
189 | /** |
190 | * Maintaining this method for backwards compatibility; |
191 | * logic moved to parent and method re-named |
192 | * |
193 | * @return void |
194 | */ |
195 | protected function storeRefererForPostLoginRedirect() |
196 | { |
197 | $this->setFollowupUrlToReferer(); |
198 | } |
199 | |
200 | /** |
201 | * Prepare and direct the home page where it needs to go |
202 | * |
203 | * @return mixed |
204 | */ |
205 | public function homeAction() |
206 | { |
207 | // Process login request, if necessary (either because a form has been |
208 | // submitted or because we're using an external login provider): |
209 | if ( |
210 | $this->params()->fromPost('processLogin') |
211 | || $this->getSessionInitiator() |
212 | || $this->params()->fromPost('auth_method') |
213 | || $this->params()->fromQuery('auth_method') |
214 | ) { |
215 | try { |
216 | if (!$this->getAuthManager()->getIdentity()) { |
217 | $this->getAuthManager()->login($this->getRequest()); |
218 | // Return early to avoid unnecessary processing if we are being |
219 | // called from login lightbox and don't have a followup action or |
220 | // followup is set to referrer. |
221 | if ( |
222 | $this->params()->fromPost('processLogin') |
223 | && $this->inLightbox() |
224 | && (!$this->hasFollowupUrl() |
225 | || $this->followup()->retrieve('isReferrer') === true) |
226 | ) { |
227 | $this->clearFollowupUrl(); |
228 | return $this->getRefreshResponse(); |
229 | } |
230 | } |
231 | } catch (AuthException $e) { |
232 | $this->processAuthenticationException($e); |
233 | } |
234 | } |
235 | |
236 | // Not logged in? Force user to log in: |
237 | if (!$this->getAuthManager()->getIdentity()) { |
238 | if ( |
239 | $this->followup()->retrieve('lightboxParent') |
240 | && $url = $this->getAndClearFollowupUrl(true) |
241 | ) { |
242 | return $this->redirect()->toUrl($url); |
243 | } |
244 | |
245 | // Allow bypassing of post-login redirect |
246 | if ($this->params()->fromQuery('redirect', true)) { |
247 | $this->setFollowupUrlToReferer(); |
248 | } |
249 | return $this->forwardTo('MyResearch', 'Login'); |
250 | } |
251 | // Logged in? Forward user to followup action |
252 | // or default action (if no followup provided): |
253 | if ($url = $this->getAndClearFollowupUrl(true)) { |
254 | return $this->redirect()->toUrl($url); |
255 | } |
256 | |
257 | $config = $this->getConfig(); |
258 | $page = $config->Site->defaultAccountPage ?? 'Favorites'; |
259 | |
260 | // Default to search history if favorites are disabled: |
261 | if ($page == 'Favorites' && !$this->listsEnabled()) { |
262 | return $this->forwardTo('Search', 'History'); |
263 | } |
264 | return $this->forwardTo('MyResearch', $page); |
265 | } |
266 | |
267 | /** |
268 | * "Create account" action |
269 | * |
270 | * @return mixed |
271 | */ |
272 | public function accountAction() |
273 | { |
274 | // If the user is already logged in, don't let them create an account: |
275 | if ($this->getAuthManager()->getIdentity()) { |
276 | return $this->redirect()->toRoute('myresearch-home'); |
277 | } |
278 | // If authentication mechanism does not support account creation, send |
279 | // the user away! |
280 | $method = trim($this->params()->fromQuery('auth_method')); |
281 | if (!$this->getAuthManager()->supportsCreation($method)) { |
282 | return $this->forwardTo('MyResearch', 'Home'); |
283 | } |
284 | |
285 | // If there's already a followup url, keep it; otherwise set one. |
286 | if (!$this->hasFollowupUrl()) { |
287 | $this->setFollowupUrlToReferer(); |
288 | } |
289 | |
290 | // Make view |
291 | $view = $this->createViewModel(); |
292 | // Username policy |
293 | $view->usernamePolicy = $this->getAuthManager()->getUsernamePolicy($method); |
294 | // Password policy |
295 | $view->passwordPolicy = $this->getAuthManager()->getPasswordPolicy($method); |
296 | // Set up Captcha |
297 | $view->useCaptcha = $this->captcha()->active('newAccount'); |
298 | // Pass request to view so we can repopulate user parameters in form: |
299 | $view->request = $this->getRequest()->getPost(); |
300 | // Process request, if necessary: |
301 | if ($this->formWasSubmitted(useCaptcha: $view->useCaptcha)) { |
302 | try { |
303 | $this->getAuthManager()->create($this->getRequest()); |
304 | return $this->forwardTo('MyResearch', 'Home'); |
305 | } catch (AuthEmailNotVerifiedException $e) { |
306 | $this->sendFirstVerificationEmail($e->getUser()); |
307 | return $this->redirect()->toRoute('myresearch-emailnotverified'); |
308 | } catch (AuthException $e) { |
309 | $this->flashMessenger()->addMessage($e->getMessage(), 'error'); |
310 | } |
311 | } else { |
312 | // If we are not processing a submission, we need to simply display |
313 | // an empty form. In case ChoiceAuth is being used, we may need to |
314 | // override the active authentication method based on request |
315 | // parameters to ensure display of the appropriate template. |
316 | $this->setUpAuthenticationFromRequest(); |
317 | } |
318 | return $view; |
319 | } |
320 | |
321 | /** |
322 | * Login Action |
323 | * |
324 | * @return mixed |
325 | */ |
326 | public function loginAction() |
327 | { |
328 | // If this authentication method doesn't use a VuFind-generated login |
329 | // form, force it through: |
330 | if ($this->getSessionInitiator()) { |
331 | // Don't get stuck in an infinite loop -- if processLogin is already |
332 | // set, it probably means Home action is forwarding back here to |
333 | // report an error! |
334 | // |
335 | // Also don't attempt to process a login that hasn't happened yet; |
336 | // if we've just been forced here from another page, we need the user |
337 | // to click the session initiator link before anything can happen. |
338 | if ( |
339 | !$this->params()->fromPost('processLogin', false) |
340 | && !$this->params()->fromPost('forcingLogin', false) |
341 | ) { |
342 | $this->getRequest()->getPost()->set('processLogin', true); |
343 | return $this->forwardTo('MyResearch', 'Home'); |
344 | } |
345 | } |
346 | |
347 | // Make request available to view for form updating: |
348 | $view = $this->createViewModel(); |
349 | $view->request = $this->getRequest()->getPost(); |
350 | return $view; |
351 | } |
352 | |
353 | /** |
354 | * User login action -- clear any previous follow-up information prior to |
355 | * triggering a login process. This is used for explicit login links within |
356 | * the UI to differentiate them from contextual login links that are triggered |
357 | * by attempting to access protected actions. |
358 | * |
359 | * @return mixed |
360 | */ |
361 | public function userloginAction() |
362 | { |
363 | // Don't log in if already logged in! |
364 | if ($this->getAuthManager()->getIdentity()) { |
365 | return $this->inLightbox() // different behavior for lightbox context |
366 | ? $this->getRefreshResponse() |
367 | : $this->redirect()->toRoute('home'); |
368 | } |
369 | $this->clearFollowupUrl(); |
370 | // Set followup with the isReferrer flag so that the post-login process |
371 | // can decide whether to use it: |
372 | $this->setFollowupUrlToReferer(true, ['isReferrer' => true]); |
373 | |
374 | if ($si = $this->getSessionInitiator()) { |
375 | return $this->redirect()->toUrl($si); |
376 | } |
377 | return $this->forwardTo('MyResearch', 'Login'); |
378 | } |
379 | |
380 | /** |
381 | * Complete login - perform a user login followed by a catalog login. |
382 | * |
383 | * @return mixed |
384 | */ |
385 | public function completeLoginAction() |
386 | { |
387 | if (!$this->getAuthManager()->getIdentity()) { |
388 | return $this->forceLogin(''); |
389 | } |
390 | if (!is_array($patron = $this->catalogLogin())) { |
391 | return $patron; |
392 | } |
393 | return $this->inLightbox() |
394 | ? $this->getRefreshResponse() |
395 | : $this->redirect()->toRoute('home'); |
396 | } |
397 | |
398 | /** |
399 | * Logout Action |
400 | * |
401 | * @return mixed |
402 | */ |
403 | public function logoutAction() |
404 | { |
405 | $config = $this->getConfig(); |
406 | if (!empty($config->Site->logOutRoute)) { |
407 | $logoutTarget = $this->getServerUrl($config->Site->logOutRoute); |
408 | } else { |
409 | $logoutTarget = $this->getRequest()->getServer()->get('HTTP_REFERER'); |
410 | if (empty($logoutTarget) || !$this->isLocalUrl($logoutTarget)) { |
411 | $logoutTarget = $this->getServerUrl('home'); |
412 | } |
413 | |
414 | // If there is an auth_method parameter in the query, we should strip |
415 | // it out. Otherwise, the user may get stuck in an infinite loop of |
416 | // logging out and getting logged back in when using environment-based |
417 | // authentication methods like Shibboleth. |
418 | $logoutTarget = preg_replace( |
419 | '/([?&])auth_method=[^&]*&?/', |
420 | '$1', |
421 | $logoutTarget |
422 | ); |
423 | $logoutTarget = rtrim($logoutTarget, '?'); |
424 | |
425 | // Another special case: if logging out will send the user back to |
426 | // the MyResearch home action, instead send them all the way to |
427 | // VuFind home. Otherwise, they might get logged back in again, |
428 | // which is confusing. Even in the best scenario, they'll just end |
429 | // up on a login screen, which is not helpful. |
430 | if ($logoutTarget == $this->getServerUrl('myresearch-home')) { |
431 | $logoutTarget = $this->getServerUrl('home'); |
432 | } |
433 | } |
434 | |
435 | return $this->redirect() |
436 | ->toUrl($this->getAuthManager()->logout($logoutTarget)); |
437 | } |
438 | |
439 | /** |
440 | * Get a search row, but throw an exception if it is not owned by the specified |
441 | * user or current active session. |
442 | * |
443 | * @param int $searchId ID of search row |
444 | * @param int $userId ID of active user |
445 | * |
446 | * @throws ForbiddenException |
447 | * @return SearchEntityInterface |
448 | */ |
449 | protected function getSearchRowSecurely($searchId, $userId) |
450 | { |
451 | $sessId = $this->serviceLocator |
452 | ->get(\Laminas\Session\SessionManager::class)->getId(); |
453 | $search = $this->getDbService(SearchServiceInterface::class) |
454 | ->getSearchByIdAndOwner($searchId, $sessId, $userId); |
455 | if (empty($search)) { |
456 | throw new ForbiddenException('Access denied.'); |
457 | } |
458 | return $search; |
459 | } |
460 | |
461 | /** |
462 | * Support method for savesearchAction(): set the saved flag in a secure |
463 | * fashion, throwing an exception if somebody attempts something invalid. |
464 | * |
465 | * @param int $searchId The search ID to save/unsave |
466 | * @param bool $saved The new desired state of the saved flag |
467 | * @param int $userId The user ID requesting the change |
468 | * |
469 | * @throws \Exception |
470 | * @return void |
471 | */ |
472 | protected function setSavedFlagSecurely($searchId, $saved, $userId) |
473 | { |
474 | $row = $this->getSearchRowSecurely($searchId, $userId); |
475 | $row->saved = $saved ? 1 : 0; |
476 | if (!$saved) { |
477 | $row->notification_frequency = 0; |
478 | } |
479 | $row->user_id = $userId; |
480 | $row->save(); |
481 | } |
482 | |
483 | /** |
484 | * Return a session container for use in user email verification. |
485 | * |
486 | * @return \Laminas\Session\Container |
487 | */ |
488 | protected function getUserVerificationContainer() |
489 | { |
490 | return new \Laminas\Session\Container( |
491 | 'user_verification', |
492 | $this->serviceLocator->get(\Laminas\Session\SessionManager::class) |
493 | ); |
494 | } |
495 | |
496 | /** |
497 | * Support method for savesearchAction() -- schedule a search. |
498 | * |
499 | * @param UserEntityInterface $user Logged-in user object |
500 | * @param int $schedule Requested schedule setting |
501 | * @param int $sid Search ID to schedule |
502 | * |
503 | * @return mixed |
504 | */ |
505 | protected function scheduleSearch(UserEntityInterface $user, $schedule, $sid) |
506 | { |
507 | // Fail if scheduled searches are disabled. |
508 | $scheduleOptions = $this->serviceLocator |
509 | ->get(\VuFind\Search\History::class) |
510 | ->getScheduleOptions(); |
511 | if (!isset($scheduleOptions[$schedule])) { |
512 | throw new ForbiddenException('Illegal schedule option: ' . $schedule); |
513 | } |
514 | $baseurl = rtrim($this->getServerUrl('home'), '/'); |
515 | $userId = $user->getId(); |
516 | $savedRow = $this->getSearchRowSecurely($sid, $userId); |
517 | |
518 | // In case the user has just logged in, let's deduplicate... |
519 | $sessId = $this->serviceLocator |
520 | ->get(\Laminas\Session\SessionManager::class)->getId(); |
521 | $duplicateId = $this->isDuplicateOfSavedSearch( |
522 | $savedRow, |
523 | $sessId, |
524 | $userId |
525 | ); |
526 | if ($duplicateId) { |
527 | $savedRow->delete(); |
528 | $sid = $duplicateId; |
529 | $savedRow = $this->getSearchRowSecurely($sid, $userId); |
530 | } |
531 | |
532 | // If we didn't find an already-saved row, let's save and retry: |
533 | if (!($savedRow->saved ?? false)) { |
534 | $this->setSavedFlagSecurely($sid, true, $userId); |
535 | $savedRow = $this->getSearchRowSecurely($sid, $userId); |
536 | } |
537 | if (!($this->getConfig()->Account->force_first_scheduled_email ?? false)) { |
538 | // By default, a first scheduled email will be sent because the database |
539 | // last notification date will be initialized to a past date. If we don't |
540 | // want that to happen, we need to set it to a more appropriate date: |
541 | $savedRow->setLastNotificationSent(new DateTime()); |
542 | } |
543 | $savedRow->setNotificationFrequency($schedule); |
544 | $savedRow->setNotificationBaseUrl($baseurl); |
545 | $this->getDbService(SearchServiceInterface::class)->persistEntity($savedRow); |
546 | return $this->redirect()->toRoute('search-history'); |
547 | } |
548 | |
549 | /** |
550 | * Handle search subscription request |
551 | * |
552 | * @return mixed |
553 | */ |
554 | public function schedulesearchAction() |
555 | { |
556 | // Fail if saved searches or subscriptions are disabled. |
557 | $check = $this->serviceLocator |
558 | ->get(\VuFind\Config\AccountCapabilities::class); |
559 | if ($check->getSavedSearchSetting() === 'disabled') { |
560 | throw new ForbiddenException('Saved searches disabled.'); |
561 | } |
562 | $scheduleOptions = $this->serviceLocator |
563 | ->get(\VuFind\Search\History::class) |
564 | ->getScheduleOptions(); |
565 | if (empty($scheduleOptions)) { |
566 | throw new ForbiddenException('Scheduled searches disabled.'); |
567 | } |
568 | // Fail if search ID is missing. |
569 | $searchId = $this->params()->fromQuery('searchid', false); |
570 | if (!$searchId) { |
571 | throw new BadRequestException('searchid missing'); |
572 | } |
573 | // Require login. |
574 | if (!($user = $this->getUser())) { |
575 | return $this->forceLogin(); |
576 | } |
577 | // Get the row, and fail if the current user doesn't own it. |
578 | $search = $this->getSearchRowSecurely($searchId, $user->getId()); |
579 | |
580 | // If the user has just logged in, the search might be a duplicate; if |
581 | // so, let's switch to the pre-existing version instead. |
582 | $sessId = $this->serviceLocator->get(\Laminas\Session\SessionManager::class)->getId(); |
583 | $duplicateId = $this->isDuplicateOfSavedSearch( |
584 | $search, |
585 | $sessId, |
586 | $user->getId() |
587 | ); |
588 | if ($duplicateId) { |
589 | $search->delete(); |
590 | $this->redirect()->toRoute( |
591 | 'myresearch-schedulesearch', |
592 | [], |
593 | ['query' => ['searchid' => $duplicateId]] |
594 | ); |
595 | } |
596 | |
597 | // Now fetch all the results: |
598 | $resultsManager = $this->serviceLocator->get(\VuFind\Search\Results\PluginManager::class); |
599 | $results = $search->getSearchObject()?->deminify($resultsManager); |
600 | if (!$results) { |
601 | throw new Exception("Problem getting search object from search {$search->getId()}."); |
602 | } |
603 | |
604 | // Build the form. |
605 | return $this->createViewModel( |
606 | compact('scheduleOptions', 'search', 'results') |
607 | ); |
608 | } |
609 | |
610 | /** |
611 | * Is the provided search row a duplicate of a search that is already saved? |
612 | * |
613 | * @param ?SearchEntityInterface $rowToCheck Search row to check (if any) |
614 | * @param string $sessId Current session ID |
615 | * @param int $userId Current user ID |
616 | * |
617 | * @return ?int |
618 | */ |
619 | protected function isDuplicateOfSavedSearch( |
620 | ?SearchEntityInterface $rowToCheck, |
621 | string $sessId, |
622 | int $userId |
623 | ): ?int { |
624 | if (!$rowToCheck) { |
625 | return null; |
626 | } |
627 | $normalizer = $this->serviceLocator->get(\VuFind\Search\SearchNormalizer::class); |
628 | $searchObject = $rowToCheck->getSearchObject(); |
629 | if (!$searchObject) { |
630 | throw new Exception("Problem getting search object from search {$rowToCheck->getId()}."); |
631 | } |
632 | $normalized = $normalizer->normalizeMinifiedSearch($searchObject); |
633 | $matches = $normalizer->getSearchesMatchingNormalizedSearch( |
634 | $normalized, |
635 | $sessId, |
636 | $userId |
637 | ); |
638 | foreach ($matches as $current) { |
639 | if ($current->getSaved() && $current->getId() !== $rowToCheck->getId()) { |
640 | return $current->getId(); |
641 | } |
642 | } |
643 | return null; |
644 | } |
645 | |
646 | /** |
647 | * Handle 'save/unsave search' request |
648 | * |
649 | * @return mixed |
650 | */ |
651 | public function savesearchAction() |
652 | { |
653 | // Fail if saved searches are disabled. |
654 | $check = $this->serviceLocator |
655 | ->get(\VuFind\Config\AccountCapabilities::class); |
656 | if ($check->getSavedSearchSetting() === 'disabled') { |
657 | throw new ForbiddenException('Saved searches disabled.'); |
658 | } |
659 | |
660 | if (!($user = $this->getUser())) { |
661 | return $this->forceLogin(); |
662 | } |
663 | |
664 | // Check for schedule-related parameters and process them first: |
665 | $schedule = $this->params()->fromQuery('schedule', false); |
666 | $sid = $this->params()->fromQuery('searchid', false); |
667 | if ($schedule !== false && $sid !== false) { |
668 | return $this->scheduleSearch($user, $schedule, $sid); |
669 | } |
670 | |
671 | // Check for the save / delete parameters and process them appropriately: |
672 | if (($id = $this->params()->fromQuery('save', false)) !== false) { |
673 | // If the row the user is trying to save is a duplicate of an already- |
674 | // saved row, we should just delete the duplicate. (This can happen if |
675 | // the user clicks "save" before logging in, then logs in during the |
676 | // save process, but has the same search already saved in their account). |
677 | $searchService = $this->getDbService(SearchServiceInterface::class); |
678 | $sessId = $this->serviceLocator |
679 | ->get(\Laminas\Session\SessionManager::class)->getId(); |
680 | $rowToCheck = $searchService->getSearchByIdAndOwner($id, $sessId, $user); |
681 | $duplicateId = $this->isDuplicateOfSavedSearch( |
682 | $rowToCheck, |
683 | $sessId, |
684 | $user->getId() |
685 | ); |
686 | if ($duplicateId) { |
687 | $rowToCheck->delete(); |
688 | $id = $duplicateId; |
689 | } else { |
690 | $this->setSavedFlagSecurely($id, true, $user->getId()); |
691 | } |
692 | $this->flashMessenger()->addMessage('search_save_success', 'success'); |
693 | } elseif (($id = $this->params()->fromQuery('delete', false)) !== false) { |
694 | $this->setSavedFlagSecurely($id, false, $user->getId()); |
695 | $this->flashMessenger()->addMessage('search_unsave_success', 'success'); |
696 | } else { |
697 | throw new \Exception('Missing save and delete parameters.'); |
698 | } |
699 | |
700 | // Forward to the appropriate place: |
701 | if ($this->params()->fromQuery('mode') == 'history') { |
702 | return $this->redirect()->toRoute('search-history'); |
703 | } else { |
704 | // Forward to the Search/Results action with the "saved" parameter set; |
705 | // this will in turn redirect the user to the appropriate results screen. |
706 | $this->getRequest()->getQuery()->set('saved', $id); |
707 | return $this->forwardTo('Search', 'Results'); |
708 | } |
709 | } |
710 | |
711 | /** |
712 | * Gather user profile data |
713 | * |
714 | * @return mixed |
715 | */ |
716 | public function profileAction() |
717 | { |
718 | if (!($user = $this->getUser())) { |
719 | return $this->forceLogin(); |
720 | } |
721 | |
722 | // Begin building view object: |
723 | $view = $this->createViewModel(['user' => $user]); |
724 | |
725 | $config = $this->getConfig(); |
726 | $allowHomeLibrary = $config->Account->set_home_library ?? true; |
727 | |
728 | $patron = $this->catalogLogin(); |
729 | if (is_array($patron)) { |
730 | // Process home library parameter (if present and allowed): |
731 | $homeLibrary = $this->params()->fromPost('home_library'); |
732 | if ($allowHomeLibrary && null !== $homeLibrary) { |
733 | // Note: for backward compatibility user's home library defaults to |
734 | // empty string indicating system default. We also allow null for |
735 | // "Always ask me", and the choice is encoded as ' ** ' on the form: |
736 | if (' ** ' === $homeLibrary) { |
737 | $homeLibrary = null; |
738 | } |
739 | $this->serviceLocator->get(ILSAuthenticator::class)->updateUserHomeLibrary($user, $homeLibrary); |
740 | $this->flashMessenger()->addMessage('profile_update', 'success'); |
741 | } |
742 | |
743 | // Obtain user information from ILS: |
744 | $catalog = $this->getILS(); |
745 | $this->addAccountBlocksToFlashMessenger($catalog, $patron); |
746 | $profile = $catalog->getMyProfile($patron); |
747 | $profile['home_library'] = $allowHomeLibrary |
748 | ? $user->getHomeLibrary() |
749 | : ($profile['home_library'] ?? ''); |
750 | $view->profile = $profile; |
751 | $pickup = $defaultPickupLocation = null; |
752 | try { |
753 | $pickup = $catalog->getPickUpLocations($patron); |
754 | $defaultPickupLocation = $catalog->getDefaultPickUpLocation($patron); |
755 | } catch (\Exception $e) { |
756 | // Do nothing; if we're unable to load information about pickup |
757 | // locations, they are not supported and we should ignore them. |
758 | } |
759 | |
760 | // Set things up differently depending on whether or not the user is |
761 | // allowed to set a home library. |
762 | if ($allowHomeLibrary) { |
763 | $view->pickup = $pickup; |
764 | $view->defaultPickupLocation = $defaultPickupLocation; |
765 | } elseif (!empty($pickup)) { |
766 | foreach ($pickup as $lib) { |
767 | if ($defaultPickupLocation == $lib['locationID']) { |
768 | $view->preferredLibraryDisplay = $lib['locationDisplay']; |
769 | break; |
770 | } |
771 | } |
772 | } |
773 | |
774 | // Add proxy details if available |
775 | if ($catalog->checkCapability('getProxiedUsers', [$patron])) { |
776 | $view->proxiedUsers = $catalog->getProxiedUsers($patron); |
777 | } |
778 | if ($catalog->checkCapability('getProxyingUsers', [$patron])) { |
779 | $view->proxyingUsers = $catalog->getProxyingUsers($patron); |
780 | } |
781 | } else { |
782 | $view->patronLoginView = $patron; |
783 | // Turn off account menu in embedded login display: |
784 | $view->patronLoginView->showMenu = false; |
785 | } |
786 | |
787 | $view->accountDeletion |
788 | = !empty($config->Authentication->account_deletion); |
789 | |
790 | $this->addPendingEmailChangeMessage($user); |
791 | |
792 | return $view; |
793 | } |
794 | |
795 | /** |
796 | * Add account blocks to the flash messenger as errors. |
797 | * These messages are lightbox ignored. |
798 | * |
799 | * @param \VuFind\ILS\Connection $catalog Catalog connection |
800 | * @param array $patron Patron details |
801 | * |
802 | * @return void |
803 | */ |
804 | public function addAccountBlocksToFlashMessenger($catalog, $patron) |
805 | { |
806 | if ( |
807 | $catalog->checkCapability('getAccountBlocks', compact('patron')) |
808 | && $blocks = $catalog->getAccountBlocks($patron) |
809 | ) { |
810 | foreach ($blocks as $block) { |
811 | $this->flashMessenger()->addMessage( |
812 | [ 'msg' => $block, 'dataset' => [ 'lightbox-ignore' => '1' ] ], |
813 | 'error' |
814 | ); |
815 | } |
816 | } |
817 | } |
818 | |
819 | /** |
820 | * Catalog Login Action |
821 | * |
822 | * @return mixed |
823 | */ |
824 | public function catalogloginAction() |
825 | { |
826 | $loginSettings = $this->getILSLoginSettings(); |
827 | return $this->createViewModel($loginSettings); |
828 | } |
829 | |
830 | /** |
831 | * Action for sending all of a user's saved favorites to the view |
832 | * |
833 | * @return mixed |
834 | */ |
835 | public function favoritesAction() |
836 | { |
837 | // Check permission: |
838 | $response = $this->permission()->check('feature.Favorites', false); |
839 | if (is_object($response)) { |
840 | return $response; |
841 | } |
842 | |
843 | // Favorites is the same as MyList, but without the list ID parameter. |
844 | return $this->forwardTo('MyResearch', 'MyList'); |
845 | } |
846 | |
847 | /** |
848 | * Delete group of records from favorites. |
849 | * |
850 | * @return mixed |
851 | */ |
852 | public function deleteAction() |
853 | { |
854 | // Force login: |
855 | if (!($user = $this->getUser())) { |
856 | return $this->forceLogin(); |
857 | } |
858 | |
859 | // Get target URL for after deletion: |
860 | $listID = $this->params()->fromPost('listID'); |
861 | $newUrl = empty($listID) |
862 | ? $this->url()->fromRoute('myresearch-favorites') |
863 | : $this->url()->fromRoute('userList', ['id' => $listID]); |
864 | |
865 | // Fail if we have nothing to delete: |
866 | $ids = $this->getSelectedIds(); |
867 | |
868 | $actionLimit = $this->getBulkActionLimit('delete'); |
869 | if (!is_array($ids) || empty($ids)) { |
870 | if ($redirect = $this->redirectToSource('error', 'bulk_noitems_advice')) { |
871 | return $redirect; |
872 | } |
873 | } elseif (count($ids) > $actionLimit) { |
874 | $errorMsg = $this->translate( |
875 | 'bulk_limit_exceeded', |
876 | ['%%count%%' => count($ids), '%%limit%%' => $actionLimit], |
877 | ); |
878 | if ($redirect = $this->redirectToSource('error', $errorMsg)) { |
879 | return $redirect; |
880 | } |
881 | } elseif ($this->formWasSubmitted()) { |
882 | $this->serviceLocator->get(FavoritesService::class) |
883 | ->deleteFavorites($ids, $listID === null ? null : (int)$listID, $user); |
884 | $this->flashMessenger()->addMessage('fav_delete_success', 'success'); |
885 | return $this->redirect()->toUrl($newUrl); |
886 | } |
887 | |
888 | // If we got this far, the operation has not been confirmed yet; show |
889 | // the necessary dialog box: |
890 | $list = empty($listID) |
891 | ? false |
892 | : $this->getDbService(UserListServiceInterface::class)->getUserListById($listID); |
893 | return $this->createViewModel( |
894 | [ |
895 | 'list' => $list, 'deleteIDS' => $ids, |
896 | 'records' => $this->getRecordLoader()->loadBatch($ids), |
897 | ] |
898 | ); |
899 | } |
900 | |
901 | /** |
902 | * Delete record |
903 | * |
904 | * @param string $id ID of record to delete |
905 | * @param string $source Source of record to delete |
906 | * |
907 | * @return mixed True on success; otherwise returns a value that can |
908 | * be returned by the controller to forward to another action (i.e. force login) |
909 | */ |
910 | public function performDeleteFavorite($id, $source) |
911 | { |
912 | // Force login: |
913 | if (!($user = $this->getUser())) { |
914 | return $this->forceLogin(); |
915 | } |
916 | |
917 | // Load/check incoming parameters: |
918 | $listID = $this->params()->fromRoute('id'); |
919 | $listID = empty($listID) ? null : $listID; |
920 | if (empty($id)) { |
921 | throw new \Exception('Cannot delete empty ID!'); |
922 | } |
923 | |
924 | // Perform delete and send appropriate flash message: |
925 | $favoritesService = $this->serviceLocator->get(FavoritesService::class); |
926 | if (null !== $listID) { |
927 | // ...Specific List |
928 | $list = $this->getDbService(UserListServiceInterface::class)->getUserListById($listID); |
929 | $favoritesService->removeListResourcesById($list, $user, [$id], $source); |
930 | $this->flashMessenger()->addMessage('Item removed from list', 'success'); |
931 | } else { |
932 | // ...All Saved Items |
933 | $favoritesService->removeUserResourcesById($user, [$id], $source); |
934 | $this->flashMessenger()->addMessage('Item removed from favorites', 'success'); |
935 | } |
936 | |
937 | // All done -- return true to indicate success. |
938 | return true; |
939 | } |
940 | |
941 | /** |
942 | * Process the submission of the edit favorite form. |
943 | * |
944 | * @param UserEntityInterface $user Logged-in user |
945 | * @param \VuFind\RecordDriver\AbstractBase $driver Record driver for favorite |
946 | * @param int $listID List being edited (null |
947 | * if editing all favorites) |
948 | * |
949 | * @return object |
950 | */ |
951 | protected function processEditSubmit(UserEntityInterface $user, $driver, $listID) |
952 | { |
953 | $lists = $this->params()->fromPost('lists', []); |
954 | $tagsService = $this->serviceLocator->get(\VuFind\Tags\TagsService::class); |
955 | $favorites = $this->serviceLocator |
956 | ->get(\VuFind\Favorites\FavoritesService::class); |
957 | $didSomething = false; |
958 | foreach ($lists as $list) { |
959 | $tags = $this->params()->fromPost('tags' . $list); |
960 | $favorites->save( |
961 | [ |
962 | 'list' => $list, |
963 | 'mytags' => $tagsService->parse($tags), |
964 | 'notes' => $this->params()->fromPost('notes' . $list), |
965 | ], |
966 | $user, |
967 | $driver |
968 | ); |
969 | $didSomething = true; |
970 | } |
971 | // add to a new list? |
972 | $addToList = $this->params()->fromPost('addToList'); |
973 | if ($addToList > -1) { |
974 | $didSomething = true; |
975 | $favorites->save(['list' => $addToList], $user, $driver); |
976 | } |
977 | if ($didSomething) { |
978 | $this->flashMessenger()->addMessage('edit_list_success', 'success'); |
979 | } |
980 | |
981 | $newUrl = null === $listID |
982 | ? $this->url()->fromRoute('myresearch-favorites') |
983 | : $this->url()->fromRoute('userList', ['id' => $listID]); |
984 | return $this->redirect()->toUrl($newUrl); |
985 | } |
986 | |
987 | /** |
988 | * Edit record |
989 | * |
990 | * @return mixed |
991 | */ |
992 | public function editAction() |
993 | { |
994 | // Force login: |
995 | if (!($user = $this->getUser())) { |
996 | return $this->forceLogin(); |
997 | } |
998 | |
999 | // Get current record (and, if applicable, selected list ID) for convenience: |
1000 | $id = $this->params()->fromPost('id', $this->params()->fromQuery('id')); |
1001 | $source = $this->params()->fromPost( |
1002 | 'source', |
1003 | $this->params()->fromQuery('source', DEFAULT_SEARCH_BACKEND) |
1004 | ); |
1005 | $driver = $this->getRecordLoader()->load($id, $source, true); |
1006 | $listID = $this->params()->fromPost( |
1007 | 'list_id', |
1008 | $this->params()->fromQuery('list_id', null) |
1009 | ); |
1010 | |
1011 | // Process save action if necessary: |
1012 | if ($this->formWasSubmitted()) { |
1013 | return $this->processEditSubmit($user, $driver, $listID); |
1014 | } |
1015 | |
1016 | // Get saved favorites for selected list (or all lists if $listID is null) |
1017 | $userResourceService = $this->getDbService(UserResourceServiceInterface::class); |
1018 | $userResources = $userResourceService->getFavoritesForRecord($id, $source, $listID, $user); |
1019 | $savedData = []; |
1020 | $favoritesService = $this->serviceLocator->get(FavoritesService::class); |
1021 | foreach ($userResources as $current) { |
1022 | // There should always be list data based on the way we retrieve this result, but |
1023 | // check just to be on the safe side. |
1024 | if ($currentList = $current->getUserList()) { |
1025 | $savedData[] = [ |
1026 | 'listId' => $currentList->getId(), |
1027 | 'listTitle' => $currentList->getTitle(), |
1028 | 'notes' => $current->getNotes(), |
1029 | 'tags' => $favoritesService->getTagStringForEditing($user, $currentList, $id, $source), |
1030 | ]; |
1031 | } |
1032 | } |
1033 | |
1034 | // In order to determine which lists contain the requested item, we may |
1035 | // need to do an extra database lookup if the previous lookup was limited |
1036 | // to a particular list ID: |
1037 | $containingLists = []; |
1038 | if (!empty($listID)) { |
1039 | $userResources = $userResourceService->getFavoritesForRecord($id, $source, null, $user); |
1040 | } |
1041 | foreach ($userResources as $current) { |
1042 | if ($currentList = $current->getUserList()) { |
1043 | $containingLists[] = $currentList->getId(); |
1044 | } |
1045 | } |
1046 | |
1047 | // Send non-containing lists to the view for user selection: |
1048 | $userLists = $this->getDbService(UserListServiceInterface::class)->getUserListsByUser($user); |
1049 | $lists = []; |
1050 | foreach ($userLists as $userList) { |
1051 | if (!in_array($userList->getId(), $containingLists)) { |
1052 | $lists[$userList->getId()] = $userList->getTitle(); |
1053 | } |
1054 | } |
1055 | |
1056 | return $this->createViewModel( |
1057 | compact('driver', 'lists', 'savedData', 'listID') |
1058 | ); |
1059 | } |
1060 | |
1061 | /** |
1062 | * Confirm a request to delete a favorite item. |
1063 | * |
1064 | * @param string $id ID of record to delete |
1065 | * @param string $source Source of record to delete |
1066 | * |
1067 | * @return mixed |
1068 | */ |
1069 | protected function confirmDeleteFavorite($id, $source) |
1070 | { |
1071 | // Normally list ID is found in the route match, but in lightbox context it |
1072 | // may sometimes be a GET parameter. We must cover both cases. |
1073 | $listID = $this->params()->fromRoute('id', $this->params()->fromQuery('id')); |
1074 | if (empty($listID)) { |
1075 | $url = $this->url()->fromRoute('myresearch-favorites'); |
1076 | } else { |
1077 | $url = $this->url()->fromRoute('userList', ['id' => $listID]); |
1078 | } |
1079 | return $this->confirm( |
1080 | 'confirm_delete_brief', |
1081 | $url, |
1082 | $url, |
1083 | 'confirm_delete', |
1084 | ['delete' => $id, 'source' => $source] |
1085 | ); |
1086 | } |
1087 | |
1088 | /** |
1089 | * Send user's saved favorites from a particular list to the view |
1090 | * |
1091 | * @return mixed |
1092 | */ |
1093 | public function mylistAction() |
1094 | { |
1095 | // Fail if lists are disabled: |
1096 | if (!$this->listsEnabled()) { |
1097 | throw new ForbiddenException('Lists disabled'); |
1098 | } |
1099 | |
1100 | // Check for "delete item" request; parameter may be in GET or POST depending |
1101 | // on calling context. |
1102 | $deleteId = $this->params()->fromPost( |
1103 | 'delete', |
1104 | $this->params()->fromQuery('delete') |
1105 | ); |
1106 | if ($deleteId) { |
1107 | $deleteSource = $this->params()->fromPost( |
1108 | 'source', |
1109 | $this->params()->fromQuery('source', DEFAULT_SEARCH_BACKEND) |
1110 | ); |
1111 | // If the user already confirmed the operation, perform the delete now; |
1112 | // otherwise prompt for confirmation: |
1113 | $confirm = $this->params()->fromPost( |
1114 | 'confirm', |
1115 | $this->params()->fromQuery('confirm') |
1116 | ); |
1117 | if ($confirm) { |
1118 | $success = $this->performDeleteFavorite($deleteId, $deleteSource); |
1119 | if ($success !== true) { |
1120 | return $success; |
1121 | } |
1122 | } else { |
1123 | return $this->confirmDeleteFavorite($deleteId, $deleteSource); |
1124 | } |
1125 | } |
1126 | |
1127 | // If we got this far, we just need to display the favorites: |
1128 | try { |
1129 | $runner = $this->serviceLocator->get(\VuFind\Search\SearchRunner::class); |
1130 | |
1131 | // We want to merge together GET, POST and route parameters to |
1132 | // initialize our search object: |
1133 | $request = $this->getRequest()->getQuery()->toArray() |
1134 | + $this->getRequest()->getPost()->toArray() |
1135 | + ['id' => $this->params()->fromRoute('id')]; |
1136 | |
1137 | // Set up listener for recommendations: |
1138 | $rManager = $this->serviceLocator |
1139 | ->get(\VuFind\Recommend\PluginManager::class); |
1140 | $setupCallback = function ($runner, $params, $searchId) use ($rManager) { |
1141 | $listener = new RecommendListener($rManager, $searchId); |
1142 | $listener->setConfig( |
1143 | $params->getOptions()->getRecommendationSettings() |
1144 | ); |
1145 | $listener->attach($runner->getEventManager()->getSharedManager()); |
1146 | }; |
1147 | |
1148 | $results = $runner->run($request, 'Favorites', $setupCallback); |
1149 | $listTags = []; |
1150 | |
1151 | if ($this->listTagsEnabled()) { |
1152 | if ($list = $results->getListObject()) { |
1153 | $tags = $this->serviceLocator->get(TagsService::class)->getListTags($list, $list->getUser()); |
1154 | foreach ($tags as $tag) { |
1155 | $listTags[$tag['id']] = $tag['tag']; |
1156 | } |
1157 | } |
1158 | } |
1159 | return $this->createViewModel( |
1160 | [ |
1161 | 'params' => $results->getParams(), 'results' => $results, |
1162 | 'listTags' => $listTags, |
1163 | ] |
1164 | ); |
1165 | } catch (ListPermissionException $e) { |
1166 | if (!$this->getUser()) { |
1167 | return $this->forceLogin(); |
1168 | } |
1169 | throw $e; |
1170 | } |
1171 | } |
1172 | |
1173 | /** |
1174 | * Process the "edit list" submission. |
1175 | * |
1176 | * @param UserEntityInterface $user Logged in user |
1177 | * @param UserListEntityInterface $list List being created/edited |
1178 | * |
1179 | * @return object|bool Response object if redirect is |
1180 | * needed, false if form needs to be redisplayed. |
1181 | */ |
1182 | protected function processEditList(UserEntityInterface $user, $list) |
1183 | { |
1184 | // Process form within a try..catch so we can handle errors appropriately: |
1185 | try { |
1186 | $favoritesService = $this->serviceLocator->get(FavoritesService::class); |
1187 | $finalId = $favoritesService->updateListFromRequest($list, $user, $this->getRequest()->getPost()); |
1188 | |
1189 | // If the user is in the process of saving a record, send them back |
1190 | // to the save screen; otherwise, send them back to the list they |
1191 | // just edited. |
1192 | $recordId = $this->params()->fromQuery('recordId'); |
1193 | $recordSource |
1194 | = $this->params()->fromQuery('recordSource', DEFAULT_SEARCH_BACKEND); |
1195 | if (!empty($recordId)) { |
1196 | $details = $this->getRecordRouter()->getActionRouteDetails( |
1197 | $recordSource . '|' . $recordId, |
1198 | 'Save' |
1199 | ); |
1200 | return $this->redirect()->toRoute( |
1201 | $details['route'], |
1202 | $details['params'] |
1203 | ); |
1204 | } |
1205 | |
1206 | // Similarly, if the user is in the process of bulk-saving records, |
1207 | // send them back to the appropriate place in the cart. |
1208 | $bulkIds = $this->params()->fromPost( |
1209 | 'ids', |
1210 | $this->params()->fromQuery('ids', []) |
1211 | ); |
1212 | if (!empty($bulkIds)) { |
1213 | $params = []; |
1214 | foreach ($bulkIds as $id) { |
1215 | $params[] = urlencode('ids[]') . '=' . urlencode($id); |
1216 | } |
1217 | $saveUrl = $this->url()->fromRoute('cart-save'); |
1218 | $saveUrl .= (!str_contains($saveUrl, '?')) ? '?' : '&'; |
1219 | return $this->redirect() |
1220 | ->toUrl($saveUrl . implode('&', $params)); |
1221 | } |
1222 | |
1223 | return $this->redirect()->toRoute('userList', ['id' => $finalId]); |
1224 | } catch (ListPermissionException | MissingFieldException $e) { |
1225 | $this->flashMessenger()->addMessage($e->getMessage(), 'error'); |
1226 | return false; |
1227 | } catch (LoginRequiredException $e) { |
1228 | return $this->forceLogin(); |
1229 | } |
1230 | } |
1231 | |
1232 | /** |
1233 | * Send user's saved favorites from a particular list to the edit view |
1234 | * |
1235 | * @return mixed |
1236 | */ |
1237 | public function editlistAction() |
1238 | { |
1239 | // Fail if lists are disabled: |
1240 | if (!$this->listsEnabled()) { |
1241 | throw new ForbiddenException('Lists disabled'); |
1242 | } |
1243 | |
1244 | // User must be logged in to edit list: |
1245 | $user = $this->getUser(); |
1246 | if (!$user) { |
1247 | return $this->forceLogin(); |
1248 | } |
1249 | |
1250 | // Is this a new list or an existing list? Handle the special 'NEW' value |
1251 | // of the ID parameter: |
1252 | $id = $this->params()->fromRoute('id', $this->params()->fromQuery('id')); |
1253 | $newList = ($id == 'NEW'); |
1254 | // If this is a new list, use the FavoritesService to pre-populate some values in |
1255 | // a fresh object; if it's an existing list, we can just fetch from the database. |
1256 | $favoritesService = $this->serviceLocator->get(FavoritesService::class); |
1257 | $list = $newList |
1258 | ? $favoritesService->createListForUser($user) |
1259 | : $this->getDbService(UserListServiceInterface::class)->getUserListById($id); |
1260 | |
1261 | // Make sure the user isn't fishing for other people's lists: |
1262 | if (!$newList && !$favoritesService->userCanEditList($user, $list)) { |
1263 | throw new ListPermissionException('Access denied.'); |
1264 | } |
1265 | |
1266 | // Process form submission: |
1267 | if ($this->formWasSubmitted()) { |
1268 | if ($redirect = $this->processEditList($user, $list)) { |
1269 | return $redirect; |
1270 | } |
1271 | } |
1272 | |
1273 | $listTags = null; |
1274 | if ($this->listTagsEnabled() && !$newList) { |
1275 | $tagsService = $this->serviceLocator->get(TagsService::class); |
1276 | $listTags = $favoritesService |
1277 | ->formatTagStringForEditing($tagsService->getListTags($list, $list->getUser())); |
1278 | } |
1279 | // Send the list to the view: |
1280 | return $this->createViewModel( |
1281 | [ |
1282 | 'list' => $list, |
1283 | 'newList' => $newList, |
1284 | 'listTags' => $listTags, |
1285 | ] |
1286 | ); |
1287 | } |
1288 | |
1289 | /** |
1290 | * Creates a message that the verification email has been sent to the user's |
1291 | * mail address. |
1292 | * |
1293 | * @return mixed |
1294 | */ |
1295 | public function emailNotVerifiedAction() |
1296 | { |
1297 | if ($this->params()->fromQuery('reverify')) { |
1298 | $change = false; |
1299 | $table = $this->getTable('User'); |
1300 | // Case 1: new user: |
1301 | $user = $table |
1302 | ->getByUsername($this->getUserVerificationContainer()->user, false); |
1303 | // Case 2: pending email change: |
1304 | if (!$user) { |
1305 | $user = $this->getUser(); |
1306 | if ($user && $user->getPendingEmail()) { |
1307 | $change = true; |
1308 | } |
1309 | } |
1310 | $this->sendVerificationEmail($user, $change); |
1311 | } else { |
1312 | $this->flashMessenger()->addMessage('verification_email_sent', 'info'); |
1313 | } |
1314 | return $this->createViewModel(); |
1315 | } |
1316 | |
1317 | /** |
1318 | * Creates a confirmation box to delete or not delete the current list |
1319 | * |
1320 | * @return mixed |
1321 | */ |
1322 | public function deletelistAction() |
1323 | { |
1324 | // Fail if lists are disabled: |
1325 | if (!$this->listsEnabled()) { |
1326 | throw new ForbiddenException('Lists disabled'); |
1327 | } |
1328 | |
1329 | // Get requested list ID: |
1330 | $listID = $this->params() |
1331 | ->fromPost('listID', $this->params()->fromQuery('listID')); |
1332 | |
1333 | // Have we confirmed this? |
1334 | $confirm = $this->params()->fromPost( |
1335 | 'confirm', |
1336 | $this->params()->fromQuery('confirm') |
1337 | ); |
1338 | if ($confirm) { |
1339 | try { |
1340 | $list = $this->getDbService(UserListServiceInterface::class)->getUserListById($listID); |
1341 | $this->serviceLocator->get(FavoritesService::class)->destroyList($list, $this->getUser()); |
1342 | |
1343 | // Success Message |
1344 | $this->flashMessenger()->addMessage('fav_list_delete', 'success'); |
1345 | } catch (LoginRequiredException | ListPermissionException $e) { |
1346 | if (!$this->getUser()) { |
1347 | return $this->forceLogin(); |
1348 | } |
1349 | // Logged in? Then we have to rethrow the exception! |
1350 | throw $e; |
1351 | } |
1352 | // Redirect to MyResearch home |
1353 | return $this->redirect()->toRoute('myresearch-favorites'); |
1354 | } |
1355 | |
1356 | // If we got this far, we must display a confirmation message: |
1357 | return $this->confirm( |
1358 | 'confirm_delete_list_brief', |
1359 | $this->url()->fromRoute('myresearch-deletelist'), |
1360 | $this->url()->fromRoute('userList', ['id' => $listID]), |
1361 | 'confirm_delete_list_text', |
1362 | ['listID' => $listID] |
1363 | ); |
1364 | } |
1365 | |
1366 | /** |
1367 | * Send list of holds to view |
1368 | * |
1369 | * @return mixed |
1370 | * |
1371 | * @deprecated |
1372 | */ |
1373 | public function holdsAction() |
1374 | { |
1375 | return $this->redirect()->toRoute('holds-list'); |
1376 | } |
1377 | |
1378 | /** |
1379 | * Send list of storage retrieval requests to view |
1380 | * |
1381 | * @return mixed |
1382 | */ |
1383 | public function storageRetrievalRequestsAction() |
1384 | { |
1385 | // Stop now if the user does not have valid catalog credentials available: |
1386 | if (!is_array($patron = $this->catalogLogin())) { |
1387 | return $patron; |
1388 | } |
1389 | |
1390 | // Connect to the ILS: |
1391 | $catalog = $this->getILS(); |
1392 | |
1393 | // Process cancel requests if necessary: |
1394 | $cancelSRR = $catalog->checkFunction( |
1395 | 'cancelStorageRetrievalRequests', |
1396 | compact('patron') |
1397 | ); |
1398 | $view = $this->createViewModel(); |
1399 | $view->cancelResults = $cancelSRR |
1400 | ? $this->storageRetrievalRequests()->cancelStorageRetrievalRequests( |
1401 | $catalog, |
1402 | $patron |
1403 | ) |
1404 | : []; |
1405 | // If we need to confirm |
1406 | if (!is_array($view->cancelResults)) { |
1407 | return $view->cancelResults; |
1408 | } |
1409 | |
1410 | // By default, assume we will not need to display a cancel form: |
1411 | $view->cancelForm = false; |
1412 | |
1413 | // Get request details: |
1414 | $result = $catalog->getMyStorageRetrievalRequests($patron); |
1415 | $driversNeeded = []; |
1416 | $this->storageRetrievalRequests()->resetValidation(); |
1417 | foreach ($result as $current) { |
1418 | // Add cancel details if appropriate: |
1419 | $current = $this->storageRetrievalRequests()->addCancelDetails( |
1420 | $catalog, |
1421 | $current, |
1422 | $cancelSRR, |
1423 | $patron |
1424 | ); |
1425 | if ( |
1426 | $cancelSRR |
1427 | && $cancelSRR['function'] != 'getCancelStorageRetrievalRequestLink' |
1428 | && isset($current['cancel_details']) |
1429 | ) { |
1430 | // Enable cancel form if necessary: |
1431 | $view->cancelForm = true; |
1432 | } |
1433 | |
1434 | $driversNeeded[] = $current; |
1435 | } |
1436 | |
1437 | // Get List of PickUp Libraries based on patron's home library |
1438 | try { |
1439 | $view->pickup = $catalog->getPickUpLocations($patron); |
1440 | } catch (\Exception $e) { |
1441 | // Do nothing; if we're unable to load information about pickup |
1442 | // locations, they are not supported and we should ignore them. |
1443 | } |
1444 | |
1445 | $view->recordList = $this->ilsRecords()->getDrivers($driversNeeded); |
1446 | $view->accountStatus = $this->ilsRecords() |
1447 | ->collectRequestStats($view->recordList); |
1448 | return $view; |
1449 | } |
1450 | |
1451 | /** |
1452 | * Send list of ill requests to view |
1453 | * |
1454 | * @return mixed |
1455 | */ |
1456 | public function illRequestsAction() |
1457 | { |
1458 | // Stop now if the user does not have valid catalog credentials available: |
1459 | if (!is_array($patron = $this->catalogLogin())) { |
1460 | return $patron; |
1461 | } |
1462 | |
1463 | // Connect to the ILS: |
1464 | $catalog = $this->getILS(); |
1465 | |
1466 | // Process cancel requests if necessary: |
1467 | $cancelStatus = $catalog->checkFunction( |
1468 | 'cancelILLRequests', |
1469 | compact('patron') |
1470 | ); |
1471 | $view = $this->createViewModel(); |
1472 | $view->cancelResults = $cancelStatus |
1473 | ? $this->ILLRequests()->cancelILLRequests( |
1474 | $catalog, |
1475 | $patron |
1476 | ) |
1477 | : []; |
1478 | // If we need to confirm |
1479 | if (!is_array($view->cancelResults)) { |
1480 | return $view->cancelResults; |
1481 | } |
1482 | |
1483 | // By default, assume we will not need to display a cancel form: |
1484 | $view->cancelForm = false; |
1485 | |
1486 | // Get request details: |
1487 | $result = $catalog->getMyILLRequests($patron); |
1488 | $driversNeeded = []; |
1489 | $this->ILLRequests()->resetValidation(); |
1490 | foreach ($result as $current) { |
1491 | // Add cancel details if appropriate: |
1492 | $current = $this->ILLRequests()->addCancelDetails( |
1493 | $catalog, |
1494 | $current, |
1495 | $cancelStatus, |
1496 | $patron |
1497 | ); |
1498 | if ( |
1499 | $cancelStatus |
1500 | && $cancelStatus['function'] != 'getCancelILLRequestLink' |
1501 | && isset($current['cancel_details']) |
1502 | ) { |
1503 | // Enable cancel form if necessary: |
1504 | $view->cancelForm = true; |
1505 | } |
1506 | |
1507 | $driversNeeded[] = $current; |
1508 | } |
1509 | |
1510 | $view->recordList = $this->ilsRecords()->getDrivers($driversNeeded); |
1511 | $view->accountStatus = $this->ilsRecords() |
1512 | ->collectRequestStats($view->recordList); |
1513 | return $view; |
1514 | } |
1515 | |
1516 | /** |
1517 | * Send list of checked out books to view |
1518 | * |
1519 | * @return mixed |
1520 | */ |
1521 | public function checkedoutAction() |
1522 | { |
1523 | // Stop now if the user does not have valid catalog credentials available: |
1524 | if (!is_array($patron = $this->catalogLogin())) { |
1525 | return $patron; |
1526 | } |
1527 | |
1528 | // Connect to the ILS: |
1529 | $catalog = $this->getILS(); |
1530 | |
1531 | // Display account blocks, if any: |
1532 | $this->addAccountBlocksToFlashMessenger($catalog, $patron); |
1533 | |
1534 | // Get the current renewal status and process renewal form, if necessary: |
1535 | $renewStatus = $catalog->checkFunction('Renewals', compact('patron')); |
1536 | $renewResult = $renewStatus |
1537 | ? $this->renewals()->processRenewals( |
1538 | $this->getRequest()->getPost(), |
1539 | $catalog, |
1540 | $patron, |
1541 | $this->serviceLocator->get(CsrfInterface::class) |
1542 | ) |
1543 | : []; |
1544 | |
1545 | // By default, assume we will not need to display a renewal form: |
1546 | $renewForm = false; |
1547 | |
1548 | // Get paging setup: |
1549 | $config = $this->getConfig(); |
1550 | $pageSize = $config->Catalog->checked_out_page_size ?? 50; |
1551 | $pageOptions = $this->getPaginationHelper()->getOptions( |
1552 | (int)$this->params()->fromQuery('page', 1), |
1553 | $this->params()->fromQuery('sort'), |
1554 | $pageSize, |
1555 | $catalog->checkFunction('getMyTransactions', $patron) |
1556 | ); |
1557 | |
1558 | // Get checked out item details: |
1559 | $result = $catalog->getMyTransactions($patron, $pageOptions['ilsParams']); |
1560 | |
1561 | // Build paginator if needed: |
1562 | $paginator = $this->getPaginationHelper()->getPaginator( |
1563 | $pageOptions, |
1564 | $result['count'], |
1565 | $result['records'] |
1566 | ); |
1567 | if ($paginator) { |
1568 | $pageStart = $paginator->getAbsoluteItemNumber(1) - 1; |
1569 | $pageEnd = $paginator->getAbsoluteItemNumber($pageOptions['limit']) - 1; |
1570 | } else { |
1571 | $pageStart = 0; |
1572 | $pageEnd = $result['count']; |
1573 | } |
1574 | |
1575 | // If the results are not paged in the ILS, collect up to date stats for ajax |
1576 | // account notifications: |
1577 | if ( |
1578 | !empty($config->Authentication->enableAjax) |
1579 | && (!$pageOptions['ilsPaging'] || !$paginator |
1580 | || $result['count'] <= $pageSize) |
1581 | ) { |
1582 | $accountStatus = $this->getTransactionSummary($result['records']); |
1583 | } else { |
1584 | $accountStatus = null; |
1585 | } |
1586 | |
1587 | $driversNeeded = $hiddenTransactions = []; |
1588 | foreach ($result['records'] as $i => $current) { |
1589 | // Add renewal details if appropriate: |
1590 | $current = $this->renewals()->addRenewDetails( |
1591 | $catalog, |
1592 | $current, |
1593 | $renewStatus |
1594 | ); |
1595 | if ( |
1596 | $renewStatus && !isset($current['renew_link']) |
1597 | && $current['renewable'] |
1598 | ) { |
1599 | // Enable renewal form if necessary: |
1600 | $renewForm = true; |
1601 | } |
1602 | |
1603 | // Build record drivers (only for the current visible page): |
1604 | if ($pageOptions['ilsPaging'] || ($i >= $pageStart && $i <= $pageEnd)) { |
1605 | $driversNeeded[] = $current; |
1606 | } else { |
1607 | $hiddenTransactions[] = $current; |
1608 | } |
1609 | } |
1610 | |
1611 | $transactions = $this->ilsRecords()->getDrivers($driversNeeded); |
1612 | |
1613 | $displayItemBarcode |
1614 | = !empty($config->Catalog->display_checked_out_item_barcode); |
1615 | |
1616 | $ilsPaging = $pageOptions['ilsPaging']; |
1617 | $sortList = $pageOptions['sortList']; |
1618 | $params = $pageOptions['ilsParams']; |
1619 | return $this->createViewModel( |
1620 | compact( |
1621 | 'transactions', |
1622 | 'renewForm', |
1623 | 'renewResult', |
1624 | 'paginator', |
1625 | 'ilsPaging', |
1626 | 'hiddenTransactions', |
1627 | 'displayItemBarcode', |
1628 | 'sortList', |
1629 | 'params', |
1630 | 'accountStatus' |
1631 | ) |
1632 | ); |
1633 | } |
1634 | |
1635 | /** |
1636 | * Send list of historic loans to view |
1637 | * |
1638 | * @return mixed |
1639 | */ |
1640 | public function historicloansAction() |
1641 | { |
1642 | return $this->redirect()->toRoute('checkouts-history'); |
1643 | } |
1644 | |
1645 | /** |
1646 | * Send list of fines to view |
1647 | * |
1648 | * @return mixed |
1649 | */ |
1650 | public function finesAction() |
1651 | { |
1652 | // Stop now if the user does not have valid catalog credentials available: |
1653 | if (!is_array($patron = $this->catalogLogin())) { |
1654 | return $patron; |
1655 | } |
1656 | |
1657 | // Connect to the ILS: |
1658 | $catalog = $this->getILS(); |
1659 | |
1660 | // Get fine details: |
1661 | $result = $catalog->getMyFines($patron); |
1662 | $fines = []; |
1663 | $driversNeeded = []; |
1664 | foreach ($result as $i => $row) { |
1665 | // If we have an id, add it to list of record drivers to load: |
1666 | if ($row['id'] ?? false) { |
1667 | $driversNeeded[$i] = [ |
1668 | 'id' => $row['id'], |
1669 | 'source' => $row['source'] ?? DEFAULT_SEARCH_BACKEND, |
1670 | ]; |
1671 | } |
1672 | // Store by original index so that we can access it when loading record |
1673 | // drivers: |
1674 | $fines[$i] = $row; |
1675 | } |
1676 | |
1677 | if ($driversNeeded) { |
1678 | $recordLoader = $this->serviceLocator->get(\VuFind\Record\Loader::class); |
1679 | $drivers = $recordLoader->loadBatch($driversNeeded, true); |
1680 | foreach ($drivers as $i => $driver) { |
1681 | $fines[$i]['driver'] = $driver; |
1682 | if (empty($fines[$i]['title'])) { |
1683 | $fines[$i]['title'] = $driver->getShortTitle(); |
1684 | } |
1685 | } |
1686 | } |
1687 | |
1688 | // Clean up array keys: |
1689 | $fines = array_values($fines); |
1690 | |
1691 | // Collect up to date stats for ajax account notifications: |
1692 | if (!empty($this->getConfig()->Authentication->enableAjax)) { |
1693 | $accountStatus = $this->getFineSummary( |
1694 | $fines, |
1695 | $this->serviceLocator->get(\VuFind\Service\CurrencyFormatter::class) |
1696 | ); |
1697 | } else { |
1698 | $accountStatus = null; |
1699 | } |
1700 | |
1701 | return $this->createViewModel(compact('fines', 'accountStatus')); |
1702 | } |
1703 | |
1704 | /** |
1705 | * Convenience method to get a session initiator URL. Returns false if not |
1706 | * applicable. |
1707 | * |
1708 | * @return string|bool |
1709 | */ |
1710 | protected function getSessionInitiator() |
1711 | { |
1712 | $url = $this->getServerUrl('myresearch-home'); |
1713 | return $this->getAuthManager()->getSessionInitiator($url); |
1714 | } |
1715 | |
1716 | /** |
1717 | * Send account recovery email |
1718 | * |
1719 | * @return mixed |
1720 | */ |
1721 | public function recoverAction() |
1722 | { |
1723 | // Make sure we're configured to do this |
1724 | $this->setUpAuthenticationFromRequest(); |
1725 | if (!$this->getAuthManager()->supportsRecovery()) { |
1726 | $this->flashMessenger()->addMessage('recovery_disabled', 'error'); |
1727 | return $this->redirect()->toRoute('myresearch-home'); |
1728 | } |
1729 | if ($this->getUser()) { |
1730 | return $this->redirect()->toRoute('myresearch-home'); |
1731 | } |
1732 | // Database |
1733 | $table = $this->getTable('User'); |
1734 | $user = false; |
1735 | // Check if we have a submitted form, and use the information |
1736 | // to get the user's information |
1737 | if ($email = $this->params()->fromPost('email')) { |
1738 | $user = $table->getByEmail($email); |
1739 | } elseif ($username = $this->params()->fromPost('username')) { |
1740 | $user = $table->getByUsername($username, false); |
1741 | } |
1742 | $view = $this->createViewModel(); |
1743 | $view->useCaptcha = $this->captcha()->active('passwordRecovery'); |
1744 | // If we have a submitted form |
1745 | if ($this->formWasSubmitted(useCaptcha: $view->useCaptcha)) { |
1746 | if ($user) { |
1747 | $this->sendRecoveryEmail($user, $this->getConfig()); |
1748 | } else { |
1749 | $this->flashMessenger() |
1750 | ->addMessage('recovery_user_not_found', 'error'); |
1751 | } |
1752 | } |
1753 | return $view; |
1754 | } |
1755 | |
1756 | /** |
1757 | * Helper function for recoverAction |
1758 | * |
1759 | * @param UserEntityInterface $user User object we're recovering |
1760 | * @param \VuFind\Config $config Configuration object |
1761 | * |
1762 | * @return void (sends email or adds error message) |
1763 | */ |
1764 | protected function sendRecoveryEmail(UserEntityInterface $user, $config) |
1765 | { |
1766 | // If we can't find a user |
1767 | if (!$user) { |
1768 | $this->flashMessenger()->addMessage('recovery_user_not_found', 'error'); |
1769 | } else { |
1770 | // Make sure we've waited long enough |
1771 | $hashtime = $this->getHashAge($user->getVerifyHash()); |
1772 | $recoveryInterval = $config->Authentication->recover_interval ?? 60; |
1773 | if (time() - $hashtime < $recoveryInterval) { |
1774 | $this->flashMessenger()->addMessage('recovery_too_soon', 'error'); |
1775 | } else { |
1776 | // Attempt to send the email |
1777 | try { |
1778 | // Create a fresh hash |
1779 | $this->getAuthManager()->updateUserVerifyHash($user); |
1780 | $config = $this->getConfig(); |
1781 | $renderer = $this->getViewRenderer(); |
1782 | $method = $this->getAuthManager()->getAuthMethod(); |
1783 | // Custom template for emails (text-only) |
1784 | $message = $renderer->render( |
1785 | 'Email/recover-password.phtml', |
1786 | [ |
1787 | 'library' => $config->Site->title, |
1788 | 'url' => $this->getServerUrl('myresearch-verify') |
1789 | . '?hash=' |
1790 | . $user->getVerifyHash() . '&auth_method=' . $method, |
1791 | ] |
1792 | ); |
1793 | $this->serviceLocator->get(Mailer::class)->send( |
1794 | $user->getEmail(), |
1795 | $config->Site->email, |
1796 | $this->translate('recovery_email_subject'), |
1797 | $message |
1798 | ); |
1799 | $this->flashMessenger() |
1800 | ->addMessage('recovery_email_sent', 'success'); |
1801 | } catch (MailException $e) { |
1802 | $this->flashMessenger()->addMessage($e->getDisplayMessage(), 'error'); |
1803 | } |
1804 | } |
1805 | } |
1806 | } |
1807 | |
1808 | /** |
1809 | * Send a verify email message for the first time (only if the user does not |
1810 | * already have a hash). |
1811 | * |
1812 | * @param UserEntityInterface $user User object we're recovering |
1813 | * |
1814 | * @return void (sends email or adds error message) |
1815 | */ |
1816 | protected function sendFirstVerificationEmail(UserEntityInterface $user) |
1817 | { |
1818 | if (!$user->getVerifyHash()) { |
1819 | $this->sendVerificationEmail($user); |
1820 | } |
1821 | } |
1822 | |
1823 | /** |
1824 | * When a request to change a user's email address has been received, we should |
1825 | * send a notification to the old email address for the user's information. |
1826 | * |
1827 | * @param UserEntityInterface $user User whose email address is being changed |
1828 | * @param string $newEmail New email address |
1829 | * |
1830 | * @return void (sends email or adds error message) |
1831 | */ |
1832 | protected function sendChangeNotificationEmail($user, $newEmail) |
1833 | { |
1834 | // Don't send the notification if the existing email is not valid: |
1835 | $validator = new \Laminas\Validator\EmailAddress(); |
1836 | if (!$validator->isValid($user->getEmail())) { |
1837 | return; |
1838 | } |
1839 | |
1840 | $config = $this->getConfig(); |
1841 | $renderer = $this->getViewRenderer(); |
1842 | // Custom template for emails (text-only) |
1843 | $message = $renderer->render( |
1844 | 'Email/notify-email-change.phtml', |
1845 | [ |
1846 | 'library' => $config->Site->title, |
1847 | 'url' => $this->getServerUrl('home'), |
1848 | 'email' => $config->Site->email, |
1849 | 'newEmail' => $newEmail, |
1850 | ] |
1851 | ); |
1852 | // If the user is setting up a new account, use the main email |
1853 | // address; if they have a pending address change, use that. |
1854 | $this->serviceLocator->get(Mailer::class)->send( |
1855 | $user->getEmail(), |
1856 | $config->Site->email, |
1857 | $this->translate('change_notification_email_subject'), |
1858 | $message |
1859 | ); |
1860 | } |
1861 | |
1862 | /** |
1863 | * Send a verify email message. |
1864 | * |
1865 | * @param ?UserEntityInterface $user User object we're recovering |
1866 | * @param bool $change Is the user changing their email (true) |
1867 | * or setting up a new account (false). |
1868 | * |
1869 | * @return void (sends email or adds error message) |
1870 | */ |
1871 | protected function sendVerificationEmail($user, $change = false) |
1872 | { |
1873 | // If we can't find a user |
1874 | if (null == $user) { |
1875 | $this->flashMessenger() |
1876 | ->addMessage('verification_user_not_found', 'error'); |
1877 | } else { |
1878 | // Make sure we've waited long enough |
1879 | $hashtime = $this->getHashAge($user->getVerifyHash()); |
1880 | $recoveryInterval = $this->getConfig()->Authentication->recover_interval |
1881 | ?? 60; |
1882 | if (time() - $hashtime < $recoveryInterval && !$change) { |
1883 | $this->flashMessenger() |
1884 | ->addMessage('verification_too_soon', 'error'); |
1885 | } else { |
1886 | // Attempt to send the email |
1887 | try { |
1888 | // Create a fresh hash |
1889 | $this->getAuthManager()->updateUserVerifyHash($user); |
1890 | $config = $this->getConfig(); |
1891 | $renderer = $this->getViewRenderer(); |
1892 | // Custom template for emails (text-only) |
1893 | $message = $renderer->render( |
1894 | 'Email/verify-email.phtml', |
1895 | [ |
1896 | 'library' => $config->Site->title, |
1897 | 'url' => $this->getServerUrl('myresearch-verifyemail') |
1898 | . '?hash=' . urlencode($user->getVerifyHash()), |
1899 | ] |
1900 | ); |
1901 | // If the user is setting up a new account, use the main email |
1902 | // address; if they have a pending address change, use that. |
1903 | $to = ($pending = $user->getPendingEmail()) ? $pending : $user->getEmail(); |
1904 | $this->serviceLocator->get(Mailer::class)->send( |
1905 | $to, |
1906 | $config->Site->email, |
1907 | $this->translate('verification_email_subject'), |
1908 | $message |
1909 | ); |
1910 | $flashMessage = $change |
1911 | ? 'verification_email_change_sent' |
1912 | : 'verification_email_sent'; |
1913 | $this->flashMessenger()->addMessage($flashMessage, 'info'); |
1914 | // If this is an email change, send a notification to the old |
1915 | // email address as well. |
1916 | if ($change) { |
1917 | $this->sendChangeNotificationEmail($user, $to); |
1918 | } |
1919 | } catch (MailException $e) { |
1920 | $this->flashMessenger()->addMessage($e->getDisplayMessage(), 'error'); |
1921 | } |
1922 | } |
1923 | } |
1924 | } |
1925 | |
1926 | /** |
1927 | * Receive a hash and display the new password form if it's valid |
1928 | * |
1929 | * @return mixed |
1930 | */ |
1931 | public function verifyAction() |
1932 | { |
1933 | // If we have a submitted form |
1934 | if ($hash = $this->params()->fromQuery('hash')) { |
1935 | $hashtime = $this->getHashAge($hash); |
1936 | $config = $this->getConfig(); |
1937 | // Check if hash is expired |
1938 | $hashLifetime = $config->Authentication->recover_hash_lifetime |
1939 | ?? 1209600; // Two weeks |
1940 | if (time() - $hashtime > $hashLifetime) { |
1941 | $this->flashMessenger() |
1942 | ->addMessage('recovery_expired_hash', 'error'); |
1943 | return $this->forwardTo('MyResearch', 'Login'); |
1944 | } else { |
1945 | $table = $this->getTable('User'); |
1946 | // If the hash is valid, forward user to create new password |
1947 | // Also treat email address as verified |
1948 | if ($user = $table->getByVerifyHash($hash)) { |
1949 | $user->setEmailVerified(new DateTime()); |
1950 | $this->getDbService(UserServiceInterface::class)->persistEntity($user); |
1951 | $this->setUpAuthenticationFromRequest(); |
1952 | $view = $this->createViewModel(); |
1953 | $view->auth_method = $this->getAuthManager()->getAuthMethod(); |
1954 | $view->hash = $hash; |
1955 | $view->username = $user->getUsername(); |
1956 | $view->useCaptcha = $this->captcha()->active('changePassword'); |
1957 | $view->passwordPolicy = $this->getAuthManager() |
1958 | ->getPasswordPolicy(); |
1959 | $view->setTemplate('myresearch/newpassword'); |
1960 | return $view; |
1961 | } |
1962 | } |
1963 | } |
1964 | $this->flashMessenger()->addMessage('recovery_invalid_hash', 'error'); |
1965 | return $this->forwardTo('MyResearch', 'Login'); |
1966 | } |
1967 | |
1968 | /** |
1969 | * Receive a hash and display the new password form if it's valid |
1970 | * |
1971 | * @return mixed |
1972 | */ |
1973 | public function verifyEmailAction() |
1974 | { |
1975 | // If we have a submitted form |
1976 | if ($hash = $this->params()->fromQuery('hash')) { |
1977 | $hashtime = $this->getHashAge($hash); |
1978 | $config = $this->getConfig(); |
1979 | // Check if hash is expired |
1980 | $hashLifetime = $config->Authentication->recover_hash_lifetime |
1981 | ?? 1209600; // Two weeks |
1982 | if (time() - $hashtime > $hashLifetime) { |
1983 | $this->flashMessenger() |
1984 | ->addMessage('recovery_expired_hash', 'error'); |
1985 | return $this->forwardTo('MyResearch', 'Profile'); |
1986 | } else { |
1987 | $table = $this->getTable('User'); |
1988 | // If the hash is valid, store validation in DB and forward to login |
1989 | if ($user = $table->getByVerifyHash($hash)) { |
1990 | // Apply pending email address change, if applicable: |
1991 | if ($pending = $user->getPendingEmail()) { |
1992 | $this->getDbService(UserServiceInterface::class) |
1993 | ->updateUserEmail($user, $pending, true); |
1994 | $user->setPendingEmail(''); |
1995 | } |
1996 | $user->setEmailVerified(new DateTime()); |
1997 | $this->getDbService(UserServiceInterface::class)->persistEntity($user); |
1998 | |
1999 | $this->flashMessenger()->addMessage('verification_done', 'info'); |
2000 | return $this->redirect()->toRoute('myresearch-profile'); |
2001 | } |
2002 | } |
2003 | } |
2004 | $this->flashMessenger()->addMessage('recovery_invalid_hash', 'error'); |
2005 | return $this->redirect()->toRoute('myresearch-profile'); |
2006 | } |
2007 | |
2008 | /** |
2009 | * Reset the new password form and return the modified view. When a user has |
2010 | * already been loaded from an existing hash, this resets the hash and updates |
2011 | * the form so that the user can try again. |
2012 | * |
2013 | * @param mixed $userFromHash User loaded from database, or false if none. |
2014 | * @param ViewModel $view View object |
2015 | * |
2016 | * @return ViewModel |
2017 | */ |
2018 | protected function resetNewPasswordForm($userFromHash, ViewModel $view) |
2019 | { |
2020 | if ($userFromHash) { |
2021 | $this->getAuthManager()->updateUserVerifyHash($userFromHash); |
2022 | $view->username = $userFromHash->username; |
2023 | $view->hash = $userFromHash->verify_hash; |
2024 | } |
2025 | return $view; |
2026 | } |
2027 | |
2028 | /** |
2029 | * Handling submission of a new password for a user. |
2030 | * |
2031 | * @return mixed |
2032 | */ |
2033 | public function newPasswordAction() |
2034 | { |
2035 | // Have we submitted the form? |
2036 | if (!$this->formWasSubmitted()) { |
2037 | return $this->redirect()->toRoute('home'); |
2038 | } |
2039 | // Set up authentication so that we can retrieve the correct password policy: |
2040 | $this->setUpAuthenticationFromRequest(); |
2041 | // Pull in from POST |
2042 | $request = $this->getRequest(); |
2043 | $post = $request->getPost(); |
2044 | // Verify hash |
2045 | $userFromHash = isset($post->hash) |
2046 | ? $this->getTable('User')->getByVerifyHash($post->hash) |
2047 | : false; |
2048 | // View, password policy and Captcha |
2049 | $view = $this->createViewModel($post); |
2050 | $view->passwordPolicy = $this->getAuthManager()->getPasswordPolicy(); |
2051 | $view->useCaptcha = $this->captcha()->active('changePassword'); |
2052 | // Check Captcha |
2053 | if (!$this->formWasSubmitted(useCaptcha: $view->useCaptcha)) { |
2054 | return $this->resetNewPasswordForm($userFromHash, $view); |
2055 | } |
2056 | // Missing or invalid hash |
2057 | if (false == $userFromHash) { |
2058 | $this->flashMessenger()->addMessage('recovery_user_not_found', 'error'); |
2059 | // Force login or restore hash |
2060 | $post->username = false; |
2061 | return $this->forwardTo('MyResearch', 'Recover'); |
2062 | } elseif ($userFromHash->username !== $post->username) { |
2063 | $this->flashMessenger() |
2064 | ->addMessage('authentication_error_invalid', 'error'); |
2065 | return $this->resetNewPasswordForm($userFromHash, $view); |
2066 | } |
2067 | // Verify old password if we're logged in |
2068 | if ($this->getUser()) { |
2069 | if (isset($post->oldpwd)) { |
2070 | // Reassign oldpwd to password in the request so login works |
2071 | $tempPassword = $post->password; |
2072 | $post->password = $post->oldpwd; |
2073 | $valid = $this->getAuthManager()->validateCredentials($request); |
2074 | $post->password = $tempPassword; |
2075 | } else { |
2076 | $valid = false; |
2077 | } |
2078 | if (!$valid) { |
2079 | $this->flashMessenger() |
2080 | ->addMessage('authentication_error_invalid', 'error'); |
2081 | $view->verifyold = true; |
2082 | return $view; |
2083 | } |
2084 | } |
2085 | // Update password |
2086 | try { |
2087 | $user = $this->getAuthManager()->updatePassword($this->getRequest()); |
2088 | } catch (AuthException $e) { |
2089 | $this->flashMessenger()->addMessage($e->getMessage(), 'error'); |
2090 | return $view; |
2091 | } |
2092 | // Update hash to prevent reusing hash |
2093 | $this->getAuthManager()->updateUserVerifyHash($user); |
2094 | // Login |
2095 | $this->getAuthManager()->login($this->request); |
2096 | // Return to account home |
2097 | $this->flashMessenger()->addMessage('new_password_success', 'success'); |
2098 | return $this->redirect()->toRoute('myresearch-home'); |
2099 | } |
2100 | |
2101 | /** |
2102 | * Handling submission of a new email for a user. |
2103 | * |
2104 | * @return mixed |
2105 | */ |
2106 | public function changeEmailAction() |
2107 | { |
2108 | // Always check that we are logged in and function is enabled first: |
2109 | if (!($user = $this->getUser())) { |
2110 | return $this->forceLogin(); |
2111 | } |
2112 | if (!$this->getAuthManager()->supportsEmailChange()) { |
2113 | $this->flashMessenger()->addMessage('change_email_disabled', 'error'); |
2114 | return $this->redirect()->toRoute('home'); |
2115 | } |
2116 | $view = $this->createViewModel($this->params()->fromPost()); |
2117 | // Display email |
2118 | $view->email = $user->getEmail(); |
2119 | // Identification |
2120 | $view->useCaptcha = $this->captcha()->active('changeEmail'); |
2121 | // Special case: form was submitted: |
2122 | if ($this->formWasSubmitted(useCaptcha: $view->useCaptcha)) { |
2123 | // Do CSRF check |
2124 | $csrf = $this->serviceLocator->get(CsrfInterface::class); |
2125 | if (!$csrf->isValid($this->getRequest()->getPost()->get('csrf'))) { |
2126 | throw new \VuFind\Exception\BadRequest( |
2127 | 'error_inconsistent_parameters' |
2128 | ); |
2129 | } |
2130 | // Update email |
2131 | $validator = new \Laminas\Validator\EmailAddress(); |
2132 | $email = $this->params()->fromPost('email', ''); |
2133 | try { |
2134 | if (!$validator->isValid($email)) { |
2135 | throw new AuthException('Email address is invalid'); |
2136 | } |
2137 | $this->getAuthManager()->updateEmail($user, $email); |
2138 | // If we have a pending change, we need to send a verification email: |
2139 | if ($user->getPendingEmail()) { |
2140 | $this->sendVerificationEmail($user, true); |
2141 | } else { |
2142 | $this->flashMessenger() |
2143 | ->addMessage('new_email_success', 'success'); |
2144 | } |
2145 | } catch (AuthException $e) { |
2146 | $this->flashMessenger()->addMessage($e->getMessage(), 'error'); |
2147 | return $view; |
2148 | } |
2149 | // Return to account home |
2150 | return $this->redirect()->toRoute('myresearch-home'); |
2151 | } elseif ($this->getConfig()->Authentication->verify_email ?? false) { |
2152 | $this->flashMessenger() |
2153 | ->addMessage('change_email_verification_reminder', 'info'); |
2154 | } |
2155 | $this->addPendingEmailChangeMessage($user); |
2156 | return $view; |
2157 | } |
2158 | |
2159 | /** |
2160 | * Handling submission of a new password for a user. |
2161 | * |
2162 | * @return mixed |
2163 | */ |
2164 | public function changePasswordAction() |
2165 | { |
2166 | if (!$this->getAuthManager()->getIdentity()) { |
2167 | return $this->forceLogin(); |
2168 | } |
2169 | // If not submitted, are we logged in? |
2170 | if (!$this->getAuthManager()->supportsPasswordChange()) { |
2171 | $this->flashMessenger()->addMessage('recovery_new_disabled', 'error'); |
2172 | return $this->redirect()->toRoute('home'); |
2173 | } |
2174 | $view = $this->createViewModel($this->params()->fromPost()); |
2175 | // Verify user password |
2176 | $view->verifyold = true; |
2177 | // Display username |
2178 | $user = $this->getUser(); |
2179 | $view->username = $user->getUsername(); |
2180 | // Password policy |
2181 | $view->passwordPolicy = $this->getAuthManager() |
2182 | ->getPasswordPolicy(); |
2183 | // Identification |
2184 | $this->getAuthManager()->updateUserVerifyHash($user); |
2185 | $view->hash = $user->getVerifyHash(); |
2186 | $view->setTemplate('myresearch/newpassword'); |
2187 | $view->useCaptcha = $this->captcha()->active('changePassword'); |
2188 | return $view; |
2189 | } |
2190 | |
2191 | /** |
2192 | * Delete a login token |
2193 | * |
2194 | * @return mixed |
2195 | */ |
2196 | public function deleteLoginTokenAction() |
2197 | { |
2198 | if (!$this->getAuthManager()->getIdentity()) { |
2199 | return $this->forceLogin(); |
2200 | } |
2201 | $csrf = $this->serviceLocator->get(CsrfInterface::class); |
2202 | if (!$csrf->isValid($this->getRequest()->getPost()->get('csrf'))) { |
2203 | throw new \VuFind\Exception\BadRequest( |
2204 | 'error_inconsistent_parameters' |
2205 | ); |
2206 | } |
2207 | $series = $this->params()->fromPost('series', ''); |
2208 | $this->getAuthManager()->deleteToken($series); |
2209 | return $this->redirect()->toRoute('myresearch-profile'); |
2210 | } |
2211 | |
2212 | /** |
2213 | * Delete all login tokens for a user |
2214 | * |
2215 | * @return mixed |
2216 | */ |
2217 | public function deleteUserLoginTokensAction() |
2218 | { |
2219 | if (!$this->getAuthManager()->getIdentity()) { |
2220 | return $this->forceLogin(); |
2221 | } |
2222 | $csrf = $this->serviceLocator->get(CsrfInterface::class); |
2223 | if (!$csrf->isValid($this->getRequest()->getPost()->get('csrf'))) { |
2224 | throw new \VuFind\Exception\BadRequest( |
2225 | 'error_inconsistent_parameters' |
2226 | ); |
2227 | } |
2228 | $this->getAuthManager()->deleteUserLoginTokens($this->getUser()->id); |
2229 | return $this->redirect()->toRoute('myresearch-profile'); |
2230 | } |
2231 | |
2232 | /** |
2233 | * Helper function for verification hashes |
2234 | * |
2235 | * @param string $hash User-unique hash string from request |
2236 | * |
2237 | * @return int age in seconds |
2238 | */ |
2239 | protected function getHashAge($hash) |
2240 | { |
2241 | return intval(substr($hash, -10)); |
2242 | } |
2243 | |
2244 | /** |
2245 | * Configure the authentication manager to use a user-specified method. |
2246 | * |
2247 | * @return void |
2248 | */ |
2249 | protected function setUpAuthenticationFromRequest() |
2250 | { |
2251 | $method = trim( |
2252 | $this->params()->fromQuery( |
2253 | 'auth_method', |
2254 | $this->params()->fromPost('auth_method') |
2255 | ) |
2256 | ); |
2257 | if (!empty($method)) { |
2258 | $this->getAuthManager()->setAuthMethod($method); |
2259 | } |
2260 | } |
2261 | |
2262 | /** |
2263 | * Account deletion |
2264 | * |
2265 | * @return mixed |
2266 | */ |
2267 | public function deleteAccountAction() |
2268 | { |
2269 | // Force login: |
2270 | if (!($user = $this->getUser())) { |
2271 | return $this->forceLogin(); |
2272 | } |
2273 | |
2274 | $config = $this->getConfig(); |
2275 | if (empty($config->Authentication->account_deletion)) { |
2276 | throw new \VuFind\Exception\BadRequest(); |
2277 | } |
2278 | |
2279 | $view = $this->createViewModel(['accountDeleted' => false]); |
2280 | if ($this->formWasSubmitted()) { |
2281 | $csrf = $this->serviceLocator->get(CsrfInterface::class); |
2282 | if (!$csrf->isValid($this->getRequest()->getPost()->get('csrf'))) { |
2283 | throw new \VuFind\Exception\BadRequest( |
2284 | 'error_inconsistent_parameters' |
2285 | ); |
2286 | } else { |
2287 | // After successful token verification, clear list to shrink session: |
2288 | $csrf->trimTokenList(0); |
2289 | } |
2290 | $this->serviceLocator->get(UserAccountService::class)->purgeUserData( |
2291 | $user, |
2292 | $config->Authentication->delete_comments_with_user ?? true, |
2293 | $config->Authentication->delete_ratings_with_user ?? true |
2294 | ); |
2295 | $view->accountDeleted = true; |
2296 | $view->redirectUrl = $this->getAuthManager()->logout( |
2297 | $this->getServerUrl('home') |
2298 | ); |
2299 | } elseif ($this->formWasSubmitted('reset')) { |
2300 | return $this->redirect()->toRoute('myresearch-profile'); |
2301 | } |
2302 | return $view; |
2303 | } |
2304 | |
2305 | /** |
2306 | * Unsubscribe a scheduled alert for a saved search. |
2307 | * |
2308 | * @return mixed |
2309 | */ |
2310 | public function unsubscribeAction() |
2311 | { |
2312 | $id = $this->params()->fromQuery('id', false); |
2313 | $key = $this->params()->fromQuery('key', false); |
2314 | $type = $this->params()->fromQuery('type', 'alert'); |
2315 | if ($id === false || $key === false) { |
2316 | throw new \Exception('Missing parameters.'); |
2317 | } |
2318 | $view = $this->createViewModel(); |
2319 | if ($this->params()->fromQuery('confirm', false) == 1) { |
2320 | if ($type == 'alert') { |
2321 | $searchService = $this->getDbService(SearchServiceInterface::class); |
2322 | $search = $searchService->getSearchById($id); |
2323 | if (!$search) { |
2324 | throw new \Exception('Invalid parameters.'); |
2325 | } |
2326 | $secret = $this->serviceLocator->get(SecretCalculator::class)->getSearchUnsubscribeSecret($search); |
2327 | if ($key !== $secret) { |
2328 | throw new \Exception('Invalid parameters.'); |
2329 | } |
2330 | $search->setNotificationFrequency(0); |
2331 | $searchService->persistEntity($search); |
2332 | $view->success = true; |
2333 | } |
2334 | } else { |
2335 | $view->unsubscribeUrl = $this->getRequest()->getRequestUri() . '&confirm=1'; |
2336 | } |
2337 | return $view; |
2338 | } |
2339 | |
2340 | /** |
2341 | * Get the ILS pagination helper |
2342 | * |
2343 | * @return PaginationHelper |
2344 | */ |
2345 | protected function getPaginationHelper() |
2346 | { |
2347 | if (null === $this->paginationHelper) { |
2348 | $this->paginationHelper = new PaginationHelper(); |
2349 | } |
2350 | return $this->paginationHelper; |
2351 | } |
2352 | |
2353 | /** |
2354 | * Are list tags enabled? |
2355 | * |
2356 | * @return bool |
2357 | */ |
2358 | protected function listTagsEnabled() |
2359 | { |
2360 | $check = $this->serviceLocator |
2361 | ->get(\VuFind\Config\AccountCapabilities::class); |
2362 | return $check->getListTagSetting() === 'enabled'; |
2363 | } |
2364 | |
2365 | /** |
2366 | * Add a message about any pending email change to the flash messenger |
2367 | * |
2368 | * @param UserEntityInterface $user User |
2369 | * |
2370 | * @return void |
2371 | */ |
2372 | protected function addPendingEmailChangeMessage(UserEntityInterface $user) |
2373 | { |
2374 | if ($pending = $user->getPendingEmail()) { |
2375 | $url = $this->url()->fromRoute( |
2376 | 'myresearch-emailnotverified', |
2377 | [], |
2378 | ['query' => ['reverify' => 'true']] |
2379 | ); |
2380 | $pendingEmailEsc = htmlspecialchars($pending, ENT_COMPAT, 'UTF-8'); |
2381 | $this->flashMessenger()->addInfoMessage( |
2382 | [ |
2383 | 'html' => true, |
2384 | 'msg' => 'email_change_pending_html', |
2385 | 'tokens' => [ |
2386 | '%%pending%%' => $pendingEmailEsc, |
2387 | '%%url%%' => $url, |
2388 | ], |
2389 | ] |
2390 | ); |
2391 | } |
2392 | } |
2393 | } |