Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 240
0.00% covered (danger)
0.00%
0 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
CartController
0.00% covered (danger)
0.00%
0 / 240
0.00% covered (danger)
0.00%
0 / 12
6006
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getCart
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCartActionFromRequest
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 searchresultsbulkAction
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 processorAction
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 homeAction
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
72
 myresearchbulkAction
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
 emailAction
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
306
 printcartAction
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 exportAction
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
156
 doexportAction
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 saveAction
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
156
1<?php
2
3/**
4 * Book Bag / Bulk Action Controller
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 *
10 * This program is free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License version 2,
12 * as published by the Free Software Foundation.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
22 *
23 * @category VuFind
24 * @package  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 Main Site
28 */
29
30namespace VuFind\Controller;
31
32use Laminas\ServiceManager\ServiceLocatorInterface;
33use Laminas\Session\Container;
34use VuFind\Controller\Feature\ListItemSelectionTrait;
35use VuFind\Db\Service\UserListServiceInterface;
36use VuFind\Exception\Forbidden as ForbiddenException;
37use VuFind\Exception\Mail as MailException;
38use VuFind\Favorites\FavoritesService;
39
40use function count;
41use function is_array;
42use function strlen;
43
44/**
45 * Book Bag / Bulk Action Controller
46 *
47 * @category VuFind
48 * @package  Controller
49 * @author   Demian Katz <demian.katz@villanova.edu>
50 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
51 * @link     https://vufind.org Main Site
52 */
53class CartController extends AbstractBase
54{
55    use Feature\BulkActionControllerTrait;
56    use ListItemSelectionTrait;
57
58    /**
59     * Session container
60     *
61     * @var \Laminas\Session\Container
62     */
63    protected $session;
64
65    /**
66     * Configuration loader
67     *
68     * @var \VuFind\Config\PluginManager
69     */
70    protected $configLoader;
71
72    /**
73     * Export support class
74     *
75     * @var \VuFind\Export
76     */
77    protected $export;
78
79    /**
80     * Constructor
81     *
82     * @param ServiceLocatorInterface      $sm           Service manager
83     * @param Container                    $container    Session container
84     * @param \VuFind\Config\PluginManager $configLoader Configuration loader
85     * @param \VuFind\Export               $export       Export support class
86     */
87    public function __construct(
88        ServiceLocatorInterface $sm,
89        Container $container,
90        \VuFind\Config\PluginManager $configLoader,
91        \VuFind\Export $export
92    ) {
93        parent::__construct($sm);
94        $this->session = $container;
95        $this->configLoader = $configLoader;
96        $this->export = $export;
97    }
98
99    /**
100     * Get the cart object.
101     *
102     * @return \VuFind\Cart
103     */
104    protected function getCart()
105    {
106        return $this->serviceLocator->get(\VuFind\Cart::class);
107    }
108
109    /**
110     * Figure out an action from the request....
111     *
112     * @param string $default Default action if none can be determined.
113     *
114     * @return string
115     */
116    protected function getCartActionFromRequest($default = 'Home')
117    {
118        if (strlen($this->params()->fromPost('email', '')) > 0) {
119            return 'Email';
120        } elseif (strlen($this->params()->fromPost('print', '')) > 0) {
121            return 'PrintCart';
122        } elseif (strlen($this->params()->fromPost('saveCart', '')) > 0) {
123            return 'Save';
124        } elseif (strlen($this->params()->fromPost('export', '')) > 0) {
125            return 'Export';
126        }
127        // Check if the user is in the midst of a login process; if not,
128        // use the provided default.
129        return $this->followup()->retrieveAndClear('cartAction', $default);
130    }
131
132    /**
133     * Process requests for bulk actions from search results.
134     *
135     * @return mixed
136     */
137    public function searchresultsbulkAction()
138    {
139        // We came in from a search, so let's remember that context so we can
140        // return to it later. However, if we came in from a previous instance
141        // of this action (for example, because of a login screen), or if we
142        // have an external site in the referer, we should ignore that!
143        $referer = $this->getRequest()->getServer()->get('HTTP_REFERER');
144        $bulk = $this->url()->fromRoute('cart-searchresultsbulk');
145        if ($this->isLocalUrl($referer) && !str_ends_with($referer, $bulk)) {
146            $this->session->url = $referer;
147        }
148
149        // Now forward to the requested action:
150        return $this->forwardTo('Cart', $this->getCartActionFromRequest());
151    }
152
153    /**
154     * Process requests for main cart.
155     *
156     * @return mixed
157     */
158    public function processorAction()
159    {
160        // We came in from the cart -- let's remember this so we can redirect there
161        // when we're done:
162        $this->session->url = $this->url()->fromRoute('cart-home');
163
164        // Now forward to the requested action:
165        return $this->forwardTo('Cart', $this->getCartActionFromRequest());
166    }
167
168    /**
169     * Display cart contents.
170     *
171     * @return mixed
172     */
173    public function homeAction()
174    {
175        // Bail out if cart is disabled.
176        if (!$this->getCart()->isActive()) {
177            return $this->redirect()->toRoute('home');
178        }
179
180        // If a user is coming directly to the cart, we should clear out any
181        // existing context information to prevent weird, unexpected workflows
182        // caused by unusual user behavior.
183        $this->followup()->retrieveAndClear('cartAction');
184        $this->followup()->retrieveAndClear('cartIds');
185
186        $ids = $this->getSelectedIds();
187
188        // Add items if necessary:
189        if (strlen($this->params()->fromPost('empty', '')) > 0) {
190            $this->getCart()->emptyCart();
191        } elseif (strlen($this->params()->fromPost('delete', '')) > 0) {
192            if (empty($ids)) {
193                return $this->redirectToSource('error', 'bulk_noitems_advice');
194            } else {
195                $this->getCart()->removeItems($ids);
196            }
197        } elseif (strlen($this->params()->fromPost('add', '')) > 0) {
198            if (empty($ids)) {
199                return $this->redirectToSource('error', 'bulk_noitems_advice');
200            } else {
201                $addItems = $this->getCart()->addItems($ids);
202                if (!$addItems['success']) {
203                    $msg = $this->translate('bookbag_full_msg') . '. '
204                        . $addItems['notAdded'] . ' '
205                        . $this->translate('items_already_in_bookbag') . '.';
206                    $this->flashMessenger()->addMessage($msg, 'info');
207                }
208            }
209        }
210        // Using the cart/cart template for the cart/home action is a legacy of
211        // an earlier controller design; we may want to rename the template for
212        // clarity, but right now we are retaining the old template name for
213        // backward compatibility.
214        $view = $this->createViewModel();
215        $view->setTemplate('cart/cart');
216        return $view;
217    }
218
219    /**
220     * Process bulk actions from the MyResearch area; most of this is only necessary
221     * when Javascript is disabled.
222     *
223     * @return mixed
224     */
225    public function myresearchbulkAction()
226    {
227        // We came in from the MyResearch section -- let's remember which list (if
228        // any) we came from so we can redirect there when we're done:
229        $listID = $this->params()->fromPost('listID');
230        $this->session->url = empty($listID)
231            ? $this->url()->fromRoute('myresearch-favorites')
232            : $this->url()->fromRoute('userList', ['id' => $listID]);
233
234        // Now forward to the requested controller/action:
235        $controller = 'Cart';   // assume Cart unless overridden below.
236        if (strlen($this->params()->fromPost('email', '')) > 0) {
237            $action = 'Email';
238        } elseif (strlen($this->params()->fromPost('print', '')) > 0) {
239            $action = 'PrintCart';
240        } elseif (strlen($this->params()->fromPost('delete', '')) > 0) {
241            $controller = 'MyResearch';
242            $action = 'Delete';
243        } elseif (strlen($this->params()->fromPost('add', '')) > 0) {
244            $action = 'Home';
245        } elseif (strlen($this->params()->fromPost('export', '')) > 0) {
246            $action = 'Export';
247        } else {
248            $action = $this->followup()->retrieveAndClear('cartAction', null);
249            if (empty($action)) {
250                throw new \Exception('Unrecognized bulk action.');
251            }
252        }
253        return $this->forwardTo($controller, $action);
254    }
255
256    /**
257     * Email a batch of records.
258     *
259     * @return mixed
260     */
261    public function emailAction()
262    {
263        // Retrieve ID list:
264        $ids = $this->getSelectedIds();
265
266        // Retrieve follow-up information if necessary:
267        if (!is_array($ids) || empty($ids)) {
268            $ids = $this->followup()->retrieveAndClear('cartIds') ?? [];
269        }
270        $actionLimit = $this->getBulkActionLimit('email');
271        if (!is_array($ids) || empty($ids)) {
272            if ($redirect = $this->redirectToSource('error', 'bulk_noitems_advice')) {
273                return $redirect;
274            }
275            $submitDisabled = true;
276        } elseif (count($ids) > $actionLimit) {
277            $errorMsg = $this->translate(
278                'bulk_limit_exceeded',
279                ['%%count%%' => count($ids), '%%limit%%' => $actionLimit],
280            );
281            if ($redirect = $this->redirectToSource('error', $errorMsg)) {
282                return $redirect;
283            }
284            $submitDisabled = true;
285        }
286
287        // Force login if necessary:
288        $config = $this->getConfig();
289        if (
290            (!isset($config->Mail->require_login) || $config->Mail->require_login)
291            && !$this->getUser()
292        ) {
293            return $this->forceLogin(
294                null,
295                ['cartIds' => $ids, 'cartAction' => 'Email']
296            );
297        }
298
299        $view = $this->createEmailViewModel(
300            null,
301            $this->translate('bulk_email_title')
302        );
303        $view->records = $this->getRecordLoader()->loadBatch($ids);
304        // Set up Captcha
305        $view->useCaptcha = $this->captcha()->active('email');
306
307        // Process form submission:
308        if (!($submitDisabled ?? false) && $this->formWasSubmitted(useCaptcha: $view->useCaptcha)) {
309            // Build the URL to share:
310            $params = [];
311            foreach ($ids as $current) {
312                $params[] = urlencode('id[]') . '=' . urlencode($current);
313            }
314            $url = $this->getServerUrl('records-home') . '?' . implode('&', $params);
315
316            // Attempt to send the email and show an appropriate flash message:
317            try {
318                // If we got this far, we're ready to send the email:
319                $mailer = $this->serviceLocator->get(\VuFind\Mailer\Mailer::class);
320                $mailer->setMaxRecipients($view->maxRecipients);
321                $cc = $this->params()->fromPost('ccself') && $view->from != $view->to
322                    ? $view->from : null;
323                $mailer->sendLink(
324                    $view->to,
325                    $view->from,
326                    $view->message,
327                    $url,
328                    $this->getViewRenderer(),
329                    $view->subject,
330                    $cc
331                );
332                return $this->redirectToSource('success', 'bulk_email_success', true);
333            } catch (MailException $e) {
334                $this->flashMessenger()->addMessage($e->getDisplayMessage(), 'error');
335            }
336        }
337
338        return $view;
339    }
340
341    /**
342     * Print a batch of records.
343     *
344     * @return mixed
345     */
346    public function printcartAction()
347    {
348        $ids = $this->getSelectedIds();
349        if (!is_array($ids) || empty($ids)) {
350            return $this->redirectToSource('error', 'bulk_noitems_advice');
351        }
352
353        // Check if id limit is exceeded
354        $actionLimit = $this->getBulkActionLimit('print');
355        if (count($ids) > $actionLimit) {
356            $errorMsg = $this->translate(
357                'bulk_limit_exceeded',
358                ['%%count%%' => count($ids), '%%limit%%' => $actionLimit],
359            );
360            return $this->redirectToSource('error', $errorMsg);
361        }
362
363        $callback = function ($i) {
364            return 'id[]=' . urlencode($i);
365        };
366        $query = '?print=true&' . implode('&', array_map($callback, $ids));
367        $url = $this->url()->fromRoute('records-home') . $query;
368        return $this->redirect()->toUrl($url);
369    }
370
371    /**
372     * Set up export of a batch of records.
373     *
374     * @return mixed
375     */
376    public function exportAction()
377    {
378        // Get the desired ID list:
379        $ids = $this->getSelectedIds();
380
381        // Get export tools:
382        $export = $this->export;
383
384        // Get id limit
385        $format = $this->params()->fromPost('format');
386        $actionLimit = $format ? $this->getExportActionLimit($format) : $this->getBulkActionLimit('export');
387
388        if (!is_array($ids) || empty($ids)) {
389            if ($redirect = $this->redirectToSource('error', 'bulk_noitems_advice')) {
390                return $redirect;
391            }
392        } elseif (count($ids) > $actionLimit) {
393            $errorMsg = $this->translate(
394                'bulk_limit_exceeded',
395                ['%%count%%' => count($ids), '%%limit%%' => $actionLimit],
396            );
397            if ($redirect = $this->redirectToSource('error', $errorMsg)) {
398                return $redirect;
399            }
400        } elseif ($this->formWasSubmitted()) {
401            $url = $export->getBulkUrl($this->getViewRenderer(), $format, $ids);
402            if ($export->needsRedirect($format)) {
403                return $this->redirect()->toUrl($url);
404            }
405            $exportType = $export->getBulkExportType($format);
406            $params = [
407                'exportType' => $exportType,
408                'format' => $format,
409            ];
410            if ('post' === $exportType) {
411                $records = $this->getRecordLoader()->loadBatch($ids);
412                $recordHelper = $this->getViewRenderer()->plugin('record');
413                $parts = [];
414                foreach ($records as $record) {
415                    $parts[] = $recordHelper($record)->getExport($format);
416                }
417
418                $params['postField'] = $export->getPostField($format);
419                $params['postData'] = $export->processGroup($format, $parts);
420                $params['targetWindow'] = $export->getTargetWindow($format);
421                $params['url'] = $export->getRedirectUrl($format, '');
422            } else {
423                $params['url'] = $url;
424            }
425            $msg = [
426                'translate' => false, 'html' => true,
427                'msg' => $this->getViewRenderer()->render(
428                    'cart/export-success.phtml',
429                    $params
430                ),
431            ];
432            return $this->redirectToSource('success', $msg, true);
433        }
434
435        // Load the records:
436        $view = $this->createViewModel();
437        $view->records = $this->getRecordLoader()->loadBatch($ids);
438
439        // Assign the list of legal export options. We'll filter them down based
440        // on what the selected records actually support.
441        $view->exportOptions = $export->getFormatsForRecords($view->records);
442
443        // No legal export options?  Display a warning:
444        if (empty($view->exportOptions)) {
445            $this->flashMessenger()
446                ->addMessage('bulk_export_not_supported', 'error');
447        }
448        return $view;
449    }
450
451    /**
452     * Actually perform the export operation.
453     *
454     * @return mixed
455     */
456    public function doexportAction()
457    {
458        // We use abbreviated parameters here to keep the URL short (there may
459        // be a long list of IDs, and we don't want to run out of room):
460        $ids = $this->params()->fromQuery('i', []);
461        $format = $this->params()->fromQuery('f');
462
463        // Make sure we have IDs to export:
464        if (!is_array($ids) || empty($ids)) {
465            return $this->redirectToSource('error', 'bulk_noitems_advice');
466        }
467
468        // Check if id limit is exceeded
469        $actionLimit = $this->getExportActionLimit($format);
470        if (count($ids) > $actionLimit) {
471            return $this->redirectToSource('error', 'bulk_limit_exceeded');
472        }
473
474        // Send appropriate HTTP headers for requested format:
475        $response = $this->getResponse();
476        $response->getHeaders()->addHeaders($this->export->getHeaders($format));
477
478        // Actually export the records
479        $records = $this->getRecordLoader()->loadBatch($ids);
480        $recordHelper = $this->getViewRenderer()->plugin('record');
481        $parts = [];
482        foreach ($records as $record) {
483            $parts[] = $recordHelper($record)->getExport($format);
484        }
485
486        // Process and display the exported records
487        $response->setContent($this->export->processGroup($format, $parts));
488        return $response;
489    }
490
491    /**
492     * Save a batch of records.
493     *
494     * @return mixed
495     */
496    public function saveAction()
497    {
498        // Fail if lists are disabled:
499        if (!$this->listsEnabled()) {
500            throw new ForbiddenException('Lists disabled');
501        }
502
503        // Load record information first (no need to prompt for login if we just
504        // need to display a "no records" error message):
505        $ids = $this->getSelectedIds();
506        if (!is_array($ids) || empty($ids)) {
507            $ids = $this->followup()->retrieveAndClear('cartIds') ?? [];
508        }
509        $actionLimit = $this->getBulkActionLimit('saveCart');
510        if (!is_array($ids) || empty($ids)) {
511            if ($redirect = $this->redirectToSource('error', 'bulk_noitems_advice')) {
512                return $redirect;
513            }
514            $submitDisabled = true;
515        } elseif (count($ids) > $actionLimit) {
516            $errorMsg = $this->translate(
517                'bulk_limit_exceeded',
518                ['%%count%%' => count($ids), '%%limit%%' => $actionLimit],
519            );
520            if ($redirect = $this->redirectToSource('error', $errorMsg)) {
521                return $redirect;
522            }
523            $submitDisabled = true;
524        }
525
526        // Make sure user is logged in:
527        if (!($user = $this->getUser())) {
528            return $this->forceLogin(
529                null,
530                ['cartIds' => $ids, 'cartAction' => 'Save']
531            );
532        }
533
534        // Process submission if necessary:
535        if (!($submitDisabled ?? false) && $this->formWasSubmitted()) {
536            $results = $this->serviceLocator->get(FavoritesService::class)
537                ->saveRecordsToFavorites($this->getRequest()->getPost()->toArray(), $user);
538            $listUrl = $this->url()->fromRoute(
539                'userList',
540                ['id' => $results['listId']]
541            );
542            $message = [
543                'html' => true,
544                'msg' => $this->translate('bulk_save_success') . '. '
545                . '<a href="' . $listUrl . '" class="gotolist">'
546                . $this->translate('go_to_list') . '</a>.',
547            ];
548            $this->flashMessenger()->addMessage($message, 'success');
549            return $this->redirect()->toUrl($listUrl);
550        }
551
552        // Pass record and list information to view:
553        return $this->createViewModel(
554            [
555                'records' => $this->getRecordLoader()->loadBatch($ids),
556                'lists' => $this->getDbService(UserListServiceInterface::class)->getUserListsByUser($user),
557            ]
558        );
559    }
560}