Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 383 |
|
0.00% |
0 / 27 |
CRAP | |
0.00% |
0 / 1 |
AbstractRecord | |
0.00% |
0 / 383 |
|
0.00% |
0 / 27 |
11772 | |
0.00% |
0 / 1 |
createViewModel | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
2 | |||
addcommentAction | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
90 | |||
deletecommentAction | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
30 | |||
addtagAction | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
deletetagAction | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
ratingAction | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
90 | |||
homeAction | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
56 | |||
ajaxtabAction | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
2 | |||
processSave | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
saveAction | |
0.00% |
0 / 39 |
|
0.00% |
0 / 1 |
156 | |||
emailAction | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
72 | |||
smsEnabled | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
smsAction | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
30 | |||
citeAction | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
permalinkAction | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
exportAction | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
72 | |||
rdfAction | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
explainAction | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
6 | |||
loadRecord | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
redirectToRecord | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
loadTabDetails | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getDefaultTab | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getAllTabs | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getBackgroundTabs | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTabsExtraScripts | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
resultScrollerActive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
showTab | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
72 |
1 | <?php |
2 | |
3 | /** |
4 | * VuFind Record Controller |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2010-2024. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Controller |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development:plugins:controllers Wiki |
28 | */ |
29 | |
30 | namespace VuFind\Controller; |
31 | |
32 | use VuFind\Db\Service\UserListServiceInterface; |
33 | use VuFind\Db\Service\UserResourceServiceInterface; |
34 | use VuFind\Exception\BadRequest as BadRequestException; |
35 | use VuFind\Exception\Forbidden as ForbiddenException; |
36 | use VuFind\Exception\Mail as MailException; |
37 | use VuFind\Ratings\RatingsService; |
38 | use VuFind\Record\ResourcePopulator; |
39 | use VuFind\RecordDriver\AbstractBase as AbstractRecordDriver; |
40 | use VuFind\Tags\TagsService; |
41 | use VuFindSearch\ParamBag; |
42 | |
43 | use function in_array; |
44 | use function intval; |
45 | use function is_array; |
46 | use function is_object; |
47 | |
48 | /** |
49 | * VuFind Record Controller |
50 | * |
51 | * @category VuFind |
52 | * @package Controller |
53 | * @author Chris Hallberg <challber@villanova.edu> |
54 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
55 | * @link https://vufind.org/wiki/development:plugins:controllers Wiki |
56 | */ |
57 | class AbstractRecord extends AbstractBase |
58 | { |
59 | /** |
60 | * Array of available tab options |
61 | * |
62 | * @var array |
63 | */ |
64 | protected $allTabs = null; |
65 | |
66 | /** |
67 | * Default tab to display (configured at record driver level) |
68 | * |
69 | * @var string |
70 | */ |
71 | protected $defaultTab = null; |
72 | |
73 | /** |
74 | * Default tab to display (fallback used if no record driver configuration) |
75 | * |
76 | * @var string |
77 | */ |
78 | protected $fallbackDefaultTab = 'Holdings'; |
79 | |
80 | /** |
81 | * Array of background tabs |
82 | * |
83 | * @var array |
84 | */ |
85 | protected $backgroundTabs = null; |
86 | |
87 | /** |
88 | * Array of extra scripts for tabs |
89 | * |
90 | * @var array |
91 | */ |
92 | protected $tabsExtraScripts = null; |
93 | |
94 | /** |
95 | * Type of record to display |
96 | * |
97 | * @var string |
98 | */ |
99 | protected $sourceId = 'Solr'; |
100 | |
101 | /** |
102 | * Record driver |
103 | * |
104 | * @var AbstractRecordDriver |
105 | */ |
106 | protected $driver = null; |
107 | |
108 | /** |
109 | * Create a new ViewModel. |
110 | * |
111 | * @param array $params Parameters to pass to ViewModel constructor. |
112 | * |
113 | * @return \Laminas\View\Model\ViewModel |
114 | */ |
115 | protected function createViewModel($params = null) |
116 | { |
117 | $view = parent::createViewModel($params); |
118 | $view->driver = $this->loadRecord(); |
119 | $this->layout()->searchClassId = $view->searchClassId |
120 | = $view->driver->getSearchBackendIdentifier(); |
121 | return $view; |
122 | } |
123 | |
124 | /** |
125 | * Add a comment |
126 | * |
127 | * @return mixed |
128 | */ |
129 | public function addcommentAction() |
130 | { |
131 | // Make sure comments are enabled: |
132 | if (!$this->commentsEnabled()) { |
133 | throw new ForbiddenException('Comments disabled'); |
134 | } |
135 | |
136 | $captchaActive = $this->captcha()->active('userComments'); |
137 | |
138 | // Force login: |
139 | if (!($user = $this->getUser())) { |
140 | // Validate CAPTCHA before redirecting to login: |
141 | if (!$this->formWasSubmitted('comment', $captchaActive)) { |
142 | return $this->redirectToRecord('', 'UserComments'); |
143 | } |
144 | |
145 | // Remember comment since POST data will be lost: |
146 | return $this->forceLogin( |
147 | null, |
148 | ['comment' => $this->params()->fromPost('comment')] |
149 | ); |
150 | } |
151 | |
152 | // Obtain the current record object: |
153 | $driver = $this->loadRecord(); |
154 | |
155 | // Save comment: |
156 | $comment = $this->params()->fromPost('comment'); |
157 | if (empty($comment)) { |
158 | $comment = $this->followup()->retrieveAndClear('comment'); |
159 | } else { |
160 | // Validate CAPTCHA now only if we're not coming back post-login: |
161 | if (!$this->formWasSubmitted('comment', $captchaActive)) { |
162 | return $this->redirectToRecord('', 'UserComments'); |
163 | } |
164 | } |
165 | |
166 | // At this point, we should have a comment to save; if we do not, |
167 | // something has gone wrong (or user submitted blank form) and we |
168 | // should do nothing: |
169 | if (!empty($comment)) { |
170 | $populator = $this->serviceLocator->get(ResourcePopulator::class); |
171 | $resource = $populator->getOrCreateResourceForDriver($driver); |
172 | $commentsService = $this->getDbService( |
173 | \VuFind\Db\Service\CommentsServiceInterface::class |
174 | ); |
175 | $commentsService->addComment($comment, $user, $resource); |
176 | |
177 | // Save rating if allowed: |
178 | if ( |
179 | $driver->isRatingAllowed() |
180 | && '0' !== ($rating = $this->params()->fromPost('rating', '0')) |
181 | ) { |
182 | $ratingsService = $this->serviceLocator->get(RatingsService::class); |
183 | $ratingsService->saveRating($driver, $user->getId(), intval($rating)); |
184 | } |
185 | |
186 | $this->flashMessenger()->addMessage('add_comment_success', 'success'); |
187 | } else { |
188 | $this->flashMessenger()->addMessage('add_comment_fail_blank', 'error'); |
189 | } |
190 | |
191 | return $this->redirectToRecord('', 'UserComments'); |
192 | } |
193 | |
194 | /** |
195 | * Delete a comment |
196 | * |
197 | * @return mixed |
198 | */ |
199 | public function deletecommentAction() |
200 | { |
201 | // Make sure comments are enabled: |
202 | if (!$this->commentsEnabled()) { |
203 | throw new ForbiddenException('Comments disabled'); |
204 | } |
205 | |
206 | // Force login: |
207 | if (!($user = $this->getUser())) { |
208 | return $this->forceLogin(); |
209 | } |
210 | $id = $this->params()->fromQuery('delete'); |
211 | $commentsService = $this->getDbService( |
212 | \VuFind\Db\Service\CommentsServiceInterface::class |
213 | ); |
214 | if (null !== $id && $commentsService->deleteIfOwnedByUser($id, $user)) { |
215 | $this->flashMessenger()->addMessage('delete_comment_success', 'success'); |
216 | } else { |
217 | $this->flashMessenger()->addMessage('delete_comment_failure', 'error'); |
218 | } |
219 | return $this->redirectToRecord('', 'UserComments'); |
220 | } |
221 | |
222 | /** |
223 | * Add a tag |
224 | * |
225 | * @return mixed |
226 | */ |
227 | public function addtagAction() |
228 | { |
229 | // Make sure tags are enabled: |
230 | if (!$this->tagsEnabled()) { |
231 | throw new ForbiddenException('Tags disabled'); |
232 | } |
233 | |
234 | // Force login: |
235 | if (!($user = $this->getUser())) { |
236 | return $this->forceLogin(); |
237 | } |
238 | |
239 | // Obtain the current record object: |
240 | $driver = $this->loadRecord(); |
241 | |
242 | // Save tags, if any: |
243 | if ($tags = $this->params()->fromPost('tag')) { |
244 | $this->serviceLocator->get(TagsService::class)->linkTagsToRecord($driver, $user, $tags); |
245 | $this->flashMessenger()->addMessage(['msg' => 'add_tag_success'], 'success'); |
246 | return $this->redirectToRecord(); |
247 | } |
248 | |
249 | // Display the "add tag" form: |
250 | $view = $this->createViewModel(); |
251 | $view->setTemplate('record/addtag'); |
252 | return $view; |
253 | } |
254 | |
255 | /** |
256 | * Delete a tag |
257 | * |
258 | * @return mixed |
259 | */ |
260 | public function deletetagAction() |
261 | { |
262 | // Make sure tags are enabled: |
263 | if (!$this->tagsEnabled()) { |
264 | throw new ForbiddenException('Tags disabled'); |
265 | } |
266 | |
267 | // Force login: |
268 | if (!($user = $this->getUser())) { |
269 | return $this->forceLogin(); |
270 | } |
271 | |
272 | // Obtain the current record object: |
273 | $driver = $this->loadRecord(); |
274 | |
275 | // Delete tags, if any: |
276 | if ($tag = $this->params()->fromPost('tag')) { |
277 | $this->serviceLocator->get(TagsService::class)->unlinkTagsFromRecord( |
278 | $driver, |
279 | $user, |
280 | [$tag] |
281 | ); |
282 | $this->flashMessenger()->addMessage( |
283 | [ |
284 | 'msg' => 'tags_deleted', |
285 | 'tokens' => ['%count%' => 1], |
286 | ], |
287 | 'success' |
288 | ); |
289 | } |
290 | |
291 | return $this->redirectToRecord(); |
292 | } |
293 | |
294 | /** |
295 | * Display and add ratings |
296 | * |
297 | * @return mixed |
298 | */ |
299 | public function ratingAction() |
300 | { |
301 | // Obtain the current record object: |
302 | $driver = $this->loadRecord(); |
303 | |
304 | // Make sure ratings are allowed for the record: |
305 | if (!$driver->isRatingAllowed()) { |
306 | throw new ForbiddenException('rating_disabled'); |
307 | } |
308 | |
309 | // Save rating, if any, and user has logged in: |
310 | $user = $this->getUser(); |
311 | if ($user && null !== ($rating = $this->params()->fromPost('rating'))) { |
312 | if ( |
313 | '' === $rating |
314 | && !($this->getConfig()->Social->remove_rating ?? true) |
315 | ) { |
316 | throw new BadRequestException('error_inconsistent_parameters'); |
317 | } |
318 | $ratingsService = $this->serviceLocator->get(RatingsService::class); |
319 | $ratingsService->saveRating( |
320 | $driver, |
321 | $user->getId(), |
322 | '' === $rating ? null : intval($rating) |
323 | ); |
324 | $this->flashMessenger()->addSuccessMessage('rating_add_success'); |
325 | if ($this->inLightbox()) { |
326 | return $this->getRefreshResponse(); |
327 | } |
328 | return $this->redirectToRecord(); |
329 | } |
330 | |
331 | // Display the "add rating" form: |
332 | $currentRating = $user |
333 | ? $this->serviceLocator->get(RatingsService::class)->getRatingData($driver, $user->getId()) |
334 | : null; |
335 | return $this->createViewModel(compact('currentRating')); |
336 | } |
337 | |
338 | /** |
339 | * Home (default) action -- forward to requested (or default) tab. |
340 | * |
341 | * @return mixed |
342 | */ |
343 | public function homeAction() |
344 | { |
345 | // If collections are active, we may need to check if the driver is actually |
346 | // a collection; if so, we should redirect to the collection controller. |
347 | $checkRoute = $this->params()->fromPost('checkRoute') |
348 | ?? $this->params()->fromQuery('checkRoute') |
349 | ?? false; |
350 | $config = $this->getConfig(); |
351 | if ($checkRoute && $config->Collections->collections ?? false) { |
352 | $routeConfig = isset($config->Collections->route) |
353 | ? $config->Collections->route->toArray() : []; |
354 | $collectionRoutes |
355 | = array_merge(['record' => 'collection'], $routeConfig); |
356 | $routeName = $this->event->getRouteMatch()->getMatchedRouteName() ?? ''; |
357 | if ($collectionRoute = ($collectionRoutes[$routeName] ?? null)) { |
358 | $driver = $this->loadRecord(); |
359 | if (true === $driver->tryMethod('isCollection')) { |
360 | $params = $this->params()->fromQuery() |
361 | + $this->params()->fromRoute(); |
362 | // Disable path normalization since it can unencode e.g. encoded |
363 | // slashes in record id's |
364 | $options = [ |
365 | 'normalize_path' => false, |
366 | ]; |
367 | if ($sid = $this->getSearchMemory()->getCurrentSearchId()) { |
368 | $options['query'] = compact('sid'); |
369 | } |
370 | $collectionUrl = $this->url() |
371 | ->fromRoute($collectionRoute, $params, $options); |
372 | return $this->redirect()->toUrl($collectionUrl); |
373 | } |
374 | } |
375 | } |
376 | |
377 | return $this->showTab( |
378 | $this->params()->fromRoute('tab', $this->getDefaultTab()) |
379 | ); |
380 | } |
381 | |
382 | /** |
383 | * AJAX tab action -- render a tab without surrounding context. |
384 | * |
385 | * @return mixed |
386 | */ |
387 | public function ajaxtabAction() |
388 | { |
389 | $this->disableSessionWrites(); |
390 | $this->loadRecord(); |
391 | // Set layout to render content only: |
392 | $this->layout()->setTemplate('layout/lightbox'); |
393 | $this->layout()->setVariable('layoutContext', 'tabs'); |
394 | return $this->showTab( |
395 | $this->params()->fromPost('tab', $this->getDefaultTab()), |
396 | true |
397 | ); |
398 | } |
399 | |
400 | /** |
401 | * ProcessSave -- store the results of the Save action. |
402 | * |
403 | * @return mixed |
404 | */ |
405 | protected function processSave() |
406 | { |
407 | // Retrieve user object and force login if necessary: |
408 | if (!($user = $this->getUser())) { |
409 | return $this->forceLogin(); |
410 | } |
411 | |
412 | // Perform the save operation: |
413 | $driver = $this->loadRecord(); |
414 | $post = $this->getRequest()->getPost()->toArray(); |
415 | $tagsService = $this->serviceLocator->get(TagsService::class); |
416 | $post['mytags'] = $tagsService->parse($post['mytags'] ?? ''); |
417 | $favorites = $this->serviceLocator->get(\VuFind\Favorites\FavoritesService::class); |
418 | $results = $favorites->saveRecordToFavorites($post, $user, $driver); |
419 | |
420 | // Display a success status message: |
421 | $listUrl = $this->url()->fromRoute('userList', ['id' => $results['listId']]); |
422 | $message = [ |
423 | 'html' => true, |
424 | 'msg' => $this->translate('bulk_save_success') . '. ' |
425 | . '<a href="' . $listUrl . '" class="gotolist">' |
426 | . $this->translate('go_to_list') . '</a>.', |
427 | ]; |
428 | $this->flashMessenger()->addMessage($message, 'success'); |
429 | |
430 | // redirect to followup url saved in saveAction |
431 | if ($url = $this->getAndClearFollowupUrl()) { |
432 | return $this->redirect()->toUrl($url); |
433 | } |
434 | |
435 | // No followup info found? Send back to record view: |
436 | return $this->redirectToRecord(); |
437 | } |
438 | |
439 | /** |
440 | * Save action - Allows the save template to appear, |
441 | * passes containingLists & nonContainingLists |
442 | * |
443 | * @return mixed |
444 | */ |
445 | public function saveAction() |
446 | { |
447 | // Fail if lists are disabled: |
448 | if (!$this->listsEnabled()) { |
449 | throw new ForbiddenException('Lists disabled'); |
450 | } |
451 | |
452 | // Check permission: |
453 | $response = $this->permission()->check('feature.Favorites', false); |
454 | if (is_object($response)) { |
455 | return $response; |
456 | } |
457 | |
458 | // Process form submission: |
459 | if ($this->formWasSubmitted()) { |
460 | return $this->processSave(); |
461 | } |
462 | |
463 | // Retrieve user object and force login if necessary: |
464 | if (!($user = $this->getUser())) { |
465 | return $this->forceLogin(); |
466 | } |
467 | |
468 | // If we got this far, we should save the referer for later use by the |
469 | // ProcessSave action (to get back to where we came from after saving). |
470 | // We shouldn't save follow-up information if it points to the Save |
471 | // screen or the "create list" screen, as this causes confusing workflows; |
472 | // in these cases, we will simply push the user to record view |
473 | // by unsetting the followup and relying on default behavior in processSave. |
474 | $referer = $this->getRequest()->getServer()->get('HTTP_REFERER'); |
475 | if ( |
476 | !str_ends_with($referer, '/Save') |
477 | && stripos($referer, 'MyResearch/EditList/NEW') === false |
478 | && $this->isLocalUrl($referer) |
479 | ) { |
480 | $this->setFollowupUrlToReferer(); |
481 | } else { |
482 | $this->clearFollowupUrl(); |
483 | } |
484 | |
485 | // Retrieve the record driver: |
486 | $driver = $this->loadRecord(); |
487 | |
488 | // Find out if the item is already part of any lists; save list info/IDs |
489 | $listIds = []; |
490 | $resources = $this->getDbService(UserResourceServiceInterface::class)->getFavoritesForRecord( |
491 | $driver->getUniqueId(), |
492 | $driver->getSourceIdentifier(), |
493 | null, |
494 | $user |
495 | ); |
496 | foreach ($resources as $userResource) { |
497 | if ($currentList = $userResource->getUserList()) { |
498 | $listIds[] = $currentList->getId(); |
499 | } |
500 | } |
501 | |
502 | // Loop through all user lists and sort out containing/non-containing lists |
503 | $containingLists = $nonContainingLists = []; |
504 | foreach ($this->getDbService(UserListServiceInterface::class)->getUserListsByUser($user) as $list) { |
505 | // Assign list to appropriate array based on whether or not we found |
506 | // it earlier in the list of lists containing the selected record. |
507 | if (in_array($list->getId(), $listIds)) { |
508 | $containingLists[] = $list; |
509 | } else { |
510 | $nonContainingLists[] = $list; |
511 | } |
512 | } |
513 | |
514 | $view = $this->createViewModel( |
515 | [ |
516 | 'containingLists' => $containingLists, |
517 | 'nonContainingLists' => $nonContainingLists, |
518 | ] |
519 | ); |
520 | $view->setTemplate('record/save'); |
521 | return $view; |
522 | } |
523 | |
524 | /** |
525 | * Email action - Allows the email form to appear. |
526 | * |
527 | * @return \Laminas\View\Model\ViewModel |
528 | */ |
529 | public function emailAction() |
530 | { |
531 | // Force login if necessary: |
532 | $config = $this->getConfig(); |
533 | if ( |
534 | (!isset($config->Mail->require_login) || $config->Mail->require_login) |
535 | && !$this->getUser() |
536 | ) { |
537 | return $this->forceLogin(); |
538 | } |
539 | |
540 | // Retrieve the record driver: |
541 | $driver = $this->loadRecord(); |
542 | |
543 | // Create view |
544 | $mailer = $this->serviceLocator->get(\VuFind\Mailer\Mailer::class); |
545 | $view = $this->createEmailViewModel( |
546 | null, |
547 | $mailer->getDefaultRecordSubject($driver) |
548 | ); |
549 | $mailer->setMaxRecipients($view->maxRecipients); |
550 | |
551 | // Set up Captcha |
552 | $view->useCaptcha = $this->captcha()->active('email'); |
553 | // Process form submission: |
554 | if ($this->formWasSubmitted(useCaptcha: $view->useCaptcha)) { |
555 | // Attempt to send the email and show an appropriate flash message: |
556 | try { |
557 | $cc = $this->params()->fromPost('ccself') && $view->from != $view->to |
558 | ? $view->from : null; |
559 | $mailer->sendRecord( |
560 | $view->to, |
561 | $view->from, |
562 | $view->message, |
563 | $driver, |
564 | $this->getViewRenderer(), |
565 | $view->subject, |
566 | $cc |
567 | ); |
568 | $this->flashMessenger()->addMessage('email_success', 'success'); |
569 | return $this->redirectToRecord(); |
570 | } catch (MailException $e) { |
571 | $this->flashMessenger()->addMessage($e->getDisplayMessage(), 'error'); |
572 | } |
573 | } |
574 | |
575 | // Display the template: |
576 | $view->setTemplate('record/email'); |
577 | return $view; |
578 | } |
579 | |
580 | /** |
581 | * Is SMS enabled? |
582 | * |
583 | * @return bool |
584 | */ |
585 | protected function smsEnabled() |
586 | { |
587 | $check = $this->serviceLocator |
588 | ->get(\VuFind\Config\AccountCapabilities::class); |
589 | return $check->getSmsSetting() !== 'disabled'; |
590 | } |
591 | |
592 | /** |
593 | * SMS action - Allows the SMS form to appear. |
594 | * |
595 | * @return \Laminas\View\Model\ViewModel |
596 | */ |
597 | public function smsAction() |
598 | { |
599 | // Make sure comments are enabled: |
600 | if (!$this->smsEnabled()) { |
601 | throw new ForbiddenException('SMS disabled'); |
602 | } |
603 | |
604 | // Retrieve the record driver: |
605 | $driver = $this->loadRecord(); |
606 | |
607 | // Load the SMS carrier list: |
608 | $sms = $this->serviceLocator->get(\VuFind\SMS\SMSInterface::class); |
609 | $view = $this->createViewModel(); |
610 | $view->carriers = $sms->getCarriers(); |
611 | $view->validation = $sms->getValidationType(); |
612 | // Set up Captcha |
613 | $view->useCaptcha = $this->captcha()->active('sms'); |
614 | // Send parameters back to view so form can be re-populated: |
615 | $view->to = $this->params()->fromPost('to'); |
616 | $view->provider = $this->params()->fromPost('provider'); |
617 | // Process form submission: |
618 | if ($this->formWasSubmitted(useCaptcha: $view->useCaptcha)) { |
619 | // Do CSRF check |
620 | $csrf = $this->serviceLocator->get(\VuFind\Validator\SessionCsrf::class); |
621 | if (!$csrf->isValid($this->getRequest()->getPost()->get('csrf'))) { |
622 | throw new \VuFind\Exception\BadRequest( |
623 | 'error_inconsistent_parameters' |
624 | ); |
625 | } |
626 | |
627 | // Attempt to send the email and show an appropriate flash message: |
628 | try { |
629 | $body = $this->getViewRenderer()->partial( |
630 | 'Email/record-sms.phtml', |
631 | ['driver' => $driver, 'to' => $view->to] |
632 | ); |
633 | $sms->text($view->provider, $view->to, null, $body); |
634 | $this->flashMessenger()->addMessage('sms_success', 'success'); |
635 | return $this->redirectToRecord(); |
636 | } catch (MailException $e) { |
637 | $this->flashMessenger()->addMessage($e->getDisplayMessage(), 'error'); |
638 | } |
639 | } |
640 | |
641 | // Display the template: |
642 | $view->setTemplate('record/sms'); |
643 | return $view; |
644 | } |
645 | |
646 | /** |
647 | * Show citations for the current record. |
648 | * |
649 | * @return \Laminas\View\Model\ViewModel |
650 | */ |
651 | public function citeAction() |
652 | { |
653 | $view = $this->createViewModel(); |
654 | $view->setTemplate('record/cite'); |
655 | return $view; |
656 | } |
657 | |
658 | /** |
659 | * Show permanent link for the current record. |
660 | * |
661 | * @return \Laminas\View\Model\ViewModel |
662 | */ |
663 | public function permalinkAction() |
664 | { |
665 | $view = $this->createViewModel(); |
666 | $view->setTemplate('record/permalink'); |
667 | return $view; |
668 | } |
669 | |
670 | /** |
671 | * Export the record |
672 | * |
673 | * @return mixed |
674 | */ |
675 | public function exportAction() |
676 | { |
677 | $driver = $this->loadRecord(); |
678 | $view = $this->createViewModel(); |
679 | $format = $this->params()->fromQuery('style'); |
680 | |
681 | // Display export menu if missing/invalid option |
682 | $export = $this->serviceLocator->get(\VuFind\Export::class); |
683 | if (empty($format) || !$export->recordSupportsFormat($driver, $format)) { |
684 | if (!empty($format)) { |
685 | $this->flashMessenger() |
686 | ->addMessage('export_invalid_format', 'error'); |
687 | } |
688 | $view->setTemplate('record/export-menu'); |
689 | return $view; |
690 | } |
691 | |
692 | // If this is an export format that redirects to an external site, perform |
693 | // the redirect now (unless we're being called back from that service!): |
694 | if ( |
695 | $export->needsRedirect($format) |
696 | && !$this->params()->fromQuery('callback') |
697 | ) { |
698 | // Build callback URL: |
699 | $parts = explode('?', $this->getServerUrl(true)); |
700 | $callback = $parts[0] . '?callback=1&style=' . urlencode($format); |
701 | |
702 | return $this->redirect() |
703 | ->toUrl($export->getRedirectUrl($format, $callback)); |
704 | } |
705 | |
706 | $recordHelper = $this->getViewRenderer()->plugin('record'); |
707 | try { |
708 | $exportedRecord = $recordHelper($driver)->getExport($format); |
709 | } catch (\VuFind\Exception\FormatUnavailable $e) { |
710 | $this->flashMessenger()->addErrorMessage('export_unsupported_format'); |
711 | return $this->redirectToRecord(); |
712 | } |
713 | |
714 | $exportType = $export->getBulkExportType($format); |
715 | if ('post' === $exportType) { |
716 | $params = [ |
717 | 'exportType' => 'post', |
718 | 'postField' => $export->getPostField($format), |
719 | 'postData' => $exportedRecord, |
720 | 'targetWindow' => $export->getTargetWindow($format), |
721 | 'url' => $export->getRedirectUrl($format, ''), |
722 | 'format' => $format, |
723 | ]; |
724 | $msg = [ |
725 | 'translate' => false, 'html' => true, |
726 | 'msg' => $this->getViewRenderer()->render( |
727 | 'cart/export-success.phtml', |
728 | $params |
729 | ), |
730 | ]; |
731 | $this->flashMessenger()->addSuccessMessage($msg); |
732 | return $this->redirectToRecord(); |
733 | } |
734 | |
735 | // Send appropriate HTTP headers for requested format: |
736 | $response = $this->getResponse(); |
737 | $response->getHeaders()->addHeaders($export->getHeaders($format)); |
738 | |
739 | // Actually export the record |
740 | $response->setContent($exportedRecord); |
741 | return $response; |
742 | } |
743 | |
744 | /** |
745 | * Special action for RDF export |
746 | * |
747 | * @return mixed |
748 | */ |
749 | public function rdfAction() |
750 | { |
751 | $this->getRequest()->getQuery()->set('style', 'RDF'); |
752 | return $this->exportAction(); |
753 | } |
754 | |
755 | /** |
756 | * Show explanation for why a record was found and how its relevancy is computed |
757 | * |
758 | * @return mixed |
759 | */ |
760 | public function explainAction() |
761 | { |
762 | $record = $this->loadRecord(); |
763 | |
764 | $view = $this->createViewModel(); |
765 | $view->setTemplate('record/explain'); |
766 | if (!$record->tryMethod('explainEnabled')) { |
767 | $view->disabled = true; |
768 | return $view; |
769 | } |
770 | |
771 | $explanation = $this->serviceLocator |
772 | ->get(\VuFind\Search\Explanation\PluginManager::class) |
773 | ->get($record->getSourceIdentifier()); |
774 | |
775 | $params = $explanation->getParams(); |
776 | $params->initFromRequest($this->getRequest()->getQuery()); |
777 | $explanation->performRequest($record->getUniqueID()); |
778 | |
779 | $view->explanation = $explanation; |
780 | return $view; |
781 | } |
782 | |
783 | /** |
784 | * Load the record requested by the user; note that this is not done in the |
785 | * init() method since we don't want to perform an expensive search twice |
786 | * when homeAction() forwards to another method. |
787 | * |
788 | * @param ParamBag $params Search backend parameters |
789 | * @param bool $force Set to true to force a reload of the record, even if |
790 | * already loaded (useful if loading a record using different parameters) |
791 | * |
792 | * @return AbstractRecordDriver |
793 | */ |
794 | protected function loadRecord(ParamBag $params = null, bool $force = false) |
795 | { |
796 | // Only load the record if it has not already been loaded. Note that |
797 | // when determining record ID, we check both the route match (the most |
798 | // common scenario) and the GET parameters (a fallback used by some |
799 | // legacy routes). |
800 | if ($force || !is_object($this->driver)) { |
801 | $recordLoader = $this->getRecordLoader(); |
802 | $cacheContext = $this->getRequest()->getQuery()->get('cacheContext'); |
803 | if (isset($cacheContext)) { |
804 | $recordLoader->setCacheContext($cacheContext); |
805 | } |
806 | $this->driver = $recordLoader->load( |
807 | $this->params()->fromRoute('id', $this->params()->fromQuery('id')), |
808 | $this->sourceId, |
809 | false, |
810 | $params |
811 | ); |
812 | } |
813 | return $this->driver; |
814 | } |
815 | |
816 | /** |
817 | * Redirect the user to the main record view. |
818 | * |
819 | * @param string $params Parameters to append to record URL. |
820 | * @param string $tab Record tab to display (null for default). |
821 | * |
822 | * @return mixed |
823 | */ |
824 | protected function redirectToRecord($params = '', $tab = null) |
825 | { |
826 | $details = $this->getRecordRouter() |
827 | ->getTabRouteDetails($this->loadRecord(), $tab); |
828 | $target = $this->url()->fromRoute($details['route'], $details['params']); |
829 | |
830 | return $this->redirect()->toUrl($target . $params); |
831 | } |
832 | |
833 | /** |
834 | * Support method to load tab information from the RecordTab PluginManager. |
835 | * |
836 | * @return void |
837 | */ |
838 | protected function loadTabDetails() |
839 | { |
840 | $driver = $this->loadRecord(); |
841 | $request = $this->getRequest(); |
842 | $manager = $this->getRecordTabManager(); |
843 | $details = $manager |
844 | ->getTabDetailsForRecord($driver, $request, $this->fallbackDefaultTab); |
845 | $this->allTabs = $details['tabs']; |
846 | $this->defaultTab = $details['default'] ? $details['default'] : false; |
847 | $this->backgroundTabs = $manager->getBackgroundTabNames($driver); |
848 | $this->tabsExtraScripts = $manager->getExtraScripts(); |
849 | } |
850 | |
851 | /** |
852 | * Get default tab for a given driver |
853 | * |
854 | * @return string |
855 | */ |
856 | protected function getDefaultTab() |
857 | { |
858 | // Load default tab if not already retrieved: |
859 | if (null === $this->defaultTab) { |
860 | $this->loadTabDetails(); |
861 | } |
862 | return $this->defaultTab; |
863 | } |
864 | |
865 | /** |
866 | * Get all tab information for a given driver. |
867 | * |
868 | * @return array |
869 | */ |
870 | protected function getAllTabs() |
871 | { |
872 | if (null === $this->allTabs) { |
873 | $this->loadTabDetails(); |
874 | } |
875 | return $this->allTabs; |
876 | } |
877 | |
878 | /** |
879 | * Get names of tabs to be loaded in the background. |
880 | * |
881 | * @return array |
882 | */ |
883 | protected function getBackgroundTabs() |
884 | { |
885 | if (null === $this->backgroundTabs) { |
886 | $this->loadTabDetails(); |
887 | } |
888 | return $this->backgroundTabs; |
889 | } |
890 | |
891 | /** |
892 | * Get extra scripts required by tabs. |
893 | * |
894 | * @param array $tabs Tab names to consider |
895 | * |
896 | * @return array |
897 | */ |
898 | protected function getTabsExtraScripts($tabs) |
899 | { |
900 | if (null === $this->tabsExtraScripts) { |
901 | $this->loadTabDetails(); |
902 | } |
903 | $allScripts = []; |
904 | foreach (array_keys($tabs) as $tab) { |
905 | if (!empty($this->tabsExtraScripts[$tab])) { |
906 | $allScripts |
907 | = array_merge($allScripts, $this->tabsExtraScripts[$tab]); |
908 | } |
909 | } |
910 | return array_unique($allScripts); |
911 | } |
912 | |
913 | /** |
914 | * Is the result scroller active? |
915 | * |
916 | * @return bool |
917 | */ |
918 | protected function resultScrollerActive() |
919 | { |
920 | // Disabled by default: |
921 | return false; |
922 | } |
923 | |
924 | /** |
925 | * Display a particular tab. |
926 | * |
927 | * @param string $tab Name of tab to display |
928 | * @param bool $ajax Are we in AJAX mode? |
929 | * |
930 | * @return mixed |
931 | */ |
932 | protected function showTab($tab, $ajax = false) |
933 | { |
934 | // Special case -- handle login request (currently needed for holdings |
935 | // tab when driver-based holds mode is enabled, but may also be useful |
936 | // in other circumstances): |
937 | if ( |
938 | $this->params()->fromQuery('login', 'false') == 'true' |
939 | && !$this->getUser() |
940 | ) { |
941 | return $this->forceLogin(null); |
942 | } elseif ( |
943 | $this->params()->fromQuery('catalogLogin', 'false') == 'true' |
944 | && !is_array($patron = $this->catalogLogin()) |
945 | ) { |
946 | return $patron; |
947 | } |
948 | |
949 | $config = $this->getConfig(); |
950 | |
951 | $view = $this->createViewModel(); |
952 | $view->tabs = $this->getAllTabs(); |
953 | $view->activeTab = strtolower($tab); |
954 | $view->defaultTab = strtolower($this->getDefaultTab()); |
955 | $view->backgroundTabs = $this->getBackgroundTabs(); |
956 | $view->tabsExtraScripts = $this->getTabsExtraScripts($view->tabs); |
957 | $view->loadInitialTabWithAjax |
958 | = isset($config->Site->loadInitialTabWithAjax) |
959 | ? (bool)$config->Site->loadInitialTabWithAjax : false; |
960 | |
961 | // Set up next/previous record links (if appropriate) |
962 | if ($this->resultScrollerActive()) { |
963 | $driver = $this->loadRecord(); |
964 | $view->scrollData = $this->resultScroller()->getScrollData($driver); |
965 | } |
966 | |
967 | $view->callnumberHandler = $config->Item_Status->callnumber_handler ?? false; |
968 | |
969 | $view->setTemplate($ajax ? 'record/ajaxtab' : 'record/view'); |
970 | return $view; |
971 | } |
972 | } |