Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
45.21% |
302 / 668 |
|
34.92% |
22 / 63 |
CRAP | |
0.00% |
0 / 1 |
PAIA | |
45.21% |
302 / 668 |
|
34.92% |
22 / 63 |
7254.82 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getCacheKey | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getSession | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getScope | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
init | |
76.92% |
10 / 13 |
|
0.00% |
0 / 1 |
5.31 | |||
cancelHolds | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
72 | |||
changePassword | |
43.48% |
20 / 46 |
|
0.00% |
0 / 1 |
15.85 | |||
getCancelHoldDetails | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDefaultPickUpLocation | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getFunds | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
cancelStorageRetrievalRequests | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCancelStorageRetrievalRequestDetails | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMyILLRequests | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
checkILLRequestIsValid | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
placeILLRequest | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getILLPickupLibraries | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getILLPickupLocations | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
cancelILLRequests | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCancelILLRequestDetails | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMyFines | |
95.65% |
22 / 23 |
|
0.00% |
0 / 1 |
7 | |||
getAdditionalFeeData | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getMyHolds | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getMyProfile | |
92.59% |
25 / 27 |
|
0.00% |
0 / 1 |
4.01 | |||
getReadableGroupType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getMyTransactions | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getMyStorageRetrievalRequests | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getNewItems | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPickUpLocations | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRenewDetails | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCallNumber | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
patronLogin | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
72 | |||
paiaHandleErrors | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
182 | |||
enrichUserDetails | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getConfirmations | |
60.00% |
3 / 5 |
|
0.00% |
0 / 1 |
2.26 | |||
placeHold | |
54.55% |
24 / 44 |
|
0.00% |
0 / 1 |
16.61 | |||
placeStorageRetrievalRequest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
renewMyItems | |
52.83% |
28 / 53 |
|
0.00% |
0 / 1 |
27.11 | |||
paiaStatusString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
paiaGetItems | |
74.07% |
20 / 27 |
|
0.00% |
0 / 1 |
15.95 | |||
getAlternativeItemId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
paiaParseUserDetails | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
mapPaiaItems | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
2.50 | |||
getBasicDetails | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
6 | |||
myHoldsMapping | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
6 | |||
myStorageRetrievalRequestsMapping | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
myTransactionsMapping | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
10 | |||
paiaPostRequest | |
70.59% |
12 / 17 |
|
0.00% |
0 / 1 |
3.23 | |||
paiaGetRequest | |
75.00% |
12 / 16 |
|
0.00% |
0 / 1 |
2.06 | |||
paiaParseJsonAsArray | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
paiaGetAsArray | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
paiaPostAsArray | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
paiaLogin | |
0.00% |
0 / 64 |
|
0.00% |
0 / 1 |
132 | |||
paiaGetUserDetails | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
paiaCheckScope | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
3 | |||
checkStorageRetrievalRequestIsValid | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
checkRequestIsValid | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
6 | |||
paiaGetSystemMessages | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
42 | |||
enrichNotifications | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
paiaRemoveSystemMessage | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
30 | |||
paiaRemoveSystemMessages | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getPaiaNotificationsId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
paiaDeleteRequest | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
20 | |||
getAccountBlocks | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 |
1 | <?php |
2 | |
3 | /** |
4 | * PAIA ILS Driver for VuFind to get patron information |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Oliver Goldschmidt, Magda Roos, Till Kinstler, André Lahmann 2013, |
9 | * 2014, 2015. |
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 ILS_Drivers |
26 | * @author Oliver Goldschmidt <o.goldschmidt@tuhh.de> |
27 | * @author Magdalena Roos <roos@gbv.de> |
28 | * @author Till Kinstler <kinstler@gbv.de> |
29 | * @author André Lahmann <lahmann@ub.uni-leipzig.de> |
30 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
31 | * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki |
32 | */ |
33 | |
34 | namespace VuFind\ILS\Driver; |
35 | |
36 | use VuFind\Exception\Auth as AuthException; |
37 | use VuFind\Exception\Forbidden as ForbiddenException; |
38 | use VuFind\Exception\ILS as ILSException; |
39 | |
40 | use function count; |
41 | use function in_array; |
42 | use function is_array; |
43 | use function is_callable; |
44 | |
45 | /** |
46 | * PAIA ILS Driver for VuFind to get patron information |
47 | * |
48 | * Holding information is obtained by DAIA, so it's not necessary to implement those |
49 | * functions here; we just need to extend the DAIA driver. |
50 | * |
51 | * @category VuFind |
52 | * @package ILS_Drivers |
53 | * @author Oliver Goldschmidt <o.goldschmidt@tuhh.de> |
54 | * @author Magdalena Roos <roos@gbv.de> |
55 | * @author Till Kinstler <kinstler@gbv.de> |
56 | * @author André Lahmann <lahmann@ub.uni-leipzig.de> |
57 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
58 | * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki |
59 | */ |
60 | class PAIA extends DAIA |
61 | { |
62 | /** |
63 | * URL of PAIA service |
64 | * |
65 | * @var string |
66 | */ |
67 | protected $paiaURL; |
68 | |
69 | /** |
70 | * Accepted grant_type for authorization |
71 | * |
72 | * @var string |
73 | */ |
74 | protected $grantType = 'password'; |
75 | |
76 | /** |
77 | * Timeout in seconds to be used for PAIA http requests |
78 | * |
79 | * @var int |
80 | */ |
81 | protected $paiaTimeout = null; |
82 | |
83 | /** |
84 | * Flag to switch on/off caching for PAIA items |
85 | * |
86 | * @var bool |
87 | */ |
88 | protected $paiaCacheEnabled = false; |
89 | |
90 | /** |
91 | * Session containing PAIA login information |
92 | * |
93 | * @var \Laminas\Session\Container |
94 | */ |
95 | protected $session; |
96 | |
97 | /** |
98 | * SessionManager |
99 | * |
100 | * @var \Laminas\Session\SessionManager |
101 | */ |
102 | protected $sessionManager; |
103 | |
104 | /** |
105 | * PAIA status strings |
106 | * |
107 | * @var array |
108 | */ |
109 | protected static $statusStrings = [ |
110 | '0' => 'no relation', |
111 | '1' => 'reserved', |
112 | '2' => 'ordered', |
113 | '3' => 'held', |
114 | '4' => 'provided', |
115 | '5' => 'rejected', |
116 | ]; |
117 | |
118 | /** |
119 | * Account blocks that should be reported to the user. |
120 | * |
121 | * @see method `getAccountBlocks` |
122 | * @var array |
123 | */ |
124 | protected $accountBlockNotificationsForMissingScopes; |
125 | |
126 | /** |
127 | * PAIA scopes as defined in |
128 | * http://gbv.github.io/paia/paia.html#access-tokens-and-scopes |
129 | * |
130 | * Notice: logged in users should ALWAYS have scope read_patron as the PAIA |
131 | * driver performs paiaGetUserDetails() upon each call of VuFind's patronLogin(). |
132 | * That means if paiaGetUserDetails() fails (which is the case if the patron has |
133 | * NOT the scope read_patron) the patronLogin() will fail as well even though |
134 | * paiaLogin() might have succeeded. Any other scope not being available for the |
135 | * patron will be handled more or less gracefully through exception handling. |
136 | */ |
137 | public const SCOPE_READ_PATRON = 'read_patron'; |
138 | public const SCOPE_UPDATE_PATRON = 'update_patron'; |
139 | public const SCOPE_UPDATE_PATRON_NAME = 'update_patron_name'; |
140 | public const SCOPE_UPDATE_PATRON_EMAIL = 'update_patron_email'; |
141 | public const SCOPE_UPDATE_PATRON_ADDRESS = 'update_patron_address'; |
142 | public const SCOPE_READ_FEES = 'read_fees'; |
143 | public const SCOPE_READ_ITEMS = 'read_items'; |
144 | public const SCOPE_WRITE_ITEMS = 'write_items'; |
145 | public const SCOPE_CHANGE_PASSWORD = 'change_password'; |
146 | public const SCOPE_READ_NOTIFICATIONS = 'read_notifications'; |
147 | public const SCOPE_DELETE_NOTIFICATIONS = 'delete_notifications'; |
148 | |
149 | /** |
150 | * Constructor |
151 | * |
152 | * @param \VuFind\Date\Converter $converter Date converter |
153 | * @param \Laminas\Session\SessionManager $sessionManager Session Manager |
154 | */ |
155 | public function __construct( |
156 | \VuFind\Date\Converter $converter, |
157 | \Laminas\Session\SessionManager $sessionManager |
158 | ) { |
159 | parent::__construct($converter); |
160 | $this->sessionManager = $sessionManager; |
161 | } |
162 | |
163 | /** |
164 | * PAIA specific override of method to ensure uniform cache keys for cached |
165 | * VuFind objects. |
166 | * |
167 | * @param string|null $suffix Optional suffix that will get appended to the |
168 | * object class name calling getCacheKey() |
169 | * |
170 | * @return string |
171 | */ |
172 | protected function getCacheKey($suffix = null) |
173 | { |
174 | return $this->getBaseCacheKey( |
175 | md5($this->baseUrl . $this->paiaURL) . $suffix |
176 | ); |
177 | } |
178 | |
179 | /** |
180 | * Get the session container (constructing it on demand if not already present) |
181 | * |
182 | * @return SessionContainer |
183 | */ |
184 | protected function getSession() |
185 | { |
186 | // SessionContainer not defined yet? Build it now: |
187 | if (null === $this->session) { |
188 | $this->session = new \Laminas\Session\Container( |
189 | 'PAIA', |
190 | $this->sessionManager |
191 | ); |
192 | } |
193 | return $this->session; |
194 | } |
195 | |
196 | /** |
197 | * Get the session scope |
198 | * |
199 | * @return array Array of the Session scope |
200 | */ |
201 | protected function getScope() |
202 | { |
203 | return $this->getSession()->scope; |
204 | } |
205 | |
206 | /** |
207 | * Initialize the driver. |
208 | * |
209 | * Validate configuration and perform all resource-intensive tasks needed to |
210 | * make the driver active. |
211 | * |
212 | * @throws ILSException |
213 | * @return void |
214 | */ |
215 | public function init() |
216 | { |
217 | parent::init(); |
218 | |
219 | if (!(isset($this->config['PAIA']['baseUrl']))) { |
220 | throw new ILSException('PAIA/baseUrl configuration needs to be set.'); |
221 | } |
222 | $this->paiaURL = $this->config['PAIA']['baseUrl']; |
223 | |
224 | // read configured grantType |
225 | if (isset($this->config['PAIA']['grantType'])) { |
226 | $this->grantType = $this->config['PAIA']['grantType']; |
227 | } |
228 | |
229 | // use PAIA specific timeout setting for http requests if configured |
230 | if ((isset($this->config['PAIA']['timeout']))) { |
231 | $this->paiaTimeout = $this->config['PAIA']['timeout']; |
232 | } |
233 | |
234 | // do we have caching enabled for PAIA |
235 | if (isset($this->config['PAIA']['paiaCache'])) { |
236 | $this->paiaCacheEnabled = $this->config['PAIA']['paiaCache']; |
237 | } else { |
238 | $this->debug('Caching not enabled, disabling it by default.'); |
239 | } |
240 | |
241 | $this->accountBlockNotificationsForMissingScopes = |
242 | $this->config['PAIA']['accountBlockNotificationsForMissingScopes'] ?? []; |
243 | } |
244 | |
245 | // public functions implemented to satisfy Driver Interface |
246 | |
247 | /* |
248 | These methods are not implemented in the PAIA driver as they are probably |
249 | not necessary in PAIA context: |
250 | - findReserves |
251 | - getCancelHoldLink |
252 | - getConsortialHoldings |
253 | - getCourses |
254 | - getDepartments |
255 | - getHoldDefaultRequiredDate |
256 | - getInstructors |
257 | - getOfflineMode |
258 | - getSuppressedAuthorityRecords |
259 | - getSuppressedRecords |
260 | - hasHoldings |
261 | - loginIsHidden |
262 | - renewMyItemsLink |
263 | - supportsMethod |
264 | */ |
265 | |
266 | /** |
267 | * This method cancels a list of holds for a specific patron. |
268 | * |
269 | * @param array $cancelDetails An associative array with two keys: |
270 | * patron array returned by the driver's patronLogin method |
271 | * details an array of strings returned by the driver's |
272 | * getCancelHoldDetails method |
273 | * |
274 | * @return array Associative array containing: |
275 | * count The number of items successfully cancelled |
276 | * items Associative array where key matches one of the item_id |
277 | * values returned by getMyHolds and the value is an |
278 | * associative array with these keys: |
279 | * success Boolean true or false |
280 | * status A status message from the language file |
281 | * (required – VuFind-specific message, |
282 | * subject to translation) |
283 | * sysMessage A system supplied failure message |
284 | */ |
285 | public function cancelHolds($cancelDetails) |
286 | { |
287 | // check if user has appropriate scope (refer to scope declaration above for |
288 | // further details) |
289 | if (!$this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) { |
290 | throw new ForbiddenException( |
291 | 'Exception::access_denied_write_items' |
292 | ); |
293 | } |
294 | |
295 | $it = $cancelDetails['details']; |
296 | $items = []; |
297 | foreach ($it as $item) { |
298 | $items[] = ['item' => stripslashes($item)]; |
299 | } |
300 | $patron = $cancelDetails['patron']; |
301 | $post_data = ['doc' => $items]; |
302 | |
303 | try { |
304 | $array_response = $this->paiaPostAsArray( |
305 | 'core/' . $patron['cat_username'] . '/cancel', |
306 | $post_data |
307 | ); |
308 | } catch (\Exception $e) { |
309 | $this->debug($e->getMessage()); |
310 | return [ |
311 | 'success' => false, |
312 | 'status' => $e->getMessage(), |
313 | ]; |
314 | } |
315 | |
316 | $details = []; |
317 | $count = 0; |
318 | if (isset($array_response['error'])) { |
319 | $details[] = [ |
320 | 'success' => false, |
321 | 'status' => $array_response['error_description'], |
322 | 'sysMessage' => $array_response['error'], |
323 | ]; |
324 | } else { |
325 | $elements = $array_response['doc']; |
326 | foreach ($elements as $element) { |
327 | $item_id = $element['item']; |
328 | if ($element['error'] ?? false) { |
329 | $details[$item_id] = [ |
330 | 'success' => false, |
331 | 'status' => $element['error'], |
332 | 'sysMessage' => 'Cancel request rejected', |
333 | ]; |
334 | } else { |
335 | $details[$item_id] = [ |
336 | 'success' => true, |
337 | 'status' => 'Success', |
338 | 'sysMessage' => 'Successfully cancelled', |
339 | ]; |
340 | $count++; |
341 | |
342 | // DAIA cache cannot be cleared for particular item as PAIA only |
343 | // operates with specific item URIs and the DAIA cache is setup |
344 | // by doc URIs (containing items with URIs) |
345 | } |
346 | } |
347 | |
348 | // If caching is enabled for PAIA clear the cache as at least for one |
349 | // item cancel was successful and therefore the status changed. |
350 | // Otherwise the changed status will not be shown before the cache |
351 | // expires. |
352 | if ($this->paiaCacheEnabled) { |
353 | $this->removeCachedData($patron['cat_username']); |
354 | } |
355 | } |
356 | $returnArray = ['count' => $count, 'items' => $details]; |
357 | |
358 | return $returnArray; |
359 | } |
360 | |
361 | /** |
362 | * Public Function which changes the password in the library system |
363 | * (not supported prior to VuFind 2.4) |
364 | * |
365 | * @param array $details Array with patron information, newPassword and |
366 | * oldPassword. |
367 | * |
368 | * @return array An array with patron information. |
369 | */ |
370 | public function changePassword($details) |
371 | { |
372 | // check if user has appropriate scope (refer to scope declaration above for |
373 | // further details) |
374 | if (!$this->paiaCheckScope(self::SCOPE_CHANGE_PASSWORD)) { |
375 | throw new ForbiddenException( |
376 | 'Exception::access_denied_change_password' |
377 | ); |
378 | } |
379 | |
380 | $post_data = [ |
381 | 'patron' => $details['patron']['cat_username'], |
382 | 'username' => $details['patron']['cat_username'], |
383 | 'old_password' => $details['oldPassword'], |
384 | 'new_password' => $details['newPassword'], |
385 | ]; |
386 | |
387 | try { |
388 | $array_response = $this->paiaPostAsArray( |
389 | 'auth/change', |
390 | $post_data |
391 | ); |
392 | } catch (AuthException $e) { |
393 | return [ |
394 | 'success' => false, |
395 | 'status' => 'password_error_auth_old', |
396 | ]; |
397 | } catch (\Exception $e) { |
398 | $this->debug($e->getMessage()); |
399 | return [ |
400 | 'success' => false, |
401 | 'status' => $e->getMessage(), |
402 | ]; |
403 | } |
404 | |
405 | $details = []; |
406 | |
407 | if (isset($array_response['error'])) { |
408 | // on error |
409 | $details = [ |
410 | 'success' => false, |
411 | 'status' => $array_response['error'], |
412 | 'sysMessage' => |
413 | $array_response['error'] ?? ' ' . |
414 | $array_response['error_description'] ?? ' ', |
415 | ]; |
416 | } elseif ( |
417 | isset($array_response['patron']) |
418 | && $array_response['patron'] === $post_data['patron'] |
419 | ) { |
420 | // on success patron_id is returned |
421 | $details = [ |
422 | 'success' => true, |
423 | 'status' => 'Successfully changed', |
424 | ]; |
425 | } else { |
426 | $details = [ |
427 | 'success' => false, |
428 | 'status' => 'Failure changing password', |
429 | 'sysMessage' => serialize($array_response), |
430 | ]; |
431 | } |
432 | return $details; |
433 | } |
434 | |
435 | /** |
436 | * This method returns a string to use as the input form value for |
437 | * cancelling each hold item. (optional, but required if you |
438 | * implement cancelHolds). Not supported prior to VuFind 1.2 |
439 | * |
440 | * @param array $hold A single hold array from getMyHolds |
441 | * @param array $patron Patron information from patronLogin |
442 | * |
443 | * @return string A string to use as the input form value for cancelling |
444 | * each hold item; you can pass any data that is needed |
445 | * by your ILS to identify the hold – the output of this |
446 | * method will be used as part of the input to the |
447 | * cancelHolds method. |
448 | * |
449 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
450 | */ |
451 | public function getCancelHoldDetails($hold, $patron = []) |
452 | { |
453 | return $hold['cancel_details']; |
454 | } |
455 | |
456 | /** |
457 | * Get Default Pick Up Location |
458 | * |
459 | * @param array $patron Patron information returned by the patronLogin |
460 | * method. |
461 | * @param array $holdDetails Optional array, only passed in when getting a list |
462 | * in the context of placing a hold; contains most of the same values passed to |
463 | * placeHold, minus the patron data. May be used to limit the pickup options |
464 | * or may be ignored. |
465 | * |
466 | * @return string The default pickup location for the patron. |
467 | */ |
468 | public function getDefaultPickUpLocation($patron = null, $holdDetails = null) |
469 | { |
470 | return false; |
471 | } |
472 | |
473 | /** |
474 | * Get Funds |
475 | * |
476 | * Return a list of funds which may be used to limit the getNewItems list. |
477 | * |
478 | * @return array An associative array with key = fund ID, value = fund name. |
479 | */ |
480 | public function getFunds() |
481 | { |
482 | // If you do not want or support such limits, just return an empty |
483 | // array here and the limit control on the new item search screen |
484 | // will disappear. |
485 | return []; |
486 | } |
487 | |
488 | /** |
489 | * Cancel Storage Retrieval Request |
490 | * |
491 | * Attempts to Cancel a Storage Retrieval Request on a particular item. The |
492 | * data in $cancelDetails['details'] is determined by |
493 | * getCancelStorageRetrievalRequestDetails(). |
494 | * |
495 | * @param array $cancelDetails An array of item and patron data |
496 | * |
497 | * @return array An array of data on each request including |
498 | * whether or not it was successful and a system message (if available) |
499 | */ |
500 | public function cancelStorageRetrievalRequests($cancelDetails) |
501 | { |
502 | // Not yet implemented |
503 | return []; |
504 | } |
505 | |
506 | /** |
507 | * Get Cancel Storage Retrieval Request Details |
508 | * |
509 | * In order to cancel a hold, Voyager requires the patron details an item ID |
510 | * and a recall ID. This function returns the item id and recall id as a string |
511 | * separated by a pipe, which is then submitted as form data in Hold.php. This |
512 | * value is then extracted by the CancelHolds function. |
513 | * |
514 | * @param array $details An array of item data |
515 | * @param array $patron Patron information from patronLogin |
516 | * |
517 | * @return string Data for use in a form field |
518 | * |
519 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
520 | */ |
521 | public function getCancelStorageRetrievalRequestDetails($details, $patron) |
522 | { |
523 | // Not yet implemented |
524 | return ''; |
525 | } |
526 | |
527 | /** |
528 | * Get Patron ILL Requests |
529 | * |
530 | * This is responsible for retrieving all ILL requests by a specific patron. |
531 | * |
532 | * @param array $patron The patron array from patronLogin |
533 | * |
534 | * @return mixed Array of the patron's ILL requests |
535 | * |
536 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
537 | */ |
538 | public function getMyILLRequests($patron) |
539 | { |
540 | // Not yet implemented |
541 | return []; |
542 | } |
543 | |
544 | /** |
545 | * Check if ILL request available |
546 | * |
547 | * This is responsible for determining if an item is requestable |
548 | * |
549 | * @param string $id The Bib ID |
550 | * @param array $data An Array of item data |
551 | * @param array $patron An array of patron data |
552 | * |
553 | * @return bool True if request is valid, false if not |
554 | * |
555 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
556 | */ |
557 | public function checkILLRequestIsValid($id, $data, $patron) |
558 | { |
559 | // Not yet implemented |
560 | return false; |
561 | } |
562 | |
563 | /** |
564 | * Place ILL Request |
565 | * |
566 | * Attempts to place an ILL request on a particular item and returns |
567 | * an array with result details |
568 | * |
569 | * @param array $details An array of item and patron data |
570 | * |
571 | * @return mixed An array of data on the request including |
572 | * whether or not it was successful and a system message (if available) |
573 | */ |
574 | public function placeILLRequest($details) |
575 | { |
576 | // Not yet implemented |
577 | return []; |
578 | } |
579 | |
580 | /** |
581 | * Get ILL Pickup Libraries |
582 | * |
583 | * This is responsible for getting information on the possible pickup libraries |
584 | * |
585 | * @param string $id Record ID |
586 | * @param array $patron Patron |
587 | * |
588 | * @return bool|array False if request not allowed, or an array of associative |
589 | * arrays with libraries. |
590 | * |
591 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
592 | */ |
593 | public function getILLPickupLibraries($id, $patron) |
594 | { |
595 | // Not yet implemented |
596 | return false; |
597 | } |
598 | |
599 | /** |
600 | * Get ILL Pickup Locations |
601 | * |
602 | * This is responsible for getting a list of possible pickup locations for a |
603 | * library |
604 | * |
605 | * @param string $id Record ID |
606 | * @param string $pickupLib Pickup library ID |
607 | * @param array $patron Patron |
608 | * |
609 | * @return bool|array False if request not allowed, or an array of locations. |
610 | * |
611 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
612 | */ |
613 | public function getILLPickupLocations($id, $pickupLib, $patron) |
614 | { |
615 | // Not yet implemented |
616 | return false; |
617 | } |
618 | |
619 | /** |
620 | * Cancel ILL Request |
621 | * |
622 | * Attempts to Cancel an ILL request on a particular item. The |
623 | * data in $cancelDetails['details'] is determined by |
624 | * getCancelILLRequestDetails(). |
625 | * |
626 | * @param array $cancelDetails An array of item and patron data |
627 | * |
628 | * @return array An array of data on each request including |
629 | * whether or not it was successful and a system message (if available) |
630 | */ |
631 | public function cancelILLRequests($cancelDetails) |
632 | { |
633 | // Not yet implemented |
634 | return []; |
635 | } |
636 | |
637 | /** |
638 | * Get Cancel ILL Request Details |
639 | * |
640 | * @param array $details An array of item data |
641 | * @param array $patron Patron information from patronLogin |
642 | * |
643 | * @return string Data for use in a form field |
644 | * |
645 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
646 | */ |
647 | public function getCancelILLRequestDetails($details, $patron) |
648 | { |
649 | // Not yet implemented |
650 | return ''; |
651 | } |
652 | |
653 | /** |
654 | * Get Patron Fines |
655 | * |
656 | * This is responsible for retrieving all fines by a specific patron. |
657 | * |
658 | * @param array $patron The patron array from patronLogin |
659 | * |
660 | * @return mixed Array of the patron's fines on success |
661 | */ |
662 | public function getMyFines($patron) |
663 | { |
664 | // check if user has appropriate scope (refer to scope declaration above for |
665 | // further details) |
666 | if (!$this->paiaCheckScope(self::SCOPE_READ_FEES)) { |
667 | throw new ForbiddenException('Exception::access_denied_read_fines'); |
668 | } |
669 | |
670 | $fees = $this->paiaGetAsArray( |
671 | 'core/' . $patron['cat_username'] . '/fees' |
672 | ); |
673 | // PAIA simple data type money: a monetary value with currency (format |
674 | // [0-9]+\.[0-9][0-9] [A-Z][A-Z][A-Z]), for instance 0.80 USD. |
675 | $feeConverter = function ($fee) { |
676 | $paiaCurrencyPattern = "/^([0-9]+\.[0-9][0-9]) ([A-Z][A-Z][A-Z])$/"; |
677 | if (preg_match($paiaCurrencyPattern, $fee, $feeMatches)) { |
678 | // VuFind expects fees in PENNIES |
679 | return $feeMatches[1] * 100; |
680 | } |
681 | return $fee; |
682 | }; |
683 | |
684 | $results = []; |
685 | if (isset($fees['fee'])) { |
686 | foreach ($fees['fee'] as $fee) { |
687 | $result = [ |
688 | // fee.amount 1..1 money amount of a single fee |
689 | 'amount' => $feeConverter($fee['amount']), |
690 | 'checkout' => '', |
691 | // fee.feetype 0..1 string textual description of the type |
692 | // of service that caused the fee |
693 | 'fine' => ($fee['feetype'] ?? null), |
694 | 'balance' => $feeConverter($fee['amount']), |
695 | // fee.date 0..1 date date when the fee was claimed |
696 | 'createdate' => (isset($fee['date']) |
697 | ? $this->convertDate($fee['date']) : null), |
698 | 'duedate' => '', |
699 | // fee.edition 0..1 URI edition that caused the fee |
700 | 'id' => (isset($fee['edition']) |
701 | ? $this->getAlternativeItemId($fee['edition']) : ''), |
702 | ]; |
703 | // custom PAIA fields can get added in getAdditionalFeeData |
704 | $results[] = $result + $this->getAdditionalFeeData($fee, $patron); |
705 | } |
706 | } |
707 | return $results; |
708 | } |
709 | |
710 | /** |
711 | * Gets additional array fields for the item. |
712 | * Override this method in your custom PAIA driver if necessary. |
713 | * |
714 | * @param array $fee The fee array from PAIA |
715 | * @param array $patron The patron array from patronLogin |
716 | * |
717 | * @return array Additional fee data for the item |
718 | */ |
719 | protected function getAdditionalFeeData($fee, $patron = null) |
720 | { |
721 | $additionalData = []; |
722 | // The title is always displayed to the user in fines view if no record can |
723 | // be found for current fee. So always populate the title with content of |
724 | // about field. |
725 | if (isset($fee['about'])) { |
726 | $additionalData['title'] = $fee['about']; |
727 | } |
728 | |
729 | // custom PAIA fields |
730 | // fee.about 0..1 string textual information about the fee |
731 | // fee.item 0..1 URI item that caused the fee |
732 | // fee.feeid 0..1 URI URI of the type of service that |
733 | // caused the fee |
734 | $additionalData['feeid'] = ($fee['feeid'] ?? null); |
735 | $additionalData['about'] = ($fee['about'] ?? null); |
736 | $additionalData['item'] = ($fee['item'] ?? null); |
737 | |
738 | return $additionalData; |
739 | } |
740 | |
741 | /** |
742 | * Get Patron Holds |
743 | * |
744 | * This is responsible for retrieving all holds by a specific patron. |
745 | * |
746 | * @param array $patron The patron array from patronLogin |
747 | * |
748 | * @return mixed Array of the patron's holds on success. |
749 | */ |
750 | public function getMyHolds($patron) |
751 | { |
752 | // filters for getMyHolds are by default configuration: |
753 | // status = 1 - reserved (the document is not accessible for the patron yet, |
754 | // but it will be) |
755 | // 4 - provided (the document is ready to be used by the patron) |
756 | $status = $this->config['Holds']['status'] ?? '1:4'; |
757 | $filter = ['status' => explode(':', $status)]; |
758 | // get items-docs for given filters |
759 | $items = $this->paiaGetItems($patron, $filter); |
760 | return $this->mapPaiaItems($items, 'myHoldsMapping'); |
761 | } |
762 | |
763 | /** |
764 | * Get Patron Profile |
765 | * |
766 | * This is responsible for retrieving the profile for a specific patron. |
767 | * |
768 | * @param array $patron The patron array |
769 | * |
770 | * @return array Array of the patron's profile data on success, |
771 | */ |
772 | public function getMyProfile($patron) |
773 | { |
774 | if (is_array($patron)) { |
775 | $type = isset($patron['type']) |
776 | ? implode( |
777 | ', ', |
778 | array_map( |
779 | [$this, 'getReadableGroupType'], |
780 | (array)$patron['type'] |
781 | ) |
782 | ) |
783 | : null; |
784 | return [ |
785 | 'firstname' => $patron['firstname'], |
786 | 'lastname' => $patron['lastname'], |
787 | 'address1' => $patron['address'], |
788 | 'address2' => null, |
789 | 'city' => null, |
790 | 'country' => null, |
791 | 'zip' => null, |
792 | 'phone' => null, |
793 | 'mobile_phone' => null, |
794 | 'group' => $type, |
795 | // PAIA specific custom values |
796 | 'expires' => isset($patron['expires']) |
797 | ? $this->convertDate($patron['expires']) : null, |
798 | 'statuscode' => $patron['status'] ?? null, |
799 | 'canWrite' => in_array(self::SCOPE_WRITE_ITEMS, $this->getScope()), |
800 | ]; |
801 | } |
802 | return []; |
803 | } |
804 | |
805 | /** |
806 | * Get Readable Group Type |
807 | * |
808 | * Due to PAIA specifications type returns an URI. This method offers a |
809 | * possibility to translate the URI in a readable value by inheritance |
810 | * and implementing a personal proceeding. |
811 | * |
812 | * @param string $type URI of usertype |
813 | * |
814 | * @return string URI of usertype |
815 | */ |
816 | protected function getReadableGroupType($type) |
817 | { |
818 | return $type; |
819 | } |
820 | |
821 | /** |
822 | * Get Patron Transactions |
823 | * |
824 | * This is responsible for retrieving all transactions (i.e. checked out items) |
825 | * by a specific patron. |
826 | * |
827 | * @param array $patron The patron array from patronLogin |
828 | * |
829 | * @return array Array of the patron's transactions on success, |
830 | */ |
831 | public function getMyTransactions($patron) |
832 | { |
833 | // filters for getMyTransactions are by default configuration: |
834 | // status = 3 - held (the document is on loan by the patron) |
835 | $status = $this->config['Transactions']['status'] ?? '3'; |
836 | $filter = ['status' => explode(':', $status)]; |
837 | // get items-docs for given filters |
838 | $items = $this->paiaGetItems($patron, $filter); |
839 | return $this->mapPaiaItems($items, 'myTransactionsMapping'); |
840 | } |
841 | |
842 | /** |
843 | * Get Patron StorageRetrievalRequests |
844 | * |
845 | * This is responsible for retrieving all storage retrieval requests |
846 | * by a specific patron. |
847 | * |
848 | * @param array $patron The patron array from patronLogin |
849 | * |
850 | * @return array Array of the patron's storage retrieval requests on success, |
851 | */ |
852 | public function getMyStorageRetrievalRequests($patron) |
853 | { |
854 | // filters for getMyStorageRetrievalRequests are by default configuration: |
855 | // status = 2 - ordered (the document is ordered by the patron) |
856 | $status = $this->config['StorageRetrievalRequests']['status'] ?? '2'; |
857 | $filter = ['status' => explode(':', $status)]; |
858 | // get items-docs for given filters |
859 | $items = $this->paiaGetItems($patron, $filter); |
860 | return $this->mapPaiaItems($items, 'myStorageRetrievalRequestsMapping'); |
861 | } |
862 | |
863 | /** |
864 | * This method queries the ILS for new items |
865 | * |
866 | * @param string $page page number of results to retrieve (counting starts @1) |
867 | * @param string $limit the size of each page of results to retrieve |
868 | * @param string $daysOld the maximum age of records to retrieve in days (max 30) |
869 | * @param string $fundID optional fund ID to use for limiting results |
870 | * |
871 | * @return array An associative array with two keys: 'count' (the number of items |
872 | * in the 'results' array) and 'results' (an array of associative arrays, each |
873 | * with a single key: 'id', a record ID). |
874 | */ |
875 | public function getNewItems($page, $limit, $daysOld, $fundID) |
876 | { |
877 | return []; |
878 | } |
879 | |
880 | /** |
881 | * Get Pick Up Locations |
882 | * |
883 | * This is responsible for gettting a list of valid library locations for |
884 | * holds / recall retrieval |
885 | * |
886 | * @param array $patron Patron information returned by the patronLogin |
887 | * method. |
888 | * @param array $holdDetails Optional array, only passed in when getting a list |
889 | * in the context of placing or editing a hold. When placing a hold, it contains |
890 | * most of the same values passed to placeHold, minus the patron data. When |
891 | * editing a hold it contains all the hold information returned by getMyHolds. |
892 | * May be used to limit the pickup options or may be ignored. The driver must |
893 | * not add new options to the return array based on this data or other areas of |
894 | * VuFind may behave incorrectly. |
895 | * |
896 | * @return array An array of associative arrays with locationID and |
897 | * locationDisplay keys |
898 | */ |
899 | public function getPickUpLocations($patron = null, $holdDetails = null) |
900 | { |
901 | // How to get valid PickupLocations for a PICA LBS? |
902 | return []; |
903 | } |
904 | |
905 | /** |
906 | * This method returns a string to use as the input form value for renewing |
907 | * each hold item. (optional, but required if you implement the |
908 | * renewMyItems method) Not supported prior to VuFind 1.2 |
909 | * |
910 | * @param array $checkOutDetails One of the individual item arrays returned by |
911 | * the getMyTransactions method |
912 | * |
913 | * @return string A string to use as the input form value for renewing |
914 | * each item; you can pass any data that is needed by your |
915 | * ILS to identify the transaction to renew – the output |
916 | * of this method will be used as part of the input to the |
917 | * renewMyItems method. |
918 | */ |
919 | public function getRenewDetails($checkOutDetails) |
920 | { |
921 | return $checkOutDetails['renew_details']; |
922 | } |
923 | |
924 | /** |
925 | * Get the callnumber of this item |
926 | * |
927 | * @param array $doc Array of PAIA item. |
928 | * |
929 | * @return String |
930 | */ |
931 | protected function getCallNumber($doc) |
932 | { |
933 | return $doc['label'] ?? null; |
934 | } |
935 | |
936 | /** |
937 | * Patron Login |
938 | * |
939 | * This is responsible for authenticating a patron against the catalog. |
940 | * |
941 | * @param string $username The patron's username |
942 | * @param string $password The patron's login password |
943 | * |
944 | * @return mixed Associative array of patron info on successful login, |
945 | * null on unsuccessful login. |
946 | * |
947 | * @throws ILSException |
948 | */ |
949 | public function patronLogin($username, $password) |
950 | { |
951 | // check also for grantType as patron's password is never required when |
952 | // grantType = client_credentials is configured |
953 | if ( |
954 | $username == '' |
955 | || ($password == '' && $this->grantType != 'client_credentials') |
956 | ) { |
957 | throw new ILSException('Invalid Login, Please try again.'); |
958 | } |
959 | |
960 | $session = $this->getSession(); |
961 | |
962 | // if we already have a session with access_token and patron id, try to get |
963 | // patron info with session data |
964 | if (isset($session->expires) && $session->expires > time()) { |
965 | return $this->enrichUserDetails( |
966 | $this->paiaGetUserDetails($session->patron), |
967 | $password |
968 | ); |
969 | } |
970 | try { |
971 | if ($this->paiaLogin($username, $password)) { |
972 | return $this->enrichUserDetails( |
973 | $this->paiaGetUserDetails($session->patron), |
974 | $password |
975 | ); |
976 | } |
977 | } catch (AuthException $e) { |
978 | // swallow auth exceptions and return null compliant to spec at: |
979 | // https://vufind.org/wiki/development:plugins:ils_drivers#patronlogin |
980 | return null; |
981 | } |
982 | } |
983 | |
984 | /** |
985 | * Handle PAIA request errors and throw appropriate exception. |
986 | * |
987 | * @param array $array Array containing error messages |
988 | * |
989 | * @return void |
990 | * |
991 | * @throws AuthException |
992 | * @throws ILSException |
993 | */ |
994 | protected function paiaHandleErrors($array) |
995 | { |
996 | // TODO: also have exception contain content of 'error' as for at least |
997 | // error code 403 two differing errors are possible |
998 | // (cf. http://gbv.github.io/paia/paia.html#request-errors) |
999 | if (isset($array['error'])) { |
1000 | switch ($array['error']) { |
1001 | case 'access_denied': |
1002 | // error code error_description |
1003 | // access_denied 403 Wrong or missing credentials to get |
1004 | // an access token |
1005 | throw new AuthException( |
1006 | $array['error_description'] ?? $array['error'], |
1007 | (int)($array['code'] ?? 0) |
1008 | ); |
1009 | |
1010 | case 'invalid_grant': |
1011 | // invalid_grant 401 The access token was missing, |
1012 | // invalid or expired |
1013 | |
1014 | case 'insufficient_scope': |
1015 | // insufficient_scope 403 The access token was accepted but |
1016 | // it lacks permission for the request |
1017 | throw new ForbiddenException( |
1018 | $array['error_description'] ?? $array['error'], |
1019 | (int)($array['code'] ?? 0) |
1020 | ); |
1021 | |
1022 | case 'not_found': |
1023 | // not_found 404 Unknown request URL or unknown patron. |
1024 | // Implementations SHOULD first check |
1025 | // authentication and prefer error |
1026 | // invalid_grant or access_denied to |
1027 | // prevent leaking patron identifiers. |
1028 | |
1029 | case 'not_implemented': |
1030 | // not_implemented 501 Known but unsupported request URL |
1031 | // (for instance a PAIA auth server |
1032 | // server may not implement |
1033 | // http://example.org/core/change) |
1034 | |
1035 | case 'invalid_request': |
1036 | // invalid_request 405 Unexpected HTTP verb |
1037 | // invalid_request 400 Malformed request (for instance |
1038 | // error parsing JSON, unsupported |
1039 | // request content type, etc.) |
1040 | // invalid_request 422 The request parameters could be |
1041 | // parsed but they don’t match the |
1042 | // request method (for instance |
1043 | // missing fields, invalid values, |
1044 | // etc.) |
1045 | |
1046 | case 'internal_error': |
1047 | // internal_error 500 An unexpected error occurred. This |
1048 | // error corresponds to a bug in the |
1049 | // implementation of a PAIA auth/core |
1050 | // server |
1051 | |
1052 | case 'service_unavailable': |
1053 | // service_unavailable 503 The request couldn’t be serviced |
1054 | // because of a temporary failure |
1055 | |
1056 | case 'bad_gateway': |
1057 | // bad_gateway 502 The request couldn’t be serviced because |
1058 | // of a backend failure (for instance the |
1059 | // library system’s database) |
1060 | |
1061 | case 'gateway_timeout': |
1062 | // gateway_timeout 504 The request couldn’t be serviced |
1063 | // because of a backend failure |
1064 | |
1065 | default: |
1066 | throw new ILSException( |
1067 | $array['error_description'] ?? $array['error'], |
1068 | (int)($array['code'] ?? 0) |
1069 | ); |
1070 | } |
1071 | } |
1072 | } |
1073 | |
1074 | /** |
1075 | * PAIA helper function to map session data to return value of patronLogin() |
1076 | * |
1077 | * @param array $details Patron details returned by patronLogin |
1078 | * @param string $password Patron catalogue password |
1079 | * |
1080 | * @return mixed |
1081 | */ |
1082 | protected function enrichUserDetails($details, $password) |
1083 | { |
1084 | $session = $this->getSession(); |
1085 | |
1086 | $details['cat_username'] = $session->patron; |
1087 | $details['cat_password'] = $password; |
1088 | return $details; |
1089 | } |
1090 | |
1091 | /** |
1092 | * Returns an array with PAIA confirmations based on the given holdDetails which |
1093 | * will be used for a request. |
1094 | * Currently two condition types are supported: |
1095 | * - http://purl.org/ontology/paia#StorageCondition to select a document |
1096 | * location -- mapped to pickUpLocation |
1097 | * - http://purl.org/ontology/paia#FeeCondition to confirm or select a document |
1098 | * service causing a fee -- not mapped yet |
1099 | * |
1100 | * @param array $holdDetails An array of item and patron data |
1101 | * |
1102 | * @return array |
1103 | */ |
1104 | protected function getConfirmations($holdDetails) |
1105 | { |
1106 | $confirmations = []; |
1107 | if (isset($holdDetails['pickUpLocation'])) { |
1108 | $confirmations['http://purl.org/ontology/paia#StorageCondition'] |
1109 | = [$holdDetails['pickUpLocation']]; |
1110 | } |
1111 | return $confirmations; |
1112 | } |
1113 | |
1114 | /** |
1115 | * Place Hold |
1116 | * |
1117 | * Attempts to place a hold or recall on a particular item and returns |
1118 | * an array with result details |
1119 | * |
1120 | * Make a request on a specific record |
1121 | * |
1122 | * @param array $holdDetails An array of item and patron data |
1123 | * |
1124 | * @return mixed An array of data on the request including |
1125 | * whether or not it was successful and a system message (if available) |
1126 | */ |
1127 | public function placeHold($holdDetails) |
1128 | { |
1129 | // check if user has appropriate scope (refer to scope declaration above for |
1130 | // further details) |
1131 | if (!$this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) { |
1132 | throw new ForbiddenException( |
1133 | 'Exception::access_denied_write_items' |
1134 | ); |
1135 | } |
1136 | |
1137 | $item = $holdDetails['item_id']; |
1138 | $patron = $holdDetails['patron']; |
1139 | |
1140 | $doc = []; |
1141 | $doc['item'] = stripslashes($item); |
1142 | if ($confirm = $this->getConfirmations($holdDetails)) { |
1143 | $doc['confirm'] = $confirm; |
1144 | } |
1145 | $post_data = []; |
1146 | $post_data['doc'][] = $doc; |
1147 | |
1148 | try { |
1149 | $array_response = $this->paiaPostAsArray( |
1150 | 'core/' . $patron['cat_username'] . '/request', |
1151 | $post_data |
1152 | ); |
1153 | } catch (\Exception $e) { |
1154 | $this->debug($e->getMessage()); |
1155 | return [ |
1156 | 'success' => false, |
1157 | 'sysMessage' => $e->getMessage(), |
1158 | ]; |
1159 | } |
1160 | |
1161 | $details = []; |
1162 | if (isset($array_response['error'])) { |
1163 | $details = [ |
1164 | 'success' => false, |
1165 | 'sysMessage' => $array_response['error_description'], |
1166 | ]; |
1167 | } else { |
1168 | $elements = $array_response['doc']; |
1169 | foreach ($elements as $element) { |
1170 | if (isset($element['error'])) { |
1171 | $details = [ |
1172 | 'success' => false, |
1173 | 'sysMessage' => $element['error'], |
1174 | ]; |
1175 | } else { |
1176 | $details = [ |
1177 | 'success' => true, |
1178 | 'sysMessage' => 'Successfully requested', |
1179 | ]; |
1180 | // if caching is enabled for DAIA remove the cached data for the |
1181 | // current item otherwise the changed status will not be shown |
1182 | // before the cache expires |
1183 | if ($this->daiaCacheEnabled) { |
1184 | $this->removeCachedData($holdDetails['doc_id']); |
1185 | } |
1186 | |
1187 | if ($this->paiaCacheEnabled) { |
1188 | $this->removeCachedData($patron['cat_username']); |
1189 | } |
1190 | } |
1191 | } |
1192 | } |
1193 | return $details; |
1194 | } |
1195 | |
1196 | /** |
1197 | * Place a Storage Retrieval Request |
1198 | * |
1199 | * Attempts to place a request on a particular item and returns |
1200 | * an array with result details. |
1201 | * |
1202 | * @param array $details An array of item and patron data |
1203 | * |
1204 | * @return mixed An array of data on the request including |
1205 | * whether or not it was successful and a system message (if available) |
1206 | */ |
1207 | public function placeStorageRetrievalRequest($details) |
1208 | { |
1209 | // Making a storage retrieval request is the same in PAIA as placing a Hold |
1210 | return $this->placeHold($details); |
1211 | } |
1212 | |
1213 | /** |
1214 | * This method renews a list of items for a specific patron. |
1215 | * |
1216 | * @param array $details - An associative array with two keys: |
1217 | * patron - array returned by patronLogin method |
1218 | * details - array of values returned by the getRenewDetails method |
1219 | * identifying which items to renew |
1220 | * |
1221 | * @return array - An associative array with two keys: |
1222 | * blocks - An array of strings specifying why a user is blocked from |
1223 | * renewing (false if no blocks) |
1224 | * details - Not set when blocks exist; otherwise, an array of |
1225 | * associative arrays (keyed by item ID) with each subarray |
1226 | * containing these keys: |
1227 | * success – Boolean true or false |
1228 | * new_date – string – A new due date |
1229 | * new_time – string – A new due time |
1230 | * item_id – The item id of the renewed item |
1231 | * sysMessage – A system supplied renewal message (optional) |
1232 | */ |
1233 | public function renewMyItems($details) |
1234 | { |
1235 | // check if user has appropriate scope (refer to scope declaration above for |
1236 | // further details) |
1237 | if (!$this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) { |
1238 | throw new ForbiddenException( |
1239 | 'Exception::access_denied_write_items' |
1240 | ); |
1241 | } |
1242 | |
1243 | $it = $details['details']; |
1244 | $items = []; |
1245 | foreach ($it as $item) { |
1246 | $items[] = ['item' => stripslashes($item)]; |
1247 | } |
1248 | $patron = $details['patron']; |
1249 | $post_data = ['doc' => $items]; |
1250 | |
1251 | try { |
1252 | $array_response = $this->paiaPostAsArray( |
1253 | 'core/' . $patron['cat_username'] . '/renew', |
1254 | $post_data |
1255 | ); |
1256 | } catch (\Exception $e) { |
1257 | $this->debug($e->getMessage()); |
1258 | return [ |
1259 | 'success' => false, |
1260 | 'sysMessage' => $e->getMessage(), |
1261 | ]; |
1262 | } |
1263 | |
1264 | $details = []; |
1265 | |
1266 | if (isset($array_response['error'])) { |
1267 | $details[] = [ |
1268 | 'success' => false, |
1269 | 'sysMessage' => $array_response['error_description'], |
1270 | ]; |
1271 | } else { |
1272 | $elements = $array_response['doc']; |
1273 | foreach ($elements as $element) { |
1274 | // VuFind can only assign the response to an id - if none is given |
1275 | // (which is possible) simply skip this response element |
1276 | if (isset($element['item'])) { |
1277 | if (isset($element['error'])) { |
1278 | $details[$element['item']] = [ |
1279 | 'success' => false, |
1280 | 'sysMessage' => $element['error'], |
1281 | ]; |
1282 | } elseif ($element['status'] == '3') { |
1283 | $details[$element['item']] = [ |
1284 | 'success' => true, |
1285 | 'new_date' => isset($element['endtime']) |
1286 | ? $this->convertDatetime($element['endtime']) : '', |
1287 | 'item_id' => 0, |
1288 | 'sysMessage' => 'Successfully renewed', |
1289 | ]; |
1290 | } else { |
1291 | $details[$element['item']] = [ |
1292 | 'success' => false, |
1293 | 'item_id' => 0, |
1294 | 'new_date' => isset($element['endtime']) |
1295 | ? $this->convertDatetime($element['endtime']) : '', |
1296 | 'sysMessage' => 'Request rejected', |
1297 | ]; |
1298 | } |
1299 | } |
1300 | |
1301 | // DAIA cache cannot be cleared for particular item as PAIA only |
1302 | // operates with specific item URIs and the DAIA cache is setup |
1303 | // by doc URIs (containing items with URIs) |
1304 | } |
1305 | |
1306 | // If caching is enabled for PAIA clear the cache as at least for one |
1307 | // item renew was successful and therefore the status changed. Otherwise |
1308 | // the changed status will not be shown before the cache expires. |
1309 | if ($this->paiaCacheEnabled) { |
1310 | $this->removeCachedData($patron['cat_username']); |
1311 | } |
1312 | } |
1313 | $returnArray = ['blocks' => false, 'details' => $details]; |
1314 | return $returnArray; |
1315 | } |
1316 | |
1317 | /* |
1318 | * PAIA functions |
1319 | */ |
1320 | |
1321 | /** |
1322 | * PAIA support method to return strings for PAIA service status values |
1323 | * |
1324 | * @param string $status PAIA service status |
1325 | * |
1326 | * @return string Describing PAIA service status |
1327 | */ |
1328 | protected function paiaStatusString($status) |
1329 | { |
1330 | return self::$statusStrings[$status] ?? ''; |
1331 | } |
1332 | |
1333 | /** |
1334 | * PAIA support method for PAIA core method 'items' returning only those |
1335 | * documents containing the given service status. |
1336 | * |
1337 | * @param array $patron Array with patron information |
1338 | * @param array $filter Array of properties identifying the wanted items |
1339 | * |
1340 | * @return array|mixed Array of documents containing the given filter properties |
1341 | */ |
1342 | protected function paiaGetItems($patron, $filter = []) |
1343 | { |
1344 | // check if user has appropriate scope (refer to scope declaration above for |
1345 | // further details) |
1346 | if (!$this->paiaCheckScope(self::SCOPE_READ_ITEMS)) { |
1347 | throw new ForbiddenException('Exception::access_denied_read_items'); |
1348 | } |
1349 | |
1350 | // check for existing data in cache |
1351 | $itemsResponse = $this->paiaCacheEnabled |
1352 | ? $this->getCachedData($patron['cat_username']) : null; |
1353 | |
1354 | if (!isset($itemsResponse) || $itemsResponse == null) { |
1355 | $itemsResponse = $this->paiaGetAsArray( |
1356 | 'core/' . $patron['cat_username'] . '/items' |
1357 | ); |
1358 | if ($this->paiaCacheEnabled) { |
1359 | $this->putCachedData($patron['cat_username'], $itemsResponse); |
1360 | } |
1361 | } |
1362 | |
1363 | if (isset($itemsResponse['doc'])) { |
1364 | if (count($filter)) { |
1365 | $filteredItems = []; |
1366 | foreach ($itemsResponse['doc'] as $doc) { |
1367 | $filterCounter = 0; |
1368 | foreach ($filter as $filterKey => $filterValue) { |
1369 | if ( |
1370 | isset($doc[$filterKey]) |
1371 | && in_array($doc[$filterKey], (array)$filterValue) |
1372 | ) { |
1373 | $filterCounter++; |
1374 | } |
1375 | } |
1376 | if ($filterCounter == count($filter)) { |
1377 | $filteredItems[] = $doc; |
1378 | } |
1379 | } |
1380 | return $filteredItems; |
1381 | } else { |
1382 | return $itemsResponse; |
1383 | } |
1384 | } else { |
1385 | $this->debug( |
1386 | 'No documents found in PAIA response. Returning empty array.' |
1387 | ); |
1388 | } |
1389 | return []; |
1390 | } |
1391 | |
1392 | /** |
1393 | * PAIA support method to retrieve needed ItemId in case PAIA-response does not |
1394 | * contain it |
1395 | * |
1396 | * @param string $id itemId |
1397 | * |
1398 | * @return string $id |
1399 | */ |
1400 | protected function getAlternativeItemId($id) |
1401 | { |
1402 | return $id; |
1403 | } |
1404 | |
1405 | /** |
1406 | * PAIA support function to implement ILS specific parsing of user_details |
1407 | * |
1408 | * @param string $patron User id |
1409 | * @param array $user_response Array with PAIA response data |
1410 | * |
1411 | * @return array |
1412 | */ |
1413 | protected function paiaParseUserDetails($patron, $user_response) |
1414 | { |
1415 | $username = trim($user_response['name']); |
1416 | if (count(explode(',', $username)) == 2) { |
1417 | $nameArr = explode(',', $username); |
1418 | $firstname = $nameArr[1]; |
1419 | $lastname = $nameArr[0]; |
1420 | } else { |
1421 | $nameArr = explode(' ', $username); |
1422 | $lastname = array_pop($nameArr); |
1423 | $firstname = trim(implode(' ', $nameArr)); |
1424 | } |
1425 | |
1426 | // TODO: implement parsing of user details according to types set |
1427 | // (cf. https://github.com/gbv/paia/issues/29) |
1428 | |
1429 | $user = []; |
1430 | $user['id'] = $patron; |
1431 | $user['firstname'] = $firstname; |
1432 | $user['lastname'] = $lastname; |
1433 | $user['email'] = ($user_response['email'] ?? ''); |
1434 | $user['major'] = null; |
1435 | $user['college'] = null; |
1436 | // add other information from PAIA - we don't want anything to get lost |
1437 | // while parsing |
1438 | foreach ($user_response as $key => $value) { |
1439 | if (!isset($user[$key])) { |
1440 | $user[$key] = $value; |
1441 | } |
1442 | } |
1443 | return $user; |
1444 | } |
1445 | |
1446 | /** |
1447 | * PAIA helper function to allow customization of mapping from PAIA response to |
1448 | * VuFind ILS-method return values. |
1449 | * |
1450 | * @param array $items Array of PAIA items to be mapped |
1451 | * @param string $mapping String identifying a custom mapping-method |
1452 | * |
1453 | * @return array |
1454 | */ |
1455 | protected function mapPaiaItems($items, $mapping) |
1456 | { |
1457 | if (is_callable([$this, $mapping])) { |
1458 | return $this->$mapping($items); |
1459 | } |
1460 | |
1461 | $this->debug('Could not call method: ' . $mapping . '() .'); |
1462 | return []; |
1463 | } |
1464 | |
1465 | /** |
1466 | * Map a PAIA document to an array for use in generating a VuFind request |
1467 | * (holds, storage retrieval, etc). |
1468 | * |
1469 | * @param array $doc Array of PAIA document to be mapped. |
1470 | * |
1471 | * @return array |
1472 | */ |
1473 | protected function getBasicDetails($doc) |
1474 | { |
1475 | $result = []; |
1476 | |
1477 | // item (0..1) URI of a particular copy |
1478 | $result['item_id'] = ($doc['item'] ?? ''); |
1479 | |
1480 | $result['cancel_details'] |
1481 | = (isset($doc['cancancel']) && $doc['cancancel'] |
1482 | && $this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) |
1483 | ? $result['item_id'] : ''; |
1484 | |
1485 | // edition (0..1) URI of a the document (no particular copy) |
1486 | // hook for retrieving alternative ItemId in case PAIA does not |
1487 | // the needed id |
1488 | $result['id'] = (isset($doc['edition']) |
1489 | ? $this->getAlternativeItemId($doc['edition']) : ''); |
1490 | |
1491 | $result['type'] = $this->paiaStatusString($doc['status']); |
1492 | |
1493 | // storage (0..1) textual description of location of the document |
1494 | $result['location'] = ($doc['storage'] ?? null); |
1495 | |
1496 | // queue (0..1) number of waiting requests for the document or item |
1497 | $result['position'] = ($doc['queue'] ?? null); |
1498 | |
1499 | // only true if status == 4 |
1500 | $result['available'] = false; |
1501 | |
1502 | // about (0..1) textual description of the document |
1503 | $result['title'] = ($doc['about'] ?? null); |
1504 | |
1505 | // PAIA custom field |
1506 | // label (0..1) call number, shelf mark or similar item label |
1507 | $result['callnumber'] = $this->getCallNumber($doc); |
1508 | |
1509 | /* |
1510 | * meaning of starttime and endtime depends on status: |
1511 | * |
1512 | * status | starttime |
1513 | * | endtime |
1514 | * -------+-------------------------------- |
1515 | * 0 | - |
1516 | * | - |
1517 | * 1 | when the document was reserved |
1518 | * | when the reserved document is expected to be available |
1519 | * 2 | when the document was ordered |
1520 | * | when the ordered document is expected to be available |
1521 | * 3 | when the document was lend |
1522 | * | when the loan period ends or ended (due) |
1523 | * 4 | when the document is provided |
1524 | * | when the provision will expire |
1525 | * 5 | when the request was rejected |
1526 | * | - |
1527 | */ |
1528 | |
1529 | $result['create'] = (isset($doc['starttime']) |
1530 | ? $this->convertDatetime($doc['starttime']) : ''); |
1531 | |
1532 | // Optional VuFind fields |
1533 | /* |
1534 | $result['reqnum'] = null; |
1535 | $result['volume'] = null; |
1536 | $result['publication_year'] = null; |
1537 | $result['isbn'] = null; |
1538 | $result['issn'] = null; |
1539 | $result['oclc'] = null; |
1540 | $result['upc'] = null; |
1541 | */ |
1542 | |
1543 | return $result; |
1544 | } |
1545 | |
1546 | /** |
1547 | * This PAIA helper function allows custom overrides for mapping of PAIA response |
1548 | * to getMyHolds data structure. |
1549 | * |
1550 | * @param array $items Array of PAIA items to be mapped. |
1551 | * |
1552 | * @return array |
1553 | */ |
1554 | protected function myHoldsMapping($items) |
1555 | { |
1556 | $results = []; |
1557 | |
1558 | foreach ($items as $doc) { |
1559 | $result = $this->getBasicDetails($doc); |
1560 | |
1561 | if ($doc['status'] == '4') { |
1562 | $result['expire'] = (isset($doc['endtime']) |
1563 | ? $this->convertDatetime($doc['endtime']) : ''); |
1564 | } else { |
1565 | $result['duedate'] = (isset($doc['endtime']) |
1566 | ? $this->convertDatetime($doc['endtime']) : ''); |
1567 | } |
1568 | |
1569 | // status: provided (the document is ready to be used by the patron) |
1570 | $result['available'] = $doc['status'] == 4 ? true : false; |
1571 | |
1572 | $results[] = $result; |
1573 | } |
1574 | return $results; |
1575 | } |
1576 | |
1577 | /** |
1578 | * This PAIA helper function allows custom overrides for mapping of PAIA response |
1579 | * to getMyStorageRetrievalRequests data structure. |
1580 | * |
1581 | * @param array $items Array of PAIA items to be mapped. |
1582 | * |
1583 | * @return array |
1584 | */ |
1585 | protected function myStorageRetrievalRequestsMapping($items) |
1586 | { |
1587 | $results = []; |
1588 | |
1589 | foreach ($items as $doc) { |
1590 | $result = $this->getBasicDetails($doc); |
1591 | |
1592 | $results[] = $result; |
1593 | } |
1594 | return $results; |
1595 | } |
1596 | |
1597 | /** |
1598 | * This PAIA helper function allows custom overrides for mapping of PAIA response |
1599 | * to getMyTransactions data structure. |
1600 | * |
1601 | * @param array $items Array of PAIA items to be mapped. |
1602 | * |
1603 | * @return array |
1604 | */ |
1605 | protected function myTransactionsMapping($items) |
1606 | { |
1607 | $results = []; |
1608 | |
1609 | foreach ($items as $doc) { |
1610 | $result = $this->getBasicDetails($doc); |
1611 | |
1612 | // canrenew (0..1) whether a document can be renewed (bool) |
1613 | $result['renewable'] = (isset($doc['canrenew']) |
1614 | && $this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) |
1615 | ? $doc['canrenew'] : false; |
1616 | |
1617 | $result['renew_details'] |
1618 | = (isset($doc['canrenew']) && $doc['canrenew'] |
1619 | && $this->paiaCheckScope(self::SCOPE_WRITE_ITEMS)) |
1620 | ? $result['item_id'] : ''; |
1621 | |
1622 | // queue (0..1) number of waiting requests for the document or item |
1623 | $result['request'] = ($doc['queue'] ?? null); |
1624 | |
1625 | // renewals (0..1) number of times the document has been renewed |
1626 | $result['renew'] = ($doc['renewals'] ?? null); |
1627 | |
1628 | // reminder (0..1) number of times the patron has been reminded |
1629 | $result['reminder'] = ( |
1630 | $doc['reminder'] ?? null |
1631 | ); |
1632 | |
1633 | // custom PAIA field |
1634 | // starttime (0..1) date and time when the status began |
1635 | $result['startTime'] = (isset($doc['starttime']) |
1636 | ? $this->convertDatetime($doc['starttime']) : ''); |
1637 | |
1638 | // endtime (0..1) date and time when the status will expire |
1639 | $result['dueTime'] = (isset($doc['endtime']) |
1640 | ? $this->convertDatetime($doc['endtime']) : ''); |
1641 | |
1642 | // duedate (0..1) date when the current status will expire (deprecated) |
1643 | $result['duedate'] = (isset($doc['duedate']) |
1644 | ? $this->convertDate($doc['duedate']) : ''); |
1645 | |
1646 | // cancancel (0..1) whether an ordered or provided document can be |
1647 | // canceled |
1648 | |
1649 | // error (0..1) error message, for instance if a request was rejected |
1650 | $result['message'] = ($doc['error'] ?? ''); |
1651 | |
1652 | // storage (0..1) textual description of location of the document |
1653 | $result['borrowingLocation'] = ($doc['storage'] ?? ''); |
1654 | |
1655 | // storageid (0..1) location URI |
1656 | |
1657 | // Optional VuFind fields |
1658 | /* |
1659 | $result['barcode'] = null; |
1660 | $result['dueStatus'] = null; |
1661 | $result['renewLimit'] = "1"; |
1662 | $result['volume'] = null; |
1663 | $result['publication_year'] = null; |
1664 | $result['isbn'] = null; |
1665 | $result['issn'] = null; |
1666 | $result['oclc'] = null; |
1667 | $result['upc'] = null; |
1668 | $result['institution_name'] = null; |
1669 | */ |
1670 | |
1671 | $results[] = $result; |
1672 | } |
1673 | |
1674 | return $results; |
1675 | } |
1676 | |
1677 | /** |
1678 | * Post something to a foreign host |
1679 | * |
1680 | * @param string $file POST target URL |
1681 | * @param string $data_to_send POST data |
1682 | * @param string $access_token PAIA access token for current session |
1683 | * |
1684 | * @return string POST response |
1685 | * @throws ILSException |
1686 | */ |
1687 | protected function paiaPostRequest($file, $data_to_send, $access_token = null) |
1688 | { |
1689 | // json-encoding |
1690 | $postData = json_encode($data_to_send); |
1691 | |
1692 | $http_headers = []; |
1693 | if (isset($access_token)) { |
1694 | $http_headers['Authorization'] = 'Bearer ' . $access_token; |
1695 | } |
1696 | |
1697 | $result = $this->httpService->post( |
1698 | $this->paiaURL . $file, |
1699 | $postData, |
1700 | 'application/json; charset=UTF-8', |
1701 | $this->paiaTimeout, |
1702 | $http_headers |
1703 | ); |
1704 | |
1705 | if (!$result->isSuccess()) { |
1706 | // log error for debugging |
1707 | $this->debug( |
1708 | 'HTTP status ' . $result->getStatusCode() . |
1709 | ' received' |
1710 | ); |
1711 | } |
1712 | // return any result as error-handling is done elsewhere |
1713 | return $result->getBody(); |
1714 | } |
1715 | |
1716 | /** |
1717 | * GET data from foreign host |
1718 | * |
1719 | * @param string $file GET target URL |
1720 | * @param string $access_token PAIA access token for current session |
1721 | * |
1722 | * @return bool|string |
1723 | * @throws ILSException |
1724 | */ |
1725 | protected function paiaGetRequest($file, $access_token) |
1726 | { |
1727 | $http_headers = [ |
1728 | 'Authorization' => 'Bearer ' . $access_token, |
1729 | 'Content-type' => 'application/json; charset=UTF-8', |
1730 | ]; |
1731 | $result = $this->httpService->get( |
1732 | $this->paiaURL . $file, |
1733 | [], |
1734 | $this->paiaTimeout, |
1735 | $http_headers |
1736 | ); |
1737 | if (!$result->isSuccess()) { |
1738 | // log error for debugging |
1739 | $this->debug( |
1740 | 'HTTP status ' . $result->getStatusCode() . |
1741 | ' received' |
1742 | ); |
1743 | } |
1744 | // return any result as error-handling is done elsewhere |
1745 | return $result->getBody(); |
1746 | } |
1747 | |
1748 | /** |
1749 | * Helper function for PAIA to uniformely parse JSON |
1750 | * |
1751 | * @param string $file JSON data |
1752 | * |
1753 | * @return mixed |
1754 | * @throws \Exception |
1755 | */ |
1756 | protected function paiaParseJsonAsArray($file) |
1757 | { |
1758 | $responseArray = json_decode($file, true); |
1759 | |
1760 | // if we have an error response handle it accordingly (any will throw an |
1761 | // exception at the moment) and pass on the resulting exception |
1762 | if (isset($responseArray['error'])) { |
1763 | $this->paiaHandleErrors($responseArray); |
1764 | } |
1765 | |
1766 | return $responseArray; |
1767 | } |
1768 | |
1769 | /** |
1770 | * Retrieve file at given URL and return it as json_decoded array |
1771 | * |
1772 | * @param string $file GET target URL |
1773 | * |
1774 | * @return array|mixed |
1775 | * @throws ILSException |
1776 | */ |
1777 | protected function paiaGetAsArray($file) |
1778 | { |
1779 | $responseJson = $this->paiaGetRequest( |
1780 | $file, |
1781 | $this->getSession()->access_token |
1782 | ); |
1783 | $responseArray = $this->paiaParseJsonAsArray($responseJson); |
1784 | return $responseArray; |
1785 | } |
1786 | |
1787 | /** |
1788 | * Post something at given URL and return it as json_decoded array |
1789 | * |
1790 | * @param string $file POST target URL |
1791 | * @param array $data POST data |
1792 | * |
1793 | * @return array|mixed |
1794 | * @throws ILSException |
1795 | */ |
1796 | protected function paiaPostAsArray($file, $data) |
1797 | { |
1798 | $responseJson = $this->paiaPostRequest( |
1799 | $file, |
1800 | $data, |
1801 | $this->getSession()->access_token |
1802 | ); |
1803 | $responseArray = $this->paiaParseJsonAsArray($responseJson); |
1804 | return $responseArray; |
1805 | } |
1806 | |
1807 | /** |
1808 | * PAIA authentication function |
1809 | * |
1810 | * @param string $username Username |
1811 | * @param string $password Password |
1812 | * |
1813 | * @return mixed Associative array of patron info on successful login, |
1814 | * null on unsuccessful login, PEAR_Error on error. |
1815 | * @throws ILSException |
1816 | */ |
1817 | protected function paiaLogin($username, $password) |
1818 | { |
1819 | // as PAIA supports two authentication methods (defined as grant_type: |
1820 | // password or client_credentials), check which one is configured |
1821 | if (!in_array($this->grantType, ['password', 'client_credentials'])) { |
1822 | throw new ILSException( |
1823 | 'Unsupported PAIA grant_type configured: ' . $this->grantType |
1824 | ); |
1825 | } |
1826 | |
1827 | // prepare http header |
1828 | $header_data = []; |
1829 | |
1830 | // prepare post data depending on configured grant type |
1831 | $post_data = []; |
1832 | switch ($this->grantType) { |
1833 | case 'password': |
1834 | $post_data['username'] = $username; |
1835 | $post_data['password'] = $password; |
1836 | break; |
1837 | case 'client_credentials': |
1838 | // client_credentials only works if we have client_credentials |
1839 | // username and password (see PAIA.ini for further explanation) |
1840 | if ( |
1841 | isset($this->config['PAIA']['clientUsername']) |
1842 | && isset($this->config['PAIA']['clientPassword']) |
1843 | ) { |
1844 | $header_data['Authorization'] = 'Basic ' . |
1845 | base64_encode( |
1846 | $this->config['PAIA']['clientUsername'] . ':' . |
1847 | $this->config['PAIA']['clientPassword'] |
1848 | ); |
1849 | $post_data['patron'] = $username; // actual patron identifier |
1850 | } else { |
1851 | throw new ILSException( |
1852 | 'Missing username and/or password for PAIA grant_type' . |
1853 | ' client_credentials in PAIA configuration.' |
1854 | ); |
1855 | } |
1856 | break; |
1857 | } |
1858 | |
1859 | // finalize post data |
1860 | $post_data['grant_type'] = $this->grantType; |
1861 | $post_data['scope'] = self::SCOPE_READ_PATRON . ' ' . |
1862 | self::SCOPE_READ_FEES . ' ' . |
1863 | self::SCOPE_READ_ITEMS . ' ' . |
1864 | self::SCOPE_WRITE_ITEMS . ' ' . |
1865 | self::SCOPE_CHANGE_PASSWORD; |
1866 | |
1867 | // perform full PAIA auth and get patron info |
1868 | $result = $this->httpService->post( |
1869 | $this->paiaURL . 'auth/login', |
1870 | json_encode($post_data), |
1871 | 'application/json; charset=UTF-8', |
1872 | $this->paiaTimeout, |
1873 | $header_data |
1874 | ); |
1875 | |
1876 | if (!$result->isSuccess()) { |
1877 | // log error for debugging |
1878 | $this->debug( |
1879 | 'HTTP status ' . $result->getStatusCode() . |
1880 | ' received' |
1881 | ); |
1882 | } |
1883 | |
1884 | // continue with result data |
1885 | $responseJson = $result->getBody(); |
1886 | |
1887 | $responseArray = $this->paiaParseJsonAsArray($responseJson); |
1888 | if (!isset($responseArray['access_token'])) { |
1889 | throw new ILSException( |
1890 | 'Unknown error! Access denied.' |
1891 | ); |
1892 | } elseif (!isset($responseArray['patron'])) { |
1893 | throw new ILSException( |
1894 | 'Login credentials accepted, but got no patron ID?!?' |
1895 | ); |
1896 | } else { |
1897 | // at least access_token and patron got returned which is sufficient for |
1898 | // us, now save all to session |
1899 | $session = $this->getSession(); |
1900 | |
1901 | $session->patron |
1902 | = $responseArray['patron'] ?? null; |
1903 | $session->access_token |
1904 | = $responseArray['access_token'] ?? null; |
1905 | $session->scope |
1906 | = isset($responseArray['scope']) |
1907 | ? explode(' ', $responseArray['scope']) : null; |
1908 | $session->expires |
1909 | = isset($responseArray['expires_in']) |
1910 | ? (time() + ($responseArray['expires_in'])) : null; |
1911 | |
1912 | return true; |
1913 | } |
1914 | } |
1915 | |
1916 | /** |
1917 | * Support method for paiaLogin() -- load user details into session and return |
1918 | * array of basic user data. |
1919 | * |
1920 | * @param array $patron patron ID |
1921 | * |
1922 | * @return array |
1923 | * @throws ILSException |
1924 | */ |
1925 | protected function paiaGetUserDetails($patron) |
1926 | { |
1927 | // check if user has appropriate scope (refer to scope declaration above for |
1928 | // further details) |
1929 | if (!$this->paiaCheckScope(self::SCOPE_READ_PATRON)) { |
1930 | throw new ForbiddenException( |
1931 | 'Exception::access_denied_read_patron' |
1932 | ); |
1933 | } |
1934 | |
1935 | $responseJson = $this->paiaGetRequest( |
1936 | 'core/' . $patron, |
1937 | $this->getSession()->access_token |
1938 | ); |
1939 | $responseArray = $this->paiaParseJsonAsArray($responseJson); |
1940 | return $this->paiaParseUserDetails($patron, $responseArray); |
1941 | } |
1942 | |
1943 | /** |
1944 | * Checks if the current scope is set for active session. |
1945 | * |
1946 | * @param string $scope The scope to test for with the current session scopes. |
1947 | * |
1948 | * @return boolean |
1949 | */ |
1950 | protected function paiaCheckScope($scope) |
1951 | { |
1952 | return (!empty($scope) && is_array($this->getScope())) |
1953 | ? in_array($scope, $this->getScope()) : false; |
1954 | } |
1955 | |
1956 | /** |
1957 | * Check if storage retrieval request available |
1958 | * |
1959 | * This is responsible for determining if an item is requestable |
1960 | * |
1961 | * @param string $id The Bib ID |
1962 | * @param array $data An Array of item data |
1963 | * @param array $patron An array of patron data |
1964 | * |
1965 | * @return bool True if request is valid, false if not |
1966 | * |
1967 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1968 | */ |
1969 | public function checkStorageRetrievalRequestIsValid($id, $data, $patron) |
1970 | { |
1971 | return $this->checkRequestIsValid($id, $data, $patron); |
1972 | } |
1973 | |
1974 | /** |
1975 | * Check if hold or recall available |
1976 | * |
1977 | * This is responsible for determining if an item is requestable |
1978 | * |
1979 | * @param string $id The Bib ID |
1980 | * @param array $data An Array of item data |
1981 | * @param array $patron An array of patron data |
1982 | * |
1983 | * @return bool True if request is valid, false if not |
1984 | * |
1985 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1986 | */ |
1987 | public function checkRequestIsValid($id, $data, $patron) |
1988 | { |
1989 | // TODO: make this more configurable |
1990 | if ( |
1991 | isset($patron['status']) && $patron['status'] == 0 |
1992 | && isset($patron['expires']) && $patron['expires'] > date('Y-m-d') |
1993 | && in_array(self::SCOPE_WRITE_ITEMS, $this->getScope()) |
1994 | ) { |
1995 | return true; |
1996 | } |
1997 | return false; |
1998 | } |
1999 | |
2000 | /** |
2001 | * PAIA support method for PAIA core method 'notifications' |
2002 | * |
2003 | * @param array $patron Array with patron information |
2004 | * |
2005 | * @return array|mixed Array of system notifications for the patron |
2006 | * @throws \Exception |
2007 | * @throws ILSException You are not entitled to read notifications |
2008 | */ |
2009 | protected function paiaGetSystemMessages($patron) |
2010 | { |
2011 | // check if user has appropriate scope |
2012 | if (!$this->paiaCheckScope(self::SCOPE_READ_NOTIFICATIONS)) { |
2013 | throw new ILSException('You are not entitled to read notifications.'); |
2014 | } |
2015 | |
2016 | $cacheKey = null; |
2017 | if ($this->paiaCacheEnabled) { |
2018 | $cacheKey = $this->getCacheKey( |
2019 | 'notifications_' . $patron['cat_username'] |
2020 | ); |
2021 | $response = $this->getCachedData($cacheKey); |
2022 | if (!empty($response)) { |
2023 | return $response; |
2024 | } |
2025 | } |
2026 | |
2027 | try { |
2028 | $response = $this->paiaGetAsArray( |
2029 | 'core/' . $patron['cat_username'] . '/notifications' |
2030 | ); |
2031 | } catch (\Exception $e) { |
2032 | // all error handling is done in paiaHandleErrors |
2033 | // so pass on the exception |
2034 | throw $e; |
2035 | } |
2036 | |
2037 | $response = $this->enrichNotifications($response); |
2038 | |
2039 | if ($this->paiaCacheEnabled) { |
2040 | $this->putCachedData($cacheKey, $response); |
2041 | } |
2042 | |
2043 | return $response; |
2044 | } |
2045 | |
2046 | /** |
2047 | * Enriches PAIA notifications response with additional mappings |
2048 | * |
2049 | * @param array $notifications list of PAIA notifications |
2050 | * |
2051 | * @return array list of enriched PAIA notifications |
2052 | */ |
2053 | protected function enrichNotifications(array $notifications) |
2054 | { |
2055 | // not yet implemented |
2056 | return $notifications; |
2057 | } |
2058 | |
2059 | /** |
2060 | * PAIA support method for PAIA core method DELETE 'notifications' |
2061 | * |
2062 | * @param array $patron Array with patron information |
2063 | * @param string $messageId PAIA service specific ID |
2064 | * of the notification to remove |
2065 | * @param bool $keepCache if set to TRUE the notification cache will survive |
2066 | * the remote operation, this is used by |
2067 | * \VuFind\ILS\Driver\PAIA::paiaRemoveSystemMessages |
2068 | * to avoid unnecessary cache operations |
2069 | * |
2070 | * @return array|mixed Array of system notifications for the patron |
2071 | * @throws \Exception |
2072 | * @throws ILSException You are not entitled to read notifications |
2073 | */ |
2074 | protected function paiaRemoveSystemMessage( |
2075 | $patron, |
2076 | $messageId, |
2077 | $keepCache = false |
2078 | ) { |
2079 | // check if user has appropriate scope |
2080 | if (!$this->paiaCheckScope(self::SCOPE_DELETE_NOTIFICATIONS)) { |
2081 | throw new ILSException('You are not entitled to delete notifications.'); |
2082 | } |
2083 | |
2084 | try { |
2085 | $response = $this->paiaDeleteRequest( |
2086 | 'core/' |
2087 | . $patron['cat_username'] |
2088 | . '/notifications/' |
2089 | . $this->getPaiaNotificationsId($messageId) |
2090 | ); |
2091 | } catch (\Exception $e) { |
2092 | // all error handling is done in paiaHandleErrors |
2093 | // so pass on the exception |
2094 | throw $e; |
2095 | } |
2096 | |
2097 | if (!$keepCache && $this->paiaCacheEnabled) { |
2098 | $this->removeCachedData( |
2099 | $this->getCacheKey('notifications_' . $patron['cat_username']) |
2100 | ); |
2101 | } |
2102 | |
2103 | return $response; |
2104 | } |
2105 | |
2106 | /** |
2107 | * Removes multiple System Messages. Bulk deletion is not implemented in PAIA, |
2108 | * so this method iterates over the set of IDs and removes them separately |
2109 | * |
2110 | * @param array $patron Array with patron information |
2111 | * @param array $messageIds list of PAIA service specific IDs |
2112 | * of the notifications to remove |
2113 | * |
2114 | * @return bool TRUE if all messages have been successfully removed, |
2115 | * otherwise FALSE |
2116 | * @throws ILSException |
2117 | */ |
2118 | protected function paiaRemoveSystemMessages($patron, array $messageIds) |
2119 | { |
2120 | foreach ($messageIds as $messageId) { |
2121 | if (!$this->paiaRemoveSystemMessage($patron, $messageId, true)) { |
2122 | return false; |
2123 | } |
2124 | } |
2125 | |
2126 | if ($this->paiaCacheEnabled) { |
2127 | $this->removeCachedData( |
2128 | $this->getCacheKey('notifications_' . $patron['cat_username']) |
2129 | ); |
2130 | } |
2131 | |
2132 | return true; |
2133 | } |
2134 | |
2135 | /** |
2136 | * Get notification identifier from message identifier |
2137 | * |
2138 | * @param string $messageId Message identifier |
2139 | * |
2140 | * @return string |
2141 | */ |
2142 | protected function getPaiaNotificationsId($messageId) |
2143 | { |
2144 | return $messageId; |
2145 | } |
2146 | |
2147 | /** |
2148 | * DELETE data on foreign host |
2149 | * |
2150 | * @param string $file DELETE target URL |
2151 | * @param string $access_token PAIA access token for current session |
2152 | * |
2153 | * @return bool|string |
2154 | * @throws ILSException |
2155 | */ |
2156 | protected function paiaDeleteRequest($file, $access_token = null) |
2157 | { |
2158 | if (null === $access_token) { |
2159 | $access_token = $this->getSession()->access_token; |
2160 | } |
2161 | |
2162 | $http_headers = [ |
2163 | 'Authorization' => 'Bearer ' . $access_token, |
2164 | 'Content-type' => 'application/json; charset=UTF-8', |
2165 | ]; |
2166 | |
2167 | try { |
2168 | $client = $this->httpService->createClient( |
2169 | $this->paiaURL . $file, |
2170 | \Laminas\Http\Request::METHOD_DELETE, |
2171 | $this->paiaTimeout |
2172 | ); |
2173 | $client->setHeaders($http_headers); |
2174 | $result = $client->send(); |
2175 | } catch (\Exception $e) { |
2176 | $this->throwAsIlsException($e); |
2177 | } |
2178 | |
2179 | if (!$result->isSuccess()) { |
2180 | // log error for debugging |
2181 | $this->debug( |
2182 | 'HTTP status ' . $result->getStatusCode() . |
2183 | ' received' |
2184 | ); |
2185 | return false; |
2186 | } |
2187 | // return TRUE on success |
2188 | return true; |
2189 | } |
2190 | |
2191 | /** |
2192 | * Check whether the patron has any blocks on their account. |
2193 | * |
2194 | * @param array $patron Patron data from patronLogin(). |
2195 | * |
2196 | * @return mixed A boolean false if no blocks are in place and an array |
2197 | * of block reasons if blocks are in place |
2198 | * |
2199 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
2200 | */ |
2201 | public function getAccountBlocks($patron) |
2202 | { |
2203 | $blocks = []; |
2204 | |
2205 | foreach ($this->accountBlockNotificationsForMissingScopes as $scope => $message) { |
2206 | if (!$this->paiaCheckScope($scope)) { |
2207 | $blocks[$scope] = $message; |
2208 | } |
2209 | } |
2210 | |
2211 | // Special case: if update patron is missing, we don't need to also add |
2212 | // more specific messages. |
2213 | if (isset($blocks[self::SCOPE_UPDATE_PATRON])) { |
2214 | unset($blocks[self::SCOPE_UPDATE_PATRON_NAME]); |
2215 | unset($blocks[self::SCOPE_UPDATE_PATRON_EMAIL]); |
2216 | unset($blocks[self::SCOPE_UPDATE_PATRON_ADDRESS]); |
2217 | } |
2218 | |
2219 | return count($blocks) ? array_values($blocks) : false; |
2220 | } |
2221 | } |