Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 136
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
Holds
0.00% covered (danger)
0.00%
0 / 136
0.00% covered (danger)
0.00%
0 / 4
1892
0.00% covered (danger)
0.00%
0 / 1
 addCancelDetails
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
 cancelHolds
0.00% covered (danger)
0.00%
0 / 64
0.00% covered (danger)
0.00%
0 / 1
182
 validateDates
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
380
 validateFrozenThrough
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3/**
4 * VuFind Action Helper - Holds Support Methods
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Villanova University 2010.
9 * Copyright (C) The National Library of Finland 2021.
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_Plugins
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 Page
30 */
31
32namespace VuFind\Controller\Plugin;
33
34use VuFind\Date\DateException;
35
36use function in_array;
37
38/**
39 * Action helper to perform holds-related actions
40 *
41 * @category VuFind
42 * @package  Controller_Plugins
43 * @author   Demian Katz <demian.katz@villanova.edu>
44 * @author   Ere Maijala <ere.maijala@helsinki.fi>
45 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
46 * @link     https://vufind.org Main Page
47 */
48class Holds extends AbstractRequestBase
49{
50    /**
51     * Update ILS details with cancellation-specific information, if appropriate.
52     *
53     * @param \VuFind\ILS\Connection $catalog      ILS connection object
54     * @param array                  $ilsDetails   Hold details from ILS driver's
55     * getMyHolds() method
56     * @param array                  $cancelStatus Cancel settings from ILS driver's
57     * checkFunction() method
58     * @param array                  $patron       ILS patron
59     *
60     * @return array $ilsDetails with cancellation info added
61     */
62    public function addCancelDetails(
63        $catalog,
64        $ilsDetails,
65        $cancelStatus,
66        $patron = []
67    ) {
68        // Generate Form Details for cancelling Holds if Cancelling Holds
69        // is enabled
70        if ($cancelStatus) {
71            if ($cancelStatus['function'] == 'getCancelHoldLink') {
72                // Build OPAC URL
73                $ilsDetails['cancel_link']
74                    = $catalog->getCancelHoldLink($ilsDetails, $patron);
75            } elseif (isset($ilsDetails['cancel_details'])) {
76                // The ILS driver provided cancel details up front. If the
77                // details are an empty string (flagging lack of support), we
78                // should unset it to prevent confusion; otherwise, we'll leave it
79                // as-is.
80                if ('' === $ilsDetails['cancel_details']) {
81                    unset($ilsDetails['cancel_details']);
82                } else {
83                    $this->rememberValidId($ilsDetails['cancel_details']);
84                }
85            } else {
86                // Default case: ILS supports cancel but we need to look up
87                // details:
88                $cancelDetails
89                    = $catalog->getCancelHoldDetails($ilsDetails, $patron);
90                if ($cancelDetails !== '') {
91                    $ilsDetails['cancel_details'] = $cancelDetails;
92                    $this->rememberValidId($ilsDetails['cancel_details']);
93                }
94            }
95        } else {
96            // Cancelling holds disabled? Make sure no details get passed back:
97            unset($ilsDetails['cancel_link']);
98            unset($ilsDetails['cancel_details']);
99        }
100
101        return $ilsDetails;
102    }
103
104    /**
105     * Process cancellation requests.
106     *
107     * @param \VuFind\ILS\Connection $catalog ILS connection object
108     * @param array                  $patron  Current logged in patron
109     *
110     * @return array                          The result of the cancellation, an
111     * associative array keyed by item ID (empty if no cancellations performed)
112     */
113    public function cancelHolds($catalog, $patron)
114    {
115        // Retrieve the flashMessenger helper:
116        $flashMsg = $this->getController()->flashMessenger();
117        $params = $this->getController()->params();
118
119        // Pick IDs to cancel based on which button was pressed:
120        $all = $params->fromPost('cancelAll');
121        $selected = $params->fromPost('cancelSelected');
122        if (!empty($all)) {
123            $details = $params->fromPost('cancelAllIDS');
124        } elseif (!empty($selected)) {
125            // Include cancelSelectedIDS for backwards-compatibility:
126            $details = $params->fromPost('selectedIDS')
127                ?? $params->fromPost('cancelSelectedIDS');
128        } else {
129            // No button pushed -- no action needed
130            return [];
131        }
132
133        if (!empty($details)) {
134            // Confirm?
135            if ($params->fromPost('confirm') === '0') {
136                if ($params->fromPost('cancelAll') !== null) {
137                    return $this->getController()->confirm(
138                        'hold_cancel_all',
139                        $this->getController()->url()->fromRoute('holds-list'),
140                        $this->getController()->url()->fromRoute('holds-list'),
141                        'confirm_hold_cancel_all_text',
142                        [
143                            'cancelAll' => 1,
144                            'cancelAllIDS' => $params->fromPost('cancelAllIDS'),
145                        ]
146                    );
147                } else {
148                    return $this->getController()->confirm(
149                        'hold_cancel_selected',
150                        $this->getController()->url()->fromRoute('holds-list'),
151                        $this->getController()->url()->fromRoute('holds-list'),
152                        'confirm_hold_cancel_selected_text',
153                        [
154                            'cancelSelected' => 1,
155                            'cancelSelectedIDS' =>
156                                $params->fromPost('cancelSelectedIDS'),
157                        ]
158                    );
159                }
160            }
161
162            foreach ($details as $info) {
163                // If the user input contains a value not found in the session
164                // legal list, something has been tampered with -- abort the process.
165                if (!in_array($info, $this->getSession()->validIds)) {
166                    $flashMsg->addErrorMessage('error_inconsistent_parameters');
167                    return [];
168                }
169            }
170
171            // Add Patron Data to Submitted Data
172            $cancelResults = $catalog->cancelHolds(
173                ['details' => $details, 'patron' => $patron]
174            );
175            if ($cancelResults == false) {
176                $flashMsg->addMessage('hold_cancel_fail', 'error');
177            } else {
178                $failed = 0;
179                foreach ($cancelResults['items'] ?? [] as $item) {
180                    if (!$item['success']) {
181                        ++$failed;
182                    }
183                }
184                if ($failed) {
185                    $msg = $this->getController()
186                        ->translate(
187                            'hold_cancel_fail_items',
188                            ['%%count%%' => $failed]
189                        );
190                    $flashMsg->addErrorMessage($msg);
191                }
192                if ($cancelResults['count'] > 0) {
193                    $msg = $this->getController()
194                        ->translate(
195                            'hold_cancel_success_items',
196                            ['%%count%%' => $cancelResults['count']]
197                        );
198                    $flashMsg->addSuccessMessage($msg);
199                }
200                return $cancelResults;
201            }
202        } else {
203            $flashMsg->addMessage('hold_empty_selection', 'error');
204        }
205        return [];
206    }
207
208    /**
209     * Check if the user-provided dates are valid.
210     *
211     * Returns validated dates and/or an array of validation errors if there are
212     * problems.
213     *
214     * @param string $startDate         User-specified start date
215     * @param string $requiredBy        User-specified required-by date
216     * @param array  $enabledFormFields Hold form fields enabled by
217     * configuration/driver
218     *
219     * @return array
220     */
221    public function validateDates(
222        ?string $startDate,
223        ?string $requiredBy,
224        array $enabledFormFields
225    ): array {
226        $result = [
227            'startDateTS' => null,
228            'requiredByTS' => null,
229            'errors' => [],
230        ];
231        if (
232            !in_array('startDate', $enabledFormFields)
233            && !in_array('requiredByDate', $enabledFormFields)
234            && !in_array('requiredByDateOptional', $enabledFormFields)
235        ) {
236            return $result;
237        }
238
239        if (in_array('startDate', $enabledFormFields)) {
240            try {
241                $result['startDateTS'] = $startDate
242                    ? (int)$this->dateConverter->convertFromDisplayDate(
243                        'U',
244                        $startDate
245                    ) : 0;
246                if ($result['startDateTS'] < strtotime('today')) {
247                    $result['errors'][] = 'hold_start_date_invalid';
248                }
249            } catch (DateException $e) {
250                $result['errors'][] = 'hold_start_date_invalid';
251            }
252        }
253
254        if (
255            in_array('requiredByDate', $enabledFormFields)
256            || in_array('requiredByDateOptional', $enabledFormFields)
257        ) {
258            $optional = in_array('requiredByDateOptional', $enabledFormFields);
259            try {
260                if ($requiredBy) {
261                    $requiredByDateTime = \DateTime::createFromFormat(
262                        'U',
263                        $this->dateConverter
264                            ->convertFromDisplayDate('U', $requiredBy)
265                    );
266                    $result['requiredByTS'] = $requiredByDateTime
267                        ->setTime(23, 59, 59)
268                        ->getTimestamp();
269                } else {
270                    $result['requiredByTS'] = 0;
271                }
272                if (
273                    (!$optional || $result['requiredByTS'])
274                    && $result['requiredByTS'] < strtotime('today')
275                ) {
276                    $result['errors'][] = 'hold_required_by_date_invalid';
277                }
278            } catch (DateException $e) {
279                $result['errors'][] = 'hold_required_by_date_invalid';
280            }
281        }
282
283        if (
284            !$result['errors']
285            && in_array('startDate', $enabledFormFields)
286            && !empty($result['requiredByTS'])
287            && $result['startDateTS'] > $result['requiredByTS']
288        ) {
289            $result['errors'][] = 'hold_required_by_date_before_start_date';
290        }
291
292        return $result;
293    }
294
295    /**
296     * Check if the user-provided "frozen through" date is valid.
297     *
298     * Returns validated date and/or an array of validation errors if there are
299     * problems.
300     *
301     * @param string $frozenThrough   User-specified "frozen through" date
302     * @param array  $extraHoldFields Hold form fields enabled by
303     * configuration/driver
304     *
305     * @return array
306     */
307    public function validateFrozenThrough(
308        ?string $frozenThrough,
309        array $extraHoldFields
310    ): array {
311        $result = [
312            'frozenThroughTS' => null,
313            'errors' => [],
314        ];
315        if (!in_array('frozenThrough', $extraHoldFields) || empty($frozenThrough)) {
316            return $result;
317        }
318
319        try {
320            $result['frozenThroughTS']
321                = $this->dateConverter->convertFromDisplayDate('U', $frozenThrough);
322            if ($result['frozenThroughTS'] < time()) {
323                $result['errors'][] = 'hold_frozen_through_date_invalid';
324            }
325        } catch (DateException $e) {
326            $result['errors'][] = 'hold_frozen_through_date_invalid';
327        }
328
329        return $result;
330    }
331}