Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 886 |
|
0.00% |
0 / 37 |
CRAP | |
0.00% |
0 / 1 |
OverdriveConnector | |
0.00% |
0 / 886 |
|
0.00% |
0 / 37 |
46010 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getSessionContainer | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
getUser | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getAccess | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
56 | |||
getAvailability | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
90 | |||
getAvailabilityBulk | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
272 | |||
getCollectionToken | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
72 | |||
doOverdriveCheckout | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
56 | |||
placeOverDriveHold | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
42 | |||
updateOverDriveHold | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
30 | |||
suspendHold | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
56 | |||
editSuspendedHold | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
42 | |||
deleteHoldSuspension | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
cancelHold | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
30 | |||
returnResource | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
30 | |||
getDownloadRedirect | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
42 | |||
getAuthHeader | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
getConfig | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
12 | |||
getFormatNames | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
2 | |||
getPermanentLinks | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
getMagazineIssues | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
42 | |||
getMetadata | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
30 | |||
getMetadataForTitles | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
getCheckout | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getHold | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
getCheckouts | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
110 | |||
getHolds | |
0.00% |
0 / 48 |
|
0.00% |
0 / 1 |
132 | |||
callUrl | |
0.00% |
0 / 47 |
|
0.00% |
0 / 1 |
132 | |||
connectToAPI | |
0.00% |
0 / 46 |
|
0.00% |
0 / 1 |
90 | |||
callPatronUrl | |
0.00% |
0 / 71 |
|
0.00% |
0 / 1 |
240 | |||
connectToPatronAPI | |
0.00% |
0 / 60 |
|
0.00% |
0 / 1 |
132 | |||
getHttpClient | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
setCacheStorage | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCachedData | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
putCachedData | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
removeCachedData | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getResultObject | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * PHP version 8 |
5 | * |
6 | * Copyright (C) Villanova University 2018. |
7 | * |
8 | * This program is free software; you can redistribute it and/or modify |
9 | * it under the terms of the GNU General Public License version 2, |
10 | * as published by the Free Software Foundation. |
11 | * |
12 | * This program is distributed in the hope that it will be useful, |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | * GNU General Public License for more details. |
16 | * |
17 | * You should have received a copy of the GNU General Public License |
18 | * along with this program; if not, write to the Free Software |
19 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 |
20 | * USA |
21 | * |
22 | * @category VuFind |
23 | * @package DigitalContent |
24 | * @author Demian Katz <demian.katz@villanova.edu> |
25 | * @author Brent Palmer <brent-palmer@icpl.org> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public |
27 | * License |
28 | * @link https://vufind.org/wiki/development Wiki |
29 | */ |
30 | |
31 | namespace VuFind\DigitalContent; |
32 | |
33 | use Exception; |
34 | use Laminas\Cache\Storage\StorageInterface; |
35 | use Laminas\Config\Config; |
36 | use Laminas\Http\Client; |
37 | use Laminas\Log\LoggerAwareInterface; |
38 | use Laminas\Session\Container; |
39 | use LmcRbacMvc\Service\AuthorizationServiceAwareInterface; |
40 | use LmcRbacMvc\Service\AuthorizationServiceAwareTrait; |
41 | use VuFind\Auth\ILSAuthenticator; |
42 | use VuFind\Cache\KeyGeneratorTrait; |
43 | use VuFind\Exception\ILS as ILSException; |
44 | |
45 | use function count; |
46 | |
47 | /** |
48 | * OverdriveConnector |
49 | * |
50 | * Class responsible for connecting to the OverDrive API |
51 | * |
52 | * @category VuFind |
53 | * @package DigitalContent |
54 | * @author Demian Katz <demian.katz@villanova.edu> |
55 | * @author Brent Palmer <brent-palmer@icpl.org> |
56 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public |
57 | * License |
58 | * @link https://vufind.org/wiki/development Wiki |
59 | * @todo provide option for autocheckout by default in config |
60 | * allow override for cover display using other covers |
61 | * provide option for not requiring email for holds |
62 | * provide option for giving users option for every hold |
63 | * provide option for asking about autocheckout for every hold |
64 | * provide config options for how to handle patrons with no access to OD |
65 | */ |
66 | class OverdriveConnector implements |
67 | LoggerAwareInterface, |
68 | AuthorizationServiceAwareInterface, |
69 | \VuFindHttp\HttpServiceAwareInterface |
70 | { |
71 | use \VuFind\Log\LoggerAwareTrait { |
72 | logError as error; |
73 | } |
74 | use AuthorizationServiceAwareTrait; |
75 | use \VuFindHttp\HttpServiceAwareTrait; |
76 | use KeyGeneratorTrait; |
77 | |
78 | /** |
79 | * Session Container |
80 | * |
81 | * @var Container |
82 | */ |
83 | protected $sessionContainer; |
84 | |
85 | /** |
86 | * OverDrive-specific configuration |
87 | * |
88 | * @var Config |
89 | */ |
90 | protected $recordConfig; |
91 | |
92 | /** |
93 | * Main VuFind configuration |
94 | * |
95 | * @var Config |
96 | */ |
97 | protected $mainConfig; |
98 | |
99 | /** |
100 | * ILS Authorization |
101 | * |
102 | * @var ILSAuthenticator |
103 | */ |
104 | protected $ilsAuth; |
105 | |
106 | /** |
107 | * HTTP Client |
108 | * |
109 | * Client for making calls to the API |
110 | * |
111 | * @var Client |
112 | */ |
113 | protected $client; |
114 | |
115 | /** |
116 | * Cache for storing ILS data temporarily (e.g. patron blocks) |
117 | * |
118 | * @var StorageInterface |
119 | */ |
120 | protected $cache = null; |
121 | |
122 | /** |
123 | * Constructor |
124 | * |
125 | * @param Config $mainConfig VuFind main conf |
126 | * @param Config $recordConfig Record-specific conf file |
127 | * @param ILSAuthenticator $ilsAuth ILS Authenticator |
128 | * @param Container $sessionContainer container |
129 | */ |
130 | public function __construct( |
131 | Config $mainConfig, |
132 | Config $recordConfig, |
133 | ILSAuthenticator $ilsAuth, |
134 | Container $sessionContainer = null |
135 | ) { |
136 | $this->mainConfig = $mainConfig; |
137 | $this->recordConfig = $recordConfig; |
138 | $this->ilsAuth = $ilsAuth; |
139 | $this->sessionContainer = $sessionContainer; |
140 | } |
141 | |
142 | /** |
143 | * Loads the session container |
144 | * |
145 | * @return \Laminas\Session\Container |
146 | */ |
147 | protected function getSessionContainer() |
148 | { |
149 | if (null === $this->sessionContainer || !$this->sessionContainer) { |
150 | error_log('NO SESSION CONTAINER'); |
151 | } |
152 | return $this->sessionContainer; |
153 | } |
154 | |
155 | /** |
156 | * Get (Logged-in) User |
157 | * |
158 | * Returns the currently logged in user or false if the user is not |
159 | * |
160 | * @return array|boolean an array of user info from the ILSAuthenticator |
161 | * or false if user is not logged in. |
162 | */ |
163 | public function getUser() |
164 | { |
165 | try { |
166 | $user = $this->ilsAuth->storedCatalogLogin(); |
167 | } catch (ILSException $e) { |
168 | return false; |
169 | } |
170 | return $user; |
171 | } |
172 | |
173 | /** |
174 | * Get OverDrive Access |
175 | * |
176 | * Whether the patron has access to overdrive actions (hold, |
177 | * checkout etc.). |
178 | * This is stored and retrieved from the session. |
179 | * |
180 | * @param bool $refresh Force a check instead of checking cache |
181 | * |
182 | * @return object |
183 | */ |
184 | public function getAccess($refresh = false) |
185 | { |
186 | if (!$user = $this->getUser()) { |
187 | return $this->getResultObject(false, 'User not logged in.'); |
188 | } |
189 | |
190 | $odAccess = $this->getSessionContainer()->odAccess; |
191 | if ($refresh || empty($odAccess)) { |
192 | if ( |
193 | $this->connectToPatronAPI( |
194 | $user['cat_username'], |
195 | $user['cat_password'], |
196 | true |
197 | ) |
198 | ) { |
199 | $result = $this->getSessionContainer()->odAccess |
200 | = $this->getResultObject(true); |
201 | } else { |
202 | $result = $this->getResultObject(); |
203 | // There is some problem with the account |
204 | $result->code = 'od_account_problem'; |
205 | $conf = $this->getConfig(); |
206 | |
207 | if ($conf->noAccessString) { |
208 | if ( |
209 | str_contains( |
210 | $this->getSessionContainer()->odAccessMessage, |
211 | $conf->noAccessString |
212 | ) |
213 | ) { |
214 | // This user should not have access to OD |
215 | $result->code = 'od_account_noaccess'; |
216 | } |
217 | } |
218 | // odAccessMessage is set in the session by the API call above |
219 | // maybe it should be saved to a class property instead |
220 | $result->msg = $this->getSessionContainer()->odAccessMessage; |
221 | $this->getSessionContainer()->odAccess = $result; |
222 | } |
223 | } else { |
224 | $result = $this->getSessionContainer()->odAccess; |
225 | } |
226 | |
227 | return $result; |
228 | } |
229 | |
230 | /** |
231 | * Get Availability |
232 | * |
233 | * Retrieves the availability for a single resource from OverDrive API |
234 | * with information like copiesOwned, copiesAvailable, numberOfHolds et. |
235 | * |
236 | * @param string $overDriveId The OverDrive ID (reserve ID) of the eResource |
237 | * |
238 | * @return object Standard object with availability info |
239 | * |
240 | * @link https://developer.overdrive.com/apis/library-availability-new |
241 | */ |
242 | public function getAvailability($overDriveId) |
243 | { |
244 | $result = $this->getResultObject(); |
245 | if (!$overDriveId) { |
246 | $this->logWarning('no overdrive content ID was passed in.'); |
247 | return $result; |
248 | } |
249 | |
250 | if ($conf = $this->getConfig()) { |
251 | $collectionToken = $this->getCollectionToken(); |
252 | // Hmm, no token. If user is logged in let's check access |
253 | if (!$collectionToken && $this->getUser()) { |
254 | $accessResult = $this->getAccess(); |
255 | if (!$accessResult->status) { |
256 | return $accessResult; |
257 | } |
258 | } |
259 | $baseUrl = $conf->discURL; |
260 | $availabilityUrl |
261 | = "$baseUrl/v2/collections/$collectionToken/products/"; |
262 | $availabilityUrl .= "$overDriveId/availability"; |
263 | $res = $this->callUrl($availabilityUrl); |
264 | |
265 | if (($res->errorCode ?? '') == 'NotFound') { |
266 | if ($conf->consortiumSupport && !$this->getUser()) { |
267 | // Consortium support is turned on but user is not logged in; |
268 | // if the title is not found it probably means that it's only |
269 | // available to some users. |
270 | $result->status = true; |
271 | $result->code = 'od_code_login_for_avail'; |
272 | } else { |
273 | $result->status = false; |
274 | $result->code = 'od_code_resource_not_found'; |
275 | $this->logWarning("resource not found: $overDriveId"); |
276 | } |
277 | } else { |
278 | $result->status = true; |
279 | $res->copiesAvailable ??= 0; |
280 | $res->copiesOwned ??= 0; |
281 | $res->numberOfHolds ??= 0; |
282 | $res->code = 'od_none'; |
283 | $result->data = $res; |
284 | } |
285 | } |
286 | |
287 | return $result; |
288 | } |
289 | |
290 | /** |
291 | * Get Availability (in) Bulk |
292 | * |
293 | * Gets availability for up to 25 titles at once. This is used by the |
294 | * the ajax availability system |
295 | * |
296 | * @param array $overDriveIds The OverDrive ID (reserve IDs) of the |
297 | * eResources |
298 | * |
299 | * @return object|bool see getAvailability |
300 | * |
301 | * @todo if more than 25 passed in, make multiple calls |
302 | */ |
303 | public function getAvailabilityBulk($overDriveIds = []) |
304 | { |
305 | $result = $this->getResultObject(); |
306 | $loginRequired = false; |
307 | if (count($overDriveIds) < 1) { |
308 | $this->logWarning('no overdrive content ID was passed in.'); |
309 | return false; |
310 | } |
311 | |
312 | if ($conf = $this->getConfig()) { |
313 | if ($conf->consortiumSupport && !$this->getUser()) { |
314 | $loginRequired = true; |
315 | } |
316 | $collectionToken = $this->getCollectionToken(); |
317 | // Hmm, no token. If user is logged in let's check access |
318 | if (!$collectionToken && $this->getUser()) { |
319 | $accessResult = $this->getAccess(); |
320 | if (!$accessResult->status) { |
321 | return $accessResult; |
322 | } |
323 | } |
324 | $baseUrl = $conf->discURL; |
325 | $availabilityPath = '/v2/collections/'; |
326 | $availabilityPath .= "$collectionToken/availability?products="; |
327 | $availabilityUrl = $baseUrl . $availabilityPath . |
328 | implode(',', $overDriveIds); |
329 | $res = $this->callUrl($availabilityUrl); |
330 | if (!$res) { |
331 | $result->code = 'od_code_connection_failed'; |
332 | } else { |
333 | if (($res->errorCode ?? null) == 'NotFound' || $res->totalItems == 0) { |
334 | if ($loginRequired) { |
335 | // Consortium support is turned on but user is not logged in. |
336 | // If the title is not found it could mean that it's only |
337 | // available to some users. |
338 | $result->status = true; |
339 | $result->code = 'od_code_login_for_avail'; |
340 | } else { |
341 | $result->status = false; |
342 | $result->code = 'od_code_resource_not_found'; |
343 | $this->logWarning('resources not found'); |
344 | } |
345 | } else { |
346 | $result->status = true; |
347 | foreach ($res->availability as $item) { |
348 | $item->copiesAvailable ??= 0; |
349 | $item->copiesOwned ??= 0; |
350 | $item->numberOfHolds ??= 0; |
351 | $result->data[strtolower($item->reserveId)] = $item; |
352 | $res->code = 'od_none'; |
353 | } |
354 | // Now look for items not returned |
355 | foreach ($overDriveIds as $id) { |
356 | if (!isset($result->data[$id])) { |
357 | if ($loginRequired) { |
358 | $result->data[$id] = $this->getResultObject( |
359 | $status = false, |
360 | $msg = '', |
361 | $code = 'od_code_login_for_avail' |
362 | ); |
363 | } else { |
364 | $result->data[$id] = $this->getResultObject( |
365 | $status = false, |
366 | $msg = '', |
367 | $code = 'od_code_resource_not_found' |
368 | ); |
369 | } |
370 | } |
371 | } |
372 | } |
373 | } |
374 | } |
375 | return $result; |
376 | } |
377 | |
378 | /** |
379 | * Get Collection Token |
380 | * |
381 | * Gets the collection token for the OverDrive collection. The collection |
382 | * token doesn't change much but according to |
383 | * the OD API docs it could change and should be retrieved each session. |
384 | * Also, the collection token depends on the user if the user is in a |
385 | * consortium. If consortium support is turned on then the user collection |
386 | * token will override the library collection token. |
387 | * The token itself is returned but it's also saved in the session and |
388 | * automatically returned. |
389 | * |
390 | * @return object|bool A collection token for the library's collection. |
391 | */ |
392 | public function getCollectionToken() |
393 | { |
394 | $collectionToken = $this->getCachedData('collectionToken'); |
395 | $userCollectionToken = $this->getSessionContainer( |
396 | )->userCollectionToken; |
397 | $conf = $this->getConfig(); |
398 | if ($conf->consortiumSupport && $conf->usePatronAPI && $user = $this->getUser()) { |
399 | if (empty($userCollectionToken)) { |
400 | $baseUrl = $conf->circURL; |
401 | $patronURL = "$baseUrl/v1/patrons/me"; |
402 | $res = $this->callPatronUrl( |
403 | $user['cat_username'], |
404 | $user['cat_password'], |
405 | $patronURL |
406 | ); |
407 | if ($res) { |
408 | $userCollectionToken = $res->collectionToken; |
409 | $this->getSessionContainer()->userCollectionToken |
410 | = $userCollectionToken; |
411 | } else { |
412 | return false; |
413 | } |
414 | } |
415 | return $userCollectionToken; |
416 | } |
417 | if (empty($collectionToken)) { |
418 | $this->debug('getting new collectionToken'); |
419 | $baseUrl = $conf->discURL; |
420 | $libraryID = $conf->libraryID; |
421 | $libraryURL = "$baseUrl/v1/libraries/$libraryID"; |
422 | $res = $this->callUrl($libraryURL); |
423 | if ($res) { |
424 | $collectionToken = $res->collectionToken; |
425 | $this->debug("OD collectionToken: $collectionToken"); |
426 | $this->putCachedData('collectionToken', $collectionToken); |
427 | } else { |
428 | return false; |
429 | } |
430 | } |
431 | return $collectionToken; |
432 | } |
433 | |
434 | /** |
435 | * OverDrive Checkout |
436 | * Processes a request to checkout a title from OverDrive |
437 | * |
438 | * @param string $overDriveId The overdrive id for the title |
439 | * |
440 | * @return object $result Results of the call. |
441 | */ |
442 | public function doOverdriveCheckout($overDriveId) |
443 | { |
444 | $result = $this->getResultObject(); |
445 | |
446 | $this->debug('OverDriveConnector: doOverdriveCheckout|overdriveID: ' . $overDriveId); |
447 | if (!$user = $this->getUser()) { |
448 | $this->error('Checkout - user is not logged in', [], true); |
449 | return $result; |
450 | } |
451 | if ($config = $this->getConfig()) { |
452 | if (!$config->usePatronAPI) { |
453 | $this->error('Checkout - OverDrive patron APIs are disabled.'); |
454 | return $result; |
455 | } |
456 | $url = $config->circURL . '/v1/patrons/me/checkouts'; |
457 | $params = [ |
458 | 'reserveId' => $overDriveId, |
459 | ]; |
460 | |
461 | $response = $this->callPatronUrl( |
462 | $user['cat_username'], |
463 | $user['cat_password'], |
464 | $url, |
465 | $params, |
466 | 'POST' |
467 | ); |
468 | |
469 | if (!empty($response)) { |
470 | if (isset($response->reserveId)) { |
471 | $expires = ''; |
472 | if ($dt = new \DateTime($response->expires)) { |
473 | $expires = $dt->format( |
474 | (string)$config->displayDateFormat |
475 | ); |
476 | } |
477 | $result->status = true; |
478 | $result->data = (object)[]; |
479 | $result->data->expires = $expires; |
480 | $result->data->formats = $response->formats; |
481 | // Add the checkout to the session cache |
482 | $this->getSessionContainer()->checkouts[] = $response; |
483 | } else { |
484 | $result->msg = $response->message; |
485 | } |
486 | } else { |
487 | $result->code = 'od_code_connection_failed'; |
488 | } |
489 | } |
490 | return $result; |
491 | } |
492 | |
493 | /** |
494 | * Places a hold on an item within OverDrive |
495 | * |
496 | * @param string $overDriveId The overdrive id for the title |
497 | * @param string $email The email overdrive should use for notification |
498 | * |
499 | * @return \stdClass Object with result |
500 | */ |
501 | public function placeOverDriveHold($overDriveId, $email) |
502 | { |
503 | $overDriveId = strtoupper($overDriveId); |
504 | $this->debug('OverDriveConnector: placeOverDriveHold: OverDriveID: $overDriveId, Email: $email'); |
505 | $holdResult = $this->getResultObject(); |
506 | |
507 | if (!$user = $this->getUser()) { |
508 | $this->error('user is not logged in (hold)', [], true); |
509 | return $holdResult; |
510 | } |
511 | |
512 | if ($config = $this->getConfig()) { |
513 | if (!$config->usePatronAPI) { |
514 | $this->error('Place hold - OverDrive patron APIs are disabled.'); |
515 | return $holdResult; |
516 | } |
517 | $ignoreHoldEmail = false; |
518 | $url = $config->circURL . '/v1/patrons/me/holds'; |
519 | $action = 'POST'; |
520 | $params = [ |
521 | 'reserveId' => $overDriveId, |
522 | 'emailAddress' => $email, |
523 | 'ignoreHoldEmail' => $ignoreHoldEmail, |
524 | ]; |
525 | |
526 | $response = $this->callPatronUrl( |
527 | $user['cat_username'], |
528 | $user['cat_password'], |
529 | $url, |
530 | $params, |
531 | $action |
532 | ); |
533 | |
534 | if (!empty($response)) { |
535 | if (isset($response->holdListPosition)) { |
536 | $holdResult->status = true; |
537 | $holdResult->data = (object)[]; |
538 | $holdResult->data->holdListPosition |
539 | = $response->holdListPosition; |
540 | } else { |
541 | $holdResult->msg = $response->message; |
542 | } |
543 | } else { |
544 | $holdResult->code = 'od_code_connection_failed'; |
545 | } |
546 | } |
547 | return $holdResult; |
548 | } |
549 | |
550 | /** |
551 | * Updates the email address for a hold on an item within OverDrive |
552 | * |
553 | * @param string $overDriveId The overdrive id for the title |
554 | * @param string $email The email overdrive should use for notif |
555 | * |
556 | * @return \stdClass Object with result |
557 | */ |
558 | public function updateOverDriveHold($overDriveId, $email) |
559 | { |
560 | $overDriveId = strtoupper($overDriveId); |
561 | $this->debug('OverDriveConnector: updateOverDriveHold: OverDriveID: $overDriveId, Email: $email'); |
562 | $holdResult = $this->getResultObject(); |
563 | |
564 | if (!$user = $this->getUser()) { |
565 | $this->error('user is not logged in (upd hold)', [], true); |
566 | return $holdResult; |
567 | } |
568 | |
569 | if ($config = $this->getConfig()) { |
570 | if (!$config->usePatronAPI) { |
571 | $this->error('Update hold - OverDrive patron APIs are disabled.'); |
572 | return $holdResult; |
573 | } |
574 | $autoCheckout = true; |
575 | $ignoreHoldEmail = false; |
576 | $url = $config->circURL . '/v1/patrons/me/holds/' . $overDriveId; |
577 | $action = 'PUT'; |
578 | $params = [ |
579 | 'reserveId' => $overDriveId, |
580 | 'emailAddress' => $email, |
581 | ]; |
582 | |
583 | $response = $this->callPatronUrl( |
584 | $user['cat_username'], |
585 | $user['cat_password'], |
586 | $url, |
587 | $params, |
588 | $action |
589 | ); |
590 | |
591 | // because this is a PUT Call, we are just looking for a boolean |
592 | if ($response) { |
593 | $holdResult->status = true; |
594 | } else { |
595 | $holdResult->msg = $response->message; |
596 | } |
597 | } |
598 | return $holdResult; |
599 | } |
600 | |
601 | /** |
602 | * Suspend Hold |
603 | * Suspend an existing OverDrive Hold |
604 | * |
605 | * @param string $overDriveId The overdrive id for the title |
606 | * @param string $email The email overdrive should use for notif |
607 | * @param string $suspensionType indefinite or limited |
608 | * @param int $numberOfDays number of days to suspend the hold |
609 | * |
610 | * @return \stdClass Object with result |
611 | */ |
612 | public function suspendHold($overDriveId, $email, $suspensionType = 'indefinite', $numberOfDays = 7) |
613 | { |
614 | $holdResult = $this->getResultObject(); |
615 | $this->debug("suspendHold: OverDriveID:$overDriveId|Email:$email|suspensionType:$suspensionType"); |
616 | |
617 | if (!$user = $this->getUser()) { |
618 | $this->error('user is not logged in (susp hold)', [], true); |
619 | return $holdResult; |
620 | } |
621 | if ($config = $this->getConfig()) { |
622 | if (!$config->usePatronAPI) { |
623 | $this->error('Suspend hold - OverDrive patron APIs are disabled.'); |
624 | return $holdResult; |
625 | } |
626 | $url = $config->circURL . "/v1/patrons/me/holds/$overDriveId/suspension"; |
627 | $action = 'POST'; |
628 | $params = [ |
629 | 'emailAddress' => $email, |
630 | 'suspensionType' => $suspensionType, |
631 | ]; |
632 | if ($suspensionType == 'limited') { |
633 | $params['numberOfDays'] = $numberOfDays; |
634 | } |
635 | |
636 | $response = $this->callPatronUrl( |
637 | $user['cat_username'], |
638 | $user['cat_password'], |
639 | $url, |
640 | $params, |
641 | $action |
642 | ); |
643 | |
644 | if (!empty($response)) { |
645 | if (isset($response->holdListPosition)) { |
646 | $holdResult->status = true; |
647 | $holdResult->data = $response; |
648 | } else { |
649 | $holdResult->msg = $response->message; |
650 | } |
651 | } else { |
652 | $holdResult->code = 'od_code_connection_failed'; |
653 | } |
654 | } |
655 | return $holdResult; |
656 | } |
657 | |
658 | /** |
659 | * Edit Suspended Hold |
660 | * Change the redelivery date on an already suspended hold |
661 | * |
662 | * @param string $overDriveId The overdrive id for the title |
663 | * @param string $email The email overdrive should use for notif |
664 | * @param string $suspensionType indefinite or limited |
665 | * @param int $numberOfDays number of days to suspend the hold |
666 | * |
667 | * @return \stdClass Object with result |
668 | */ |
669 | public function editSuspendedHold($overDriveId, $email, $suspensionType = 'indefinite', $numberOfDays = 7) |
670 | { |
671 | $holdResult = $this->getResultObject(); |
672 | |
673 | if (!$user = $this->getUser()) { |
674 | $this->error('user is not logged in (edit susp hold)', [], true); |
675 | return $holdResult; |
676 | } |
677 | if ($config = $this->getConfig()) { |
678 | if (!$config->usePatronAPI) { |
679 | $this->error('Edit suspended hold - OverDrive patron APIs are disabled.'); |
680 | return $holdResult; |
681 | } |
682 | $url = $config->circURL . "/v1/patrons/me/holds/$overDriveId/suspension"; |
683 | $action = 'PUT'; |
684 | $params = [ |
685 | 'emailAddress' => $email, |
686 | 'suspensionType' => $suspensionType, |
687 | ]; |
688 | if ($suspensionType == 'limited') { |
689 | $params['numberOfDays'] = $numberOfDays; |
690 | } |
691 | |
692 | $response = $this->callPatronUrl( |
693 | $user['cat_username'], |
694 | $user['cat_password'], |
695 | $url, |
696 | $params, |
697 | $action |
698 | ); |
699 | |
700 | // because this is a PUT Call, we are just looking for a boolean |
701 | if ($response) { |
702 | $holdResult->status = true; |
703 | } else { |
704 | $holdResult->msg = $response->message; |
705 | } |
706 | } |
707 | return $holdResult; |
708 | } |
709 | |
710 | /** |
711 | * Delete Suspended Hold |
712 | * Removes the suspension from a hold |
713 | * |
714 | * @param string $overDriveId The overdrive id for the title |
715 | * |
716 | * @return \stdClass Object with result |
717 | */ |
718 | public function deleteHoldSuspension($overDriveId) |
719 | { |
720 | $holdResult = $this->getResultObject(); |
721 | |
722 | if (!$user = $this->getUser()) { |
723 | $this->error('user is not logged in (del hold susp)', [], true); |
724 | return $holdResult; |
725 | } |
726 | if ($config = $this->getConfig()) { |
727 | if (!$config->usePatronAPI) { |
728 | $this->error('Delete hold suspension - OverDrive patron APIs are disabled.'); |
729 | return $holdResult; |
730 | } |
731 | $url = $config->circURL . "/v1/patrons/me/holds/$overDriveId/suspension"; |
732 | $action = 'DELETE'; |
733 | $response = $this->callPatronUrl( |
734 | $user['cat_username'], |
735 | $user['cat_password'], |
736 | $url, |
737 | null, |
738 | $action |
739 | ); |
740 | |
741 | // because this is a DELETE Call, we are just looking for a boolean |
742 | if ($response) { |
743 | $holdResult->status = true; |
744 | } else { |
745 | $holdResult->msg = $response->message; |
746 | } |
747 | } |
748 | return $holdResult; |
749 | } |
750 | |
751 | /** |
752 | * Cancel Hold |
753 | * Cancel and existing OverDrive Hold |
754 | * |
755 | * @param string $overDriveId The overdrive id for the title |
756 | * |
757 | * @return \stdClass Object with result |
758 | */ |
759 | public function cancelHold($overDriveId) |
760 | { |
761 | $holdResult = $this->getResultObject(); |
762 | $this->debug('OverDriveConnector: cancelHold | OverDriveID: $overDriveID'); |
763 | if (!$user = $this->getUser()) { |
764 | $this->error('user is not logged in (cancel hold)', [], true); |
765 | return $holdResult; |
766 | } |
767 | if ($config = $this->getConfig()) { |
768 | if (!$config->usePatronAPI) { |
769 | $this->error('Cancel hold - OverDrive patron APIs are disabled.'); |
770 | return $holdResult; |
771 | } |
772 | $url = $config->circURL . "/v1/patrons/me/holds/$overDriveId"; |
773 | $action = 'DELETE'; |
774 | $response = $this->callPatronUrl( |
775 | $user['cat_username'], |
776 | $user['cat_password'], |
777 | $url, |
778 | null, |
779 | $action |
780 | ); |
781 | |
782 | // Because this is a DELETE Call, we are just looking for a boolean |
783 | if ($response) { |
784 | $holdResult->status = true; |
785 | } else { |
786 | $holdResult->msg = $response->message; |
787 | } |
788 | } |
789 | return $holdResult; |
790 | } |
791 | |
792 | /** |
793 | * Return Resource |
794 | * Return a title early. |
795 | * |
796 | * @param string $resourceID OverDrive ID of the resource |
797 | * |
798 | * @return object|bool Object with result |
799 | */ |
800 | public function returnResource($resourceID) |
801 | { |
802 | $result = $this->getResultObject(); |
803 | if (!$user = $this->getUser()) { |
804 | $this->error('user is not logged in (return)', [], true); |
805 | return $result; |
806 | } |
807 | if ($config = $this->getConfig()) { |
808 | if (!$config->usePatronAPI) { |
809 | $this->error('Return resource - OverDrive patron APIs are disabled.'); |
810 | return $result; |
811 | } |
812 | $url = $config->circURL . "/v1/patrons/me/checkouts/$resourceID"; |
813 | $action = 'DELETE'; |
814 | $response = $this->callPatronUrl( |
815 | $user['cat_username'], |
816 | $user['cat_password'], |
817 | $url, |
818 | null, |
819 | $action |
820 | ); |
821 | |
822 | // Because this is a DELETE Call, we are just looking for a boolean |
823 | if ($response) { |
824 | $result->status = true; |
825 | } else { |
826 | $result->msg = $response->message; |
827 | } |
828 | } |
829 | return $result; |
830 | } |
831 | |
832 | /** |
833 | * Get Download Redirect for an OverDrive Resource |
834 | * |
835 | * @param string $overDriveId OverDrive ID |
836 | * |
837 | * @return object Object with result. If successful, then data will |
838 | * have the download URI ($result->data->downloadRedirect) |
839 | */ |
840 | public function getDownloadRedirect($overDriveId) |
841 | { |
842 | $result = $this->getResultObject(); |
843 | $downloadLink = false; |
844 | if (!$user = $this->getUser()) { |
845 | $this->error('user is not logged in', false, true); |
846 | return $result; |
847 | } |
848 | if (($config = $this->getConfig()) && !$config->usePatronAPI) { |
849 | $this->error('Get download redirect - OverDrive patron APIs are disabled.'); |
850 | return $result; |
851 | } |
852 | $checkout = $this->getCheckout($overDriveId, false); |
853 | if ($checkout) { |
854 | $dlRedirectUrl = $checkout->links->downloadRedirect->href; |
855 | |
856 | $response = $this->callPatronUrl( |
857 | $user['cat_username'], |
858 | $user['cat_password'], |
859 | $dlRedirectUrl, |
860 | null, |
861 | 'GET', |
862 | 'redirect' |
863 | ); |
864 | if (!empty($response)) { |
865 | $result->status = true; |
866 | $result->data = (object)[]; |
867 | $result->data->downloadRedirect = $response; |
868 | } else { |
869 | $this->debug('OverDriveConnector: problem getting redirect for $overDriveId.'); |
870 | $result->msg |
871 | = 'Could not get redirect link for resourceID ' |
872 | . "[$overDriveId]"; |
873 | } |
874 | } else { |
875 | $this->debug('OverDriveConnector: could not find checkout.'); |
876 | $result->msg |
877 | = 'Could not get download redirect link for resourceID ' |
878 | . "[$overDriveId]"; |
879 | } |
880 | return $result; |
881 | } |
882 | |
883 | /** |
884 | * Find the authentication header |
885 | * |
886 | * @return object |
887 | */ |
888 | public function getAuthHeader() |
889 | { |
890 | $result = $this->getResultObject(); |
891 | if (!$user = $this->getUser()) { |
892 | $this->error('user is not logged in (getauth header)', [], true); |
893 | return $result; |
894 | } |
895 | // todo: check result |
896 | $patronTokenData = $this->connectToPatronAPI( |
897 | $user['cat_username'], |
898 | $user['cat_password'], |
899 | $forceNewConnection = false |
900 | ); |
901 | $authorizationData = $patronTokenData->token_type . |
902 | ' ' . $patronTokenData->access_token; |
903 | $header = "Authorization: $authorizationData"; |
904 | $result->data->authheader = $header; |
905 | $result->status = true; |
906 | return $result; |
907 | } |
908 | |
909 | /** |
910 | * Get Configuration |
911 | * Sets up a local copy of configurations for convenience |
912 | * |
913 | * @return bool|\stdClass |
914 | */ |
915 | public function getConfig() |
916 | { |
917 | $conf = new \stdClass(); |
918 | if (!$this->recordConfig) { |
919 | $this->error( |
920 | 'Could not locate the OverDrive Record Driver ' |
921 | . 'configuration.' |
922 | ); |
923 | return false; |
924 | } |
925 | if ($this->recordConfig->API->productionMode == false) { |
926 | $conf->productionMode = false; |
927 | $conf->discURL = $this->recordConfig->API->integrationDiscoveryURL; |
928 | $conf->circURL = $this->recordConfig->API->integrationCircURL; |
929 | $conf->libraryID = $this->recordConfig->API->integrationLibraryID; |
930 | $conf->websiteID = $this->recordConfig->API->integrationWebsiteID; |
931 | } else { |
932 | $conf->productionMode = true; |
933 | $conf->discURL = $this->recordConfig->API->productionDiscoveryURL; |
934 | $conf->circURL = $this->recordConfig->API->productionCircURL; |
935 | $conf->libraryID = $this->recordConfig->API->productionLibraryID; |
936 | $conf->websiteID = $this->recordConfig->API->productionWebsiteID; |
937 | } |
938 | |
939 | $conf->clientKey = $this->recordConfig->API->clientKey; |
940 | $conf->clientSecret = $this->recordConfig->API->clientSecret; |
941 | $conf->tokenURL = $this->recordConfig->API->tokenURL; |
942 | $conf->patronTokenURL = $this->recordConfig->API->patronTokenURL; |
943 | $conf->usePatronAPI = (bool)($this->recordConfig->API->usePatronAPI ?? true); |
944 | $conf->idField = $this->recordConfig->Overdrive->overdriveIdMarcField; |
945 | $conf->idSubfield |
946 | = $this->recordConfig->Overdrive->overdriveIdMarcSubfield; |
947 | $conf->ILSname = $this->recordConfig->API->ILSname; |
948 | $conf->isMarc = $this->recordConfig->Overdrive->isMarc; |
949 | $conf->displayDateFormat = $this->mainConfig->Site->displayDateFormat; |
950 | $conf->consortiumSupport |
951 | = $this->recordConfig->Overdrive->consortiumSupport; |
952 | $conf->showMyContent |
953 | = strtolower($this->recordConfig->Overdrive->showMyContent); |
954 | $conf->noAccessString = $this->recordConfig->Overdrive->noAccessString; |
955 | $conf->tokenCacheLifetime |
956 | = $this->recordConfig->API->tokenCacheLifetime; |
957 | $conf->libraryURL = $this->recordConfig->Overdrive->overdriveLibraryURL; |
958 | $conf->enableAjaxStatus = $this->recordConfig->Overdrive->enableAjaxStatus; |
959 | $conf->showOverdriveAdminMenu = $this->recordConfig->Overdrive->showOverdriveAdminMenu; |
960 | return $conf; |
961 | } |
962 | |
963 | /** |
964 | * Returns an array of OverDrive Formats and translation tokens |
965 | * |
966 | * @return array |
967 | */ |
968 | public function getFormatNames() |
969 | { |
970 | return [ |
971 | 'ebook-kindle' => 'od_ebook-kindle', |
972 | 'ebook-overdrive' => 'od_ebook-overdrive', |
973 | 'ebook-epub-adobe' => 'od_ebook-epub-adobe', |
974 | 'ebook-epub-open' => 'od_ebook-epub-open', |
975 | 'ebook-pdf-adobe' => 'od_ebook-pdf-adobe', |
976 | 'ebook-pdf-open' => 'od_ebook-pdf-open', |
977 | 'ebook-mediado' => 'od_ebook-mediado', |
978 | 'audiobook-overdrive' => 'od_audiobook-overdrive', |
979 | 'audiobook-mp3' => 'od_audiobook-mp3', |
980 | 'video-streaming' => 'od_video-streaming', |
981 | 'magazine-overdrive' => 'od_magazine-overdrive', |
982 | ]; |
983 | } |
984 | |
985 | /** |
986 | * Returns permanant links for Ovedrive resources |
987 | * |
988 | * @param array $overDriveIds An array of overdrive IDs we need links for |
989 | * |
990 | * @return array<string> |
991 | */ |
992 | public function getPermanentLinks($overDriveIds = []) |
993 | { |
994 | $links = []; |
995 | if (!$overDriveIds || count($overDriveIds) < 1) { |
996 | $this->logWarning('OverDriveConnector: getPermanentLinks: no overdrive content IDs were passed in.'); |
997 | return []; |
998 | } |
999 | if ($conf = $this->getConfig()) { |
1000 | $libraryURL = $conf->libraryURL; |
1001 | $md = $this->getMetadata($overDriveIds); |
1002 | |
1003 | foreach ($md as $item) { |
1004 | $links[$item->id] = "$libraryURL/media/" . $item->crossRefId; |
1005 | } |
1006 | } |
1007 | return $links; |
1008 | } |
1009 | |
1010 | /** |
1011 | * Returns all the issues for an overdrive magazine title |
1012 | * |
1013 | * @param string $overDriveId OverDrive Identifier for magazine title |
1014 | * @param bool $checkouts Whether to add checkout information to each issue |
1015 | * @param int $limit maximum number of issues to retrieve (default 100) |
1016 | * @param int $offset page of results (default 0) |
1017 | * |
1018 | * @return object results of metadata fetch |
1019 | */ |
1020 | public function getMagazineIssues($overDriveId = false, $checkouts = false, $limit = 100, $offset = 0) |
1021 | { |
1022 | $result = $this->getResultObject(); |
1023 | if (!$overDriveId) { |
1024 | $this->logWarning('OverDriveConnector - getMagazineIssues: no overdrive content ID was passed in.'); |
1025 | $result->msg = 'no overdrive content ID was passed in.'; |
1026 | return $result; |
1027 | } |
1028 | if ($conf = $this->getConfig()) { |
1029 | $libraryURL = $conf->libraryURL; |
1030 | $productsKey = $this->getCollectionToken(); |
1031 | $baseUrl = $conf->discURL; |
1032 | $issuesURL = "$baseUrl/v1/collections/$productsKey/products/$overDriveId/issues"; |
1033 | $issuesURL .= "?limit=$limit&offset=$offset"; |
1034 | $response = $this->callUrl($issuesURL); |
1035 | if ($response) { |
1036 | $result->status = true; |
1037 | $result->data = (object)[]; |
1038 | $result->data = $response; |
1039 | } |
1040 | |
1041 | if ($checkouts) { |
1042 | $checkoutResult = $this->getCheckouts(); |
1043 | $checkoutData = $checkoutResult->data; |
1044 | foreach ($result->data->products as $key => $issue) { |
1045 | $issue->checkedout = isset($checkoutData[strtolower($issue->id)]); |
1046 | } |
1047 | } |
1048 | } |
1049 | return $result; |
1050 | } |
1051 | |
1052 | /** |
1053 | * Returns a hash of metadata keyed on overdrive reserveID |
1054 | * |
1055 | * @param array $overDriveIds Set of OverDrive IDs |
1056 | * |
1057 | * @return array results of metadata fetch |
1058 | * |
1059 | * @todo if more than 25 passed in, make multiple calls |
1060 | */ |
1061 | public function getMetadata($overDriveIds = []) |
1062 | { |
1063 | $metadata = []; |
1064 | if (!$overDriveIds || count($overDriveIds) < 1) { |
1065 | $this->logWarning('OverDriveConnector - getMetadata: no overdrive content IDs were passed in.'); |
1066 | return []; |
1067 | } |
1068 | if ($conf = $this->getConfig()) { |
1069 | $libraryURL = $conf->libraryURL; |
1070 | $productsKey = $this->getCollectionToken(); |
1071 | $baseUrl = $conf->discURL; |
1072 | $metadataUrl = "$baseUrl/v1/collections/$productsKey/"; |
1073 | $metadataUrl .= 'bulkmetadata?reserveIds=' . implode( |
1074 | ',', |
1075 | $overDriveIds |
1076 | ); |
1077 | $res = $this->callUrl($metadataUrl); |
1078 | $md = $res->metadata; |
1079 | foreach ($md as $item) { |
1080 | $item->external_url = "$libraryURL/media/" . $item->crossRefId; |
1081 | $metadata[$item->id] = $item; |
1082 | } |
1083 | } |
1084 | return $metadata; |
1085 | } |
1086 | |
1087 | /** |
1088 | * For array of titles passed in this will return the same array |
1089 | * with metadata attached to the records with the property name of 'metadata' |
1090 | * |
1091 | * @param array $overDriveTitles Assoc array of objects with OD IDs as keys (generally what |
1092 | * you get from getCheckouts and getHolds) |
1093 | * |
1094 | * @return array initial array with results of metadata attached as "metadata" property |
1095 | */ |
1096 | public function getMetadataForTitles($overDriveTitles = []) |
1097 | { |
1098 | if (!$overDriveTitles || count($overDriveTitles) < 1) { |
1099 | $this->logWarning('OverDriveConnector - getMetadataForTitles: no overdrive content was passed in.'); |
1100 | return []; |
1101 | } |
1102 | $metadata = $this->getMetadata(array_column($overDriveTitles, 'reserveId', 'reserveId')); |
1103 | foreach ($overDriveTitles as &$title) { |
1104 | $id = $title->reserveId; |
1105 | if (!isset($metadata[strtolower($id)])) { |
1106 | $this->logWarning('no metadata found for ' . strtolower($id)); |
1107 | } else { |
1108 | $title->metadata = $metadata[strtolower($id)]; |
1109 | } |
1110 | } |
1111 | return $overDriveTitles; |
1112 | } |
1113 | |
1114 | /** |
1115 | * Get OverDrive Checkout |
1116 | * |
1117 | * Get the overdrive checkout object for an overdrive title |
1118 | * for the current user |
1119 | * |
1120 | * @param string $overDriveId OverDrive resource id |
1121 | * @param bool $refresh Whether or not to ignore cache and get latest |
1122 | * |
1123 | * @return object|false PHP object that represents the checkout or false |
1124 | * the checkout is not in the current list of checkouts for the current |
1125 | * user. |
1126 | */ |
1127 | public function getCheckout($overDriveId, $refresh = true) |
1128 | { |
1129 | $result = $this->getCheckouts($refresh); |
1130 | if ($result->status) { |
1131 | $checkouts = $result->data; |
1132 | foreach ($checkouts as $checkout) { |
1133 | if ( |
1134 | strtolower($checkout->reserveId) == strtolower( |
1135 | $overDriveId |
1136 | ) |
1137 | ) { |
1138 | return $checkout; |
1139 | } |
1140 | } |
1141 | return false; |
1142 | } else { |
1143 | return false; |
1144 | } |
1145 | } |
1146 | |
1147 | /** |
1148 | * Get OverDrive Hold |
1149 | * |
1150 | * Get the overdrive hold object for an overdrive title |
1151 | * for the current user |
1152 | * |
1153 | * @param string $overDriveId OverDrive resource id |
1154 | * @param bool $refresh Whether or not to ignore cache and get latest |
1155 | * |
1156 | * @return object|false PHP object that represents the checkout or false |
1157 | * the checkout is not in the current list of checkouts for the current |
1158 | * user. |
1159 | */ |
1160 | public function getHold($overDriveId, $refresh = true) |
1161 | { |
1162 | $result = $this->getHolds($refresh); |
1163 | if ($result->status) { |
1164 | $holds = $result->data; |
1165 | foreach ($holds as $hold) { |
1166 | if (strtolower($hold->reserveId) == strtolower($overDriveId)) { |
1167 | return $hold; |
1168 | } |
1169 | } |
1170 | return false; |
1171 | } else { |
1172 | return false; |
1173 | } |
1174 | } |
1175 | |
1176 | /** |
1177 | * Get OverDrive Checkouts (for a user) |
1178 | * |
1179 | * @param bool $refresh Whether or not to ignore cache and get latest |
1180 | * |
1181 | * @return object Results of the call |
1182 | */ |
1183 | public function getCheckouts($refresh = true) |
1184 | { |
1185 | // The checkouts are cached in the session, but we can force a refresh |
1186 | $result = $this->getResultObject(); |
1187 | |
1188 | if (!$user = $this->getUser()) { |
1189 | $this->error('OverDriveConnector - user is not logged in (getcheckouts)'); |
1190 | return $result; |
1191 | } |
1192 | |
1193 | $checkouts = $this->getSessionContainer()->checkouts; |
1194 | if (!$checkouts || $refresh) { |
1195 | if ($config = $this->getConfig()) { |
1196 | if (!$config->usePatronAPI) { |
1197 | $this->error('Get checkouts - OverDrive patron APIs are disabled.'); |
1198 | return $result; |
1199 | } |
1200 | $url = $config->circURL . '/v1/patrons/me/checkouts'; |
1201 | |
1202 | $response = $this->callPatronUrl( |
1203 | $user['cat_username'], |
1204 | $user['cat_password'], |
1205 | $url, |
1206 | null |
1207 | ); |
1208 | if (!empty($response)) { |
1209 | $result->status = true; |
1210 | $result->message = ''; |
1211 | if (isset($response->checkouts)) { |
1212 | //reset checkouts array to be keyed by id |
1213 | $mycheckouts = []; |
1214 | foreach ($response->checkouts as $co) { |
1215 | $mycheckouts[$co->reserveId] = $co; |
1216 | } |
1217 | |
1218 | // get the metadata for these so we can check for magazines. |
1219 | $result->data = (object)[]; |
1220 | $result->data = $this->getMetadataForTitles($mycheckouts); |
1221 | |
1222 | foreach ($mycheckouts as $key => $checkout) { |
1223 | // Convert dates to desired format |
1224 | $coExpires = new \DateTime($checkout->expires); |
1225 | $result->data[$key]->expires = $coExpires->format( |
1226 | $config->displayDateFormat |
1227 | ); |
1228 | $result->data[$key]->isReturnable |
1229 | = !$checkout->isFormatLockedIn; |
1230 | } |
1231 | $this->getSessionContainer()->checkouts |
1232 | = $result->data; |
1233 | } else { |
1234 | $result->data = []; |
1235 | $this->getSessionContainer()->checkouts = false; |
1236 | } |
1237 | } else { |
1238 | $accessResult = $this->getAccess(); |
1239 | return $accessResult; |
1240 | } |
1241 | } // no config... |
1242 | } else { |
1243 | $result->status = true; |
1244 | $result->msg = []; |
1245 | $result->data = (object)[]; |
1246 | $result->data = $this->getSessionContainer()->checkouts; |
1247 | } |
1248 | return $result; |
1249 | } |
1250 | |
1251 | /** |
1252 | * Get OverDrive Holds (for a user) |
1253 | * |
1254 | * @param bool $refresh Whether or not to ignore cache and get latest |
1255 | * |
1256 | * @return \stdClass Results of the call. the data property will be set |
1257 | * to an empty array if there are no holds. |
1258 | */ |
1259 | public function getHolds($refresh = true) |
1260 | { |
1261 | $this->debug('OverDriveConnector - getHolds'); |
1262 | $result = $this->getResultObject(); |
1263 | if (!$user = $this->getUser()) { |
1264 | $this->error('OverDriveConnector - user is not logged in (getholds)'); |
1265 | return $result; |
1266 | } |
1267 | |
1268 | $holds = $this->getSessionContainer()->holds; |
1269 | if (!$holds || $refresh) { |
1270 | if ($config = $this->getConfig()) { |
1271 | if (!$config->usePatronAPI) { |
1272 | $this->error('Get holds - OverDrive patron APIs are disabled.'); |
1273 | return $result; |
1274 | } |
1275 | $url = $config->circURL . '/v1/patrons/me/holds'; |
1276 | |
1277 | $response = $this->callPatronUrl( |
1278 | $user['cat_username'], |
1279 | $user['cat_password'], |
1280 | $url |
1281 | ); |
1282 | |
1283 | $result->status = false; |
1284 | $result->message = ''; |
1285 | |
1286 | if (!empty($response)) { |
1287 | $result->status = true; |
1288 | $result->message = 'hold_place_success_html'; |
1289 | if (isset($response->holds)) { |
1290 | $result->data = $response->holds; |
1291 | // Check for holds ready for checkout |
1292 | foreach ($response->holds as $key => $hold) { |
1293 | // check for hold suspension |
1294 | $result->data[$key]->holdSuspension = $hold->holdSuspension ?? false; |
1295 | // check if ready for checkout |
1296 | foreach ($hold->actions as $action => $value) { |
1297 | if ($action == 'checkout') { |
1298 | $result->data[$key]->holdReadyForCheckout = true; |
1299 | // format the expires date. |
1300 | $holdExpires = new \DateTime($hold->holdExpires); |
1301 | $result->data[$key]->holdExpires |
1302 | = $holdExpires->format( |
1303 | (string)$config->displayDateFormat |
1304 | ); |
1305 | } else { |
1306 | $result->data[$key]->holdReadyForCheckout = false; |
1307 | } |
1308 | } |
1309 | |
1310 | $holdPlacedDate = new \DateTime($hold->holdPlacedDate); |
1311 | $result->data[$key]->holdPlacedDate |
1312 | = $holdPlacedDate->format( |
1313 | (string)$config->displayDateFormat |
1314 | ); |
1315 | } // end foreach |
1316 | $this->getSessionContainer()->holds = $response->holds; |
1317 | } else { |
1318 | // no holds found for this patron |
1319 | $result->data = []; |
1320 | $this->getSessionContainer()->holds; |
1321 | } |
1322 | } else { |
1323 | $result->code = 'od_code_connection_failed'; |
1324 | } |
1325 | } // no config |
1326 | } else { |
1327 | $result->status = true; |
1328 | $result->message = []; |
1329 | $result->data = $this->getSessionContainer()->holds; |
1330 | } |
1331 | return $result; |
1332 | } |
1333 | |
1334 | /** |
1335 | * Call a URL on the API |
1336 | * |
1337 | * @param string $url The url to call |
1338 | * @param array $headers Headers to set for the request. |
1339 | * if null, then the auth headers are used. |
1340 | * @param bool $checkToken Whether to check and get a new token |
1341 | * @param string $requestType The request type (GET, POST etc) |
1342 | * |
1343 | * @return object|bool The json response from the API call |
1344 | * converted to an object. If the call fails at the |
1345 | * HTTP level then the error is logged and false is returned. |
1346 | */ |
1347 | protected function callUrl( |
1348 | $url, |
1349 | $headers = null, |
1350 | $checkToken = true, |
1351 | $requestType = 'GET' |
1352 | ) { |
1353 | if (!$checkToken || $this->connectToAPI()) { |
1354 | $tokenData = $this->getSessionContainer()->tokenData; |
1355 | try { |
1356 | $client = $this->getHttpClient($url); |
1357 | } catch (Exception $e) { |
1358 | $this->error( |
1359 | 'OverDriveConnector - error while setting up the client: ' . $e->getMessage() |
1360 | ); |
1361 | return false; |
1362 | } |
1363 | if ($headers === null) { |
1364 | $headers = []; |
1365 | if ( |
1366 | isset($tokenData->token_type) |
1367 | && isset($tokenData->access_token) |
1368 | ) { |
1369 | $headers[] = "Authorization: {$tokenData->token_type} " |
1370 | . $tokenData->access_token; |
1371 | } |
1372 | $headers[] = 'User-Agent: VuFind'; |
1373 | } |
1374 | $client->setHeaders($headers); |
1375 | $client->setMethod($requestType); |
1376 | $client->setUri($url); |
1377 | try { |
1378 | // throw new Exception('testException'); |
1379 | $response = $client->send(); |
1380 | } catch (Exception $ex) { |
1381 | $this->error( |
1382 | 'Exception during request: ' . |
1383 | $ex->getMessage() |
1384 | ); |
1385 | return false; |
1386 | } |
1387 | |
1388 | if ($response->isServerError()) { |
1389 | $this->error( |
1390 | 'OverDrive HTTP Error: ' . |
1391 | $response->getStatusCode() |
1392 | ); |
1393 | $this->debug('Request: ' . $client->getRequest()); |
1394 | $this->debug('Response: ' . $client->getResponse()); |
1395 | return false; |
1396 | } |
1397 | |
1398 | $body = $response->getBody(); |
1399 | $returnVal = json_decode($body); |
1400 | if ($returnVal != null) { |
1401 | if (isset($returnVal->errorCode)) { |
1402 | // In some cases, this should be returned perhaps... |
1403 | $this->error('OverDrive Error: ' . $returnVal->errorCode); |
1404 | return $returnVal; |
1405 | } else { |
1406 | return $returnVal; |
1407 | } |
1408 | } else { |
1409 | $this->error( |
1410 | 'OverDrive Error: Nothing returned from API call.' |
1411 | ); |
1412 | $this->debug( |
1413 | 'Body return from OD API Call: ' . print_r($body, true) |
1414 | ); |
1415 | } |
1416 | } |
1417 | return false; |
1418 | } |
1419 | |
1420 | /** |
1421 | * Connect to API |
1422 | * |
1423 | * @param bool $forceNewConnection Force a new connection (get a new token) |
1424 | * |
1425 | * @return string token for the session or false |
1426 | * if the token request failed |
1427 | */ |
1428 | protected function connectToAPI($forceNewConnection = false) |
1429 | { |
1430 | $conf = $this->getConfig(); |
1431 | $tokenData = $this->getSessionContainer()->tokenData; |
1432 | if ( |
1433 | $forceNewConnection || $tokenData == null |
1434 | || !isset($tokenData->access_token) |
1435 | || time() >= $tokenData->expirationTime |
1436 | ) { |
1437 | $authHeader = base64_encode( |
1438 | $conf->clientKey . ':' . $conf->clientSecret |
1439 | ); |
1440 | $headers = [ |
1441 | 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8', |
1442 | "Authorization: Basic $authHeader", |
1443 | ]; |
1444 | |
1445 | try { |
1446 | $client = $this->getHttpClient(); |
1447 | } catch (Exception $e) { |
1448 | $this->error( |
1449 | 'error while setting up the client: ' . $e->getMessage() |
1450 | ); |
1451 | return false; |
1452 | } |
1453 | $client->setHeaders($headers); |
1454 | $client->setMethod('POST'); |
1455 | $client->setRawBody('grant_type=client_credentials'); |
1456 | $response = $client->setUri($conf->tokenURL)->send(); |
1457 | |
1458 | if ($response->isServerError()) { |
1459 | $this->error( |
1460 | 'OverDrive HTTP Error: ' . |
1461 | $response->getStatusCode() |
1462 | ); |
1463 | $this->debug('Request: ' . $client->getRequest()); |
1464 | return false; |
1465 | } |
1466 | |
1467 | $body = $response->getBody(); |
1468 | $tokenData = json_decode($body); |
1469 | if ($tokenData != null) { |
1470 | if (isset($tokenData->errorCode)) { |
1471 | // In some cases, this should be returned perhaps... |
1472 | $this->error('OverDrive Error: ' . $tokenData->errorCode); |
1473 | return false; |
1474 | } else { |
1475 | $tokenData->expirationTime = time() |
1476 | + ($tokenData->expires_in ?? 0); |
1477 | $this->getSessionContainer()->tokenData = $tokenData; |
1478 | return $tokenData; |
1479 | } |
1480 | } else { |
1481 | $this->error( |
1482 | 'OverDrive Error: Nothing returned from API call.' |
1483 | ); |
1484 | $this->debug( |
1485 | 'Body return from OD API Call: ' . print_r($body, true) |
1486 | ); |
1487 | } |
1488 | } |
1489 | return $tokenData; |
1490 | } |
1491 | |
1492 | /** |
1493 | * Call a Patron URL on the API |
1494 | * |
1495 | * The patron URL is used for the circulation API's and requires a patron |
1496 | * specific token. |
1497 | * |
1498 | * @param string $patronBarcode Patrons barcode |
1499 | * @param string $patronPin Patrons password |
1500 | * @param string $url The url to call |
1501 | * @param array $params parameters to call |
1502 | * @param string $requestType HTTP request type (default=GET) |
1503 | * @param string $returnType options are json(def),body,redirect |
1504 | * |
1505 | * @return object|bool The json response from the API call |
1506 | * converted to an object. If body is specified, the raw body is returned. |
1507 | * If redirect, then it returns the URL specified in the redirect header. |
1508 | * If the call fails at the HTTP level then the error is logged and false is returned. |
1509 | */ |
1510 | protected function callPatronUrl( |
1511 | $patronBarcode, |
1512 | $patronPin, |
1513 | $url, |
1514 | $params = null, |
1515 | $requestType = 'GET', |
1516 | $returnType = 'json' |
1517 | ) { |
1518 | if ($this->connectToPatronAPI($patronBarcode, $patronPin, false)) { |
1519 | $patronTokenData = $this->getSessionContainer()->patronTokenData; |
1520 | $authorizationData = $patronTokenData->token_type . |
1521 | ' ' . $patronTokenData->access_token; |
1522 | $headers = [ |
1523 | "Authorization: $authorizationData", |
1524 | 'User-Agent: VuFind', |
1525 | 'Content-Type: application/json', |
1526 | ]; |
1527 | try { |
1528 | $client = $this->getHttpClient(null, $returnType != 'redirect'); |
1529 | } catch (Exception $e) { |
1530 | $this->error( |
1531 | 'error while setting up the client: ' . $e->getMessage() |
1532 | ); |
1533 | return false; |
1534 | } |
1535 | $client->setHeaders($headers); |
1536 | $client->setMethod($requestType); |
1537 | $this->debug('callPatronURL method: ' . $client->getMethod() . " url: $url"); |
1538 | $client->setUri($url); |
1539 | if ($params != null) { |
1540 | $jsonData = ['fields' => []]; |
1541 | foreach ($params as $key => $value) { |
1542 | $jsonData['fields'][] = [ |
1543 | 'name' => $key, |
1544 | 'value' => $value, |
1545 | ]; |
1546 | } |
1547 | $postData = json_encode($jsonData); |
1548 | $client->setRawBody($postData); |
1549 | $this->debug("patron data sent: $postData"); |
1550 | } |
1551 | |
1552 | try { |
1553 | $response = $client->send(); |
1554 | } catch (Exception $ex) { |
1555 | $this->error( |
1556 | 'Exception during request: ' . |
1557 | $ex->getMessage() |
1558 | ); |
1559 | return false; |
1560 | } |
1561 | $body = $response->getBody(); |
1562 | |
1563 | // if all goes well for DELETE, the code will be 204 |
1564 | // and response is empty. |
1565 | if ($requestType == 'DELETE' || $requestType == 'PUT') { |
1566 | if ($response->getStatusCode() == 204) { |
1567 | return true; |
1568 | } else { |
1569 | $this->error( |
1570 | $requestType . ' Patron call failed. HTTP return code: ' . |
1571 | $response->getStatusCode() . " body: $body" |
1572 | ); |
1573 | return false; |
1574 | } |
1575 | } |
1576 | |
1577 | if ($returnType == 'body') { |
1578 | // probably need to check for return status |
1579 | return $body; |
1580 | } elseif ($returnType == 'redirect') { |
1581 | $headers = $response->getHeaders(); |
1582 | if ($headers->has('location')) { |
1583 | $loc = $headers->get('location'); |
1584 | $uri = $loc->getUri(); |
1585 | return $uri; |
1586 | } else { |
1587 | $this->error( |
1588 | 'OverDrive Error: returnType is redirect but no redirect found.' |
1589 | ); |
1590 | return false; |
1591 | } |
1592 | } else { |
1593 | $returnVal = json_decode($body); |
1594 | if ($returnVal != null) { |
1595 | if ( |
1596 | !isset($returnVal->message) |
1597 | || $returnVal->message != 'An unexpected error has occurred.' |
1598 | ) { |
1599 | return $returnVal; |
1600 | } else { |
1601 | $this->debug( |
1602 | 'OverDrive API problem: ' . $returnVal->message |
1603 | ); |
1604 | } |
1605 | } else { |
1606 | $this->error( |
1607 | 'OverDrive Error: Nothing returned from API call.' |
1608 | ); |
1609 | return false; |
1610 | } |
1611 | } |
1612 | } else { |
1613 | $this->error('OverDrive Error: Not connected to the Patron API.'); |
1614 | } |
1615 | return false; |
1616 | } |
1617 | |
1618 | /** |
1619 | * Connect to Patron API |
1620 | * |
1621 | * @param string $patronBarcode Patrons barcode |
1622 | * @param string $patronPin Patrons password |
1623 | * @param bool $forceNewConnection force a new connection (get a new |
1624 | * token) |
1625 | * |
1626 | * @return object|bool token for the session |
1627 | */ |
1628 | protected function connectToPatronAPI( |
1629 | $patronBarcode, |
1630 | $patronPin = '1234', |
1631 | $forceNewConnection = false |
1632 | ) { |
1633 | $patronTokenData = $this->getSessionContainer()->patronTokenData; |
1634 | $config = $this->getConfig(); |
1635 | if (!$config->usePatronAPI) { |
1636 | return false; |
1637 | } |
1638 | if ( |
1639 | $forceNewConnection |
1640 | || $patronTokenData == null |
1641 | || (isset($patronTokenData->expirationTime) |
1642 | and time() >= $patronTokenData->expirationTime) |
1643 | ) { |
1644 | $url = $config->patronTokenURL; |
1645 | $websiteId = $config->websiteID; |
1646 | $ilsname = $config->ILSname; |
1647 | $authHeader = base64_encode( |
1648 | $config->clientKey . ':' . $config->clientSecret |
1649 | ); |
1650 | $headers = [ |
1651 | 'Content-Type: application/x-www-form-urlencoded;charset=UTF-8', |
1652 | "Authorization: Basic $authHeader", |
1653 | 'User-Agent: VuFind', |
1654 | ]; |
1655 | try { |
1656 | $client = $this->getHttpClient($url); |
1657 | } catch (Exception $e) { |
1658 | $this->error( |
1659 | 'error while setting up the client: ' . $e->getMessage() |
1660 | ); |
1661 | return false; |
1662 | } |
1663 | $client->setHeaders($headers); |
1664 | $client->setMethod('POST'); |
1665 | if ($patronPin == null) { |
1666 | $postFields = "grant_type=password&username={$patronBarcode}"; |
1667 | $postFields .= '&password=ignore&password_required=false'; |
1668 | $postFields .= "&scope=websiteId:{$websiteId}%20"; |
1669 | $postFields .= "authorizationname:{$ilsname}"; |
1670 | } else { |
1671 | $postFields = "grant_type=password&username={$patronBarcode}"; |
1672 | $postFields .= "&password={$patronPin}&scope=websiteId"; |
1673 | $postFields .= ":{$websiteId}%20authorizationname:{$ilsname}"; |
1674 | } |
1675 | $client->setRawBody($postFields); |
1676 | $response = $client->setUri($url)->send(); |
1677 | $body = $response->getBody(); |
1678 | $patronTokenData = json_decode($body); |
1679 | |
1680 | if (isset($patronTokenData->expires_in)) { |
1681 | $patronTokenData->expirationTime = time() |
1682 | + $patronTokenData->expires_in; |
1683 | } else { |
1684 | $this->debug( |
1685 | 'problem with OD patron API token Call: ' . |
1686 | print_r( |
1687 | $patronTokenData, |
1688 | true |
1689 | ) |
1690 | ); |
1691 | // If we have an unauthorized error, then we are going |
1692 | // to cache that in the session so we don't keep making |
1693 | // unnecessary calls; otherwise, just don't store the tokenData |
1694 | // object so that it gets checked again next time |
1695 | if ($patronTokenData->error == 'unauthorized_client') { |
1696 | $this->getSessionContainer()->odAccessMessage |
1697 | = $patronTokenData->error_description; |
1698 | $this->getSessionContainer()->patronTokenData |
1699 | = $patronTokenData; |
1700 | } else { |
1701 | $patronTokenData = null; |
1702 | } |
1703 | return false; |
1704 | } |
1705 | $this->getSessionContainer()->patronTokenData = $patronTokenData; |
1706 | } |
1707 | if (isset($patronTokenData->error)) { |
1708 | return false; |
1709 | } |
1710 | return $patronTokenData; |
1711 | } |
1712 | |
1713 | /** |
1714 | * Get an HTTP client |
1715 | * |
1716 | * @param string $url URL for client to use |
1717 | * @param bool $allowRedirects Whether to allow the client to follow redirects |
1718 | * |
1719 | * @return \Laminas\Http\Client |
1720 | * @throws \Exception |
1721 | */ |
1722 | protected function getHttpClient($url = null, $allowRedirects = true) |
1723 | { |
1724 | if (null === $this->httpService) { |
1725 | throw new Exception('HTTP service missing.'); |
1726 | } |
1727 | if (!$this->client) { |
1728 | $this->client = $this->httpService->createClient($url); |
1729 | // Set keep alive to true since we are sending to the same server |
1730 | $this->client->setOptions(['keepalive', true]); |
1731 | } |
1732 | $this->client->resetParameters(); |
1733 | // set keep alive to true since we are sending to the same server |
1734 | $options = ['keepalive' => true]; |
1735 | if (!$allowRedirects) { |
1736 | $options['maxredirects'] = 0; |
1737 | } |
1738 | $this->client->setOptions($options); |
1739 | return $this->client; |
1740 | } |
1741 | |
1742 | /** |
1743 | * Set a cache storage object. |
1744 | * |
1745 | * @param StorageInterface $cache Cache storage interface |
1746 | * |
1747 | * @return void |
1748 | */ |
1749 | public function setCacheStorage(StorageInterface $cache = null) |
1750 | { |
1751 | $this->cache = $cache; |
1752 | } |
1753 | |
1754 | /** |
1755 | * Helper function for fetching cached data. |
1756 | * Data is cached for up to $this->cacheLifetime seconds so that it would |
1757 | * be |
1758 | * faster to process e.g. requests where multiple calls to the backend are |
1759 | * made. |
1760 | * |
1761 | * @param string $key Cache entry key |
1762 | * |
1763 | * @return mixed|null Cached entry or null if not cached or expired |
1764 | */ |
1765 | protected function getCachedData($key) |
1766 | { |
1767 | // No cache object, no cached results! |
1768 | if (null === $this->cache) { |
1769 | return null; |
1770 | } |
1771 | $conf = $this->getConfig(); |
1772 | $fullKey = $this->getCacheKey($key); |
1773 | $item = $this->cache->getItem($fullKey); |
1774 | if (null !== $item) { |
1775 | // Return value if still valid: |
1776 | if (time() - $item['time'] < $conf->tokenCacheLifetime) { |
1777 | return $item['entry']; |
1778 | } |
1779 | |
1780 | // Clear expired item from cache: |
1781 | $this->cache->removeItem($fullKey); |
1782 | } |
1783 | return null; |
1784 | } |
1785 | |
1786 | /** |
1787 | * Helper function for storing cached data. |
1788 | * Data is cached for up to $this->cacheLifetime seconds so that it would |
1789 | * be |
1790 | * faster to process e.g. requests where multiple calls to the backend are |
1791 | * made. |
1792 | * |
1793 | * @param string $key Cache entry key |
1794 | * @param mixed $entry Entry to be cached |
1795 | * |
1796 | * @return void |
1797 | */ |
1798 | protected function putCachedData($key, $entry) |
1799 | { |
1800 | // Don't write to cache if we don't have a cache! |
1801 | if (null === $this->cache) { |
1802 | return; |
1803 | } |
1804 | $item = [ |
1805 | 'time' => time(), |
1806 | 'entry' => $entry, |
1807 | ]; |
1808 | $this->cache->setItem($this->getCacheKey($key), $item); |
1809 | } |
1810 | |
1811 | /** |
1812 | * Helper function for removing cached data. |
1813 | * |
1814 | * @param string $key Cache entry key |
1815 | * |
1816 | * @return void |
1817 | */ |
1818 | protected function removeCachedData($key) |
1819 | { |
1820 | // Don't write to cache if we don't have a cache! |
1821 | if (null === $this->cache) { |
1822 | return; |
1823 | } |
1824 | $this->cache->removeItem($this->getCacheKey($key)); |
1825 | } |
1826 | |
1827 | /** |
1828 | * Get Result Object |
1829 | * |
1830 | * @param bool $status Whether it succeeded |
1831 | * @param string $msg More information |
1832 | * @param string $code code used for end user display/translation |
1833 | * |
1834 | * @return object |
1835 | */ |
1836 | public function getResultObject($status = false, $msg = '', $code = '') |
1837 | { |
1838 | return (object)[ |
1839 | 'status' => $status, |
1840 | 'msg' => $msg, |
1841 | 'data' => false, |
1842 | 'code' => $code, |
1843 | ]; |
1844 | } |
1845 | } |