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