Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
9.73% |
77 / 791 |
|
7.69% |
3 / 39 |
CRAP | |
0.00% |
0 / 1 |
Aleph | |
9.73% |
77 / 791 |
|
7.69% |
3 / 39 |
22957.93 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
init | |
5.45% |
3 / 55 |
|
0.00% |
0 / 1 |
393.70 | |||
getDefaultAddressMappings | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
2 | |||
doXRequest | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
30 | |||
doRestDLFRequest | |
38.46% |
10 / 26 |
|
0.00% |
0 / 1 |
18.42 | |||
appendQueryString | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
doHTTPRequest | |
54.17% |
13 / 24 |
|
0.00% |
0 / 1 |
14.16 | |||
parseId | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
getStatus | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getStatusesX | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
42 | |||
getStatuses | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
getHolding | |
0.00% |
0 / 88 |
|
0.00% |
0 / 1 |
240 | |||
getMyTransactionHistory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMyTransactions | |
0.00% |
0 / 65 |
|
0.00% |
0 / 1 |
90 | |||
getRenewDetails | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renewMyItems | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
12 | |||
getMyHolds | |
0.00% |
0 / 52 |
|
0.00% |
0 / 1 |
20 | |||
getCancelHoldDetails | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
cancelHolds | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
12 | |||
getMyFines | |
100.00% |
37 / 37 |
|
100.00% |
1 / 1 |
3 | |||
getMyProfile | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getMyProfileX | |
0.00% |
0 / 42 |
|
0.00% |
0 / 1 |
30 | |||
getMyProfileDLF | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
20 | |||
patronLogin | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
72 | |||
getHoldingInfoForItem | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
20 | |||
getHoldDefaultRequiredDate | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
placeHold | |
0.00% |
0 / 50 |
|
0.00% |
0 / 1 |
42 | |||
barcodeToID | |
8.70% |
2 / 23 |
|
0.00% |
0 / 1 |
33.40 | |||
parseDate | |
27.27% |
3 / 11 |
|
0.00% |
0 / 1 |
25.85 | |||
supportsMethod | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getConfig | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
getPickUpLocations | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
getDefaultPickUpLocation | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
getPurchaseHistory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getNewItems | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDepartments | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getInstructors | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getCourses | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
findReserves | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * Aleph ILS driver |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) UB/FU Berlin |
9 | * |
10 | * last update: 7.11.2007 |
11 | * tested with X-Server Aleph 18.1. |
12 | * |
13 | * TODO: login, course information, getNewItems, duedate in holdings, |
14 | * https connection to x-server, ... |
15 | * |
16 | * This program is free software; you can redistribute it and/or modify |
17 | * it under the terms of the GNU General Public License version 2, |
18 | * as published by the Free Software Foundation. |
19 | * |
20 | * This program is distributed in the hope that it will be useful, |
21 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
22 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
23 | * GNU General Public License for more details. |
24 | * |
25 | * You should have received a copy of the GNU General Public License |
26 | * along with this program; if not, write to the Free Software |
27 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
28 | * |
29 | * @category VuFind |
30 | * @package ILS_Drivers |
31 | * @author Christoph Krempe <vufind-tech@lists.sourceforge.net> |
32 | * @author Alan Rykhus <vufind-tech@lists.sourceforge.net> |
33 | * @author Jason L. Cooper <vufind-tech@lists.sourceforge.net> |
34 | * @author Kun Lin <vufind-tech@lists.sourceforge.net> |
35 | * @author Vaclav Rosecky <vufind-tech@lists.sourceforge.net> |
36 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
37 | * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki |
38 | */ |
39 | |
40 | namespace VuFind\ILS\Driver; |
41 | |
42 | use Laminas\I18n\Translator\TranslatorInterface; |
43 | use VuFind\Date\DateException; |
44 | use VuFind\Exception\ILS as ILSException; |
45 | |
46 | use function array_key_exists; |
47 | use function count; |
48 | use function in_array; |
49 | use function is_callable; |
50 | use function strlen; |
51 | |
52 | /** |
53 | * Aleph ILS driver |
54 | * |
55 | * @category VuFind |
56 | * @package ILS_Drivers |
57 | * @author Christoph Krempe <vufind-tech@lists.sourceforge.net> |
58 | * @author Alan Rykhus <vufind-tech@lists.sourceforge.net> |
59 | * @author Jason L. Cooper <vufind-tech@lists.sourceforge.net> |
60 | * @author Kun Lin <vufind-tech@lists.sourceforge.net> |
61 | * @author Vaclav Rosecky <vufind-tech@lists.sourceforge.net> |
62 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
63 | * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki |
64 | */ |
65 | class Aleph extends AbstractBase implements |
66 | \Laminas\Log\LoggerAwareInterface, |
67 | \VuFindHttp\HttpServiceAwareInterface |
68 | { |
69 | use \VuFind\Log\LoggerAwareTrait; |
70 | use \VuFindHttp\HttpServiceAwareTrait; |
71 | |
72 | public const RECORD_ID_BASE_SEPARATOR = '-'; |
73 | |
74 | /** |
75 | * Translator object |
76 | * |
77 | * @var Aleph\Translator |
78 | */ |
79 | protected $alephTranslator = false; |
80 | |
81 | /** |
82 | * Cache manager |
83 | * |
84 | * @var \VuFind\Cache\Manager |
85 | */ |
86 | protected $cacheManager; |
87 | |
88 | /** |
89 | * Translator |
90 | * |
91 | * @var TranslatorInterface |
92 | */ |
93 | protected $translator; |
94 | |
95 | /** |
96 | * Date converter object |
97 | * |
98 | * @var \VuFind\Date\Converter |
99 | */ |
100 | protected $dateConverter = null; |
101 | |
102 | /** |
103 | * The base URL, where the REST DLF API is running |
104 | * |
105 | * @var string |
106 | */ |
107 | protected $dlfbaseurl = null; |
108 | |
109 | /** |
110 | * Aleph server |
111 | * |
112 | * @var string |
113 | */ |
114 | protected $host; |
115 | |
116 | /** |
117 | * Bibliographic bases |
118 | * |
119 | * @var array |
120 | */ |
121 | protected $bib; |
122 | |
123 | /** |
124 | * User library |
125 | * |
126 | * @var string |
127 | */ |
128 | protected $useradm; |
129 | |
130 | /** |
131 | * Item library |
132 | * |
133 | * @var string |
134 | */ |
135 | protected $admlib; |
136 | |
137 | /** |
138 | * X server user name |
139 | * |
140 | * @var string |
141 | */ |
142 | protected $wwwuser; |
143 | |
144 | /** |
145 | * X server user password |
146 | * |
147 | * @var string |
148 | */ |
149 | protected $wwwpasswd; |
150 | |
151 | /** |
152 | * Is X server enabled? |
153 | * |
154 | * @var bool |
155 | */ |
156 | protected $xserver_enabled; |
157 | |
158 | /** |
159 | * X server port (defaults to 80) |
160 | * |
161 | * @var int |
162 | */ |
163 | protected $xport; |
164 | |
165 | /** |
166 | * DLF REST API port |
167 | * |
168 | * @var int |
169 | */ |
170 | protected $dlfport; |
171 | |
172 | /** |
173 | * Statuses considered as available |
174 | * |
175 | * @var array |
176 | */ |
177 | protected $available_statuses; |
178 | |
179 | /** |
180 | * List of patron hoe libraries |
181 | * |
182 | * @var array |
183 | */ |
184 | protected $sublibadm; |
185 | |
186 | /** |
187 | * If enabled and Xserver is disabled, slower RESTful API is used for |
188 | * availability check. |
189 | * |
190 | * @var bool |
191 | */ |
192 | protected $quick_availability; |
193 | |
194 | /** |
195 | * Is debug mode enabled? |
196 | * |
197 | * @var bool |
198 | */ |
199 | protected $debug_enabled; |
200 | |
201 | /** |
202 | * Preferred pickup locations |
203 | * |
204 | * @var array |
205 | */ |
206 | protected $preferredPickUpLocations; |
207 | |
208 | /** |
209 | * Patron id used when no specific patron defined |
210 | * |
211 | * @var string |
212 | */ |
213 | protected $defaultPatronId; |
214 | |
215 | /** |
216 | * Mapping of z304 address elements in Aleph to getMyProfile attributes |
217 | * |
218 | * @var array |
219 | */ |
220 | protected $addressMappings = null; |
221 | |
222 | /** |
223 | * ISO 3166-1 alpha-2 to ISO 3166-1 alpha-3 mapping for |
224 | * translation in REST DLF API. |
225 | * |
226 | * @var array |
227 | */ |
228 | protected $languages = []; |
229 | |
230 | /** |
231 | * Regex for extracting position in queue from status in holdings. |
232 | * |
233 | * @var string |
234 | */ |
235 | protected $queuePositionRegex = '/Waiting in position ' |
236 | . '(?<position>[0-9]+) in queue;/'; |
237 | |
238 | /** |
239 | * Constructor |
240 | * |
241 | * @param \VuFind\Date\Converter $dateConverter Date converter |
242 | * @param \VuFind\Cache\Manager $cacheManager Cache manager (optional) |
243 | * @param TranslatorInterface $translator Translator (optional) |
244 | */ |
245 | public function __construct( |
246 | \VuFind\Date\Converter $dateConverter, |
247 | \VuFind\Cache\Manager $cacheManager = null, |
248 | TranslatorInterface $translator = null |
249 | ) { |
250 | $this->dateConverter = $dateConverter; |
251 | $this->cacheManager = $cacheManager; |
252 | $this->translator = $translator; |
253 | } |
254 | |
255 | /** |
256 | * Initialize the driver. |
257 | * |
258 | * Validate configuration and perform all resource-intensive tasks needed to |
259 | * make the driver active. |
260 | * |
261 | * @throws ILSException |
262 | * @return void |
263 | */ |
264 | public function init() |
265 | { |
266 | // Validate config |
267 | $required = [ |
268 | 'host', 'bib', 'useradm', 'admlib', 'dlfport', 'available_statuses', |
269 | ]; |
270 | foreach ($required as $current) { |
271 | if (!isset($this->config['Catalog'][$current])) { |
272 | throw new ILSException("Missing Catalog/{$current} config setting."); |
273 | } |
274 | } |
275 | if (!isset($this->config['sublibadm'])) { |
276 | throw new ILSException('Missing sublibadm config setting.'); |
277 | } |
278 | |
279 | // Process config |
280 | $this->host = $this->config['Catalog']['host']; |
281 | $this->bib = explode(',', $this->config['Catalog']['bib']); |
282 | $this->useradm = $this->config['Catalog']['useradm']; |
283 | $this->admlib = $this->config['Catalog']['admlib']; |
284 | if ( |
285 | isset($this->config['Catalog']['wwwuser']) |
286 | && isset($this->config['Catalog']['wwwpasswd']) |
287 | ) { |
288 | $this->wwwuser = $this->config['Catalog']['wwwuser']; |
289 | $this->wwwpasswd = $this->config['Catalog']['wwwpasswd']; |
290 | $this->xserver_enabled = true; |
291 | $this->xport = $this->config['Catalog']['xport'] ?? 80; |
292 | } else { |
293 | $this->xserver_enabled = false; |
294 | } |
295 | $this->dlfport = $this->config['Catalog']['dlfport']; |
296 | if (isset($this->config['Catalog']['dlfbaseurl'])) { |
297 | $this->dlfbaseurl = $this->config['Catalog']['dlfbaseurl']; |
298 | } |
299 | $this->sublibadm = $this->config['sublibadm']; |
300 | $this->available_statuses |
301 | = explode(',', $this->config['Catalog']['available_statuses']); |
302 | $this->quick_availability |
303 | = $this->config['Catalog']['quick_availability'] ?? false; |
304 | $this->debug_enabled = $this->config['Catalog']['debug'] ?? false; |
305 | if ( |
306 | isset($this->config['util']['tab40']) |
307 | && isset($this->config['util']['tab15']) |
308 | && isset($this->config['util']['tab_sub_library']) |
309 | ) { |
310 | $cache = null; |
311 | if ( |
312 | isset($this->config['Cache']['type']) |
313 | && null !== $this->cacheManager |
314 | ) { |
315 | $cache = $this->cacheManager |
316 | ->getCache($this->config['Cache']['type']); |
317 | $this->alephTranslator = $cache->getItem('alephTranslator'); |
318 | } |
319 | if ($this->alephTranslator == false) { |
320 | $this->alephTranslator = new Aleph\Translator($this->config); |
321 | if (isset($cache)) { |
322 | $cache->setItem('alephTranslator', $this->alephTranslator); |
323 | } |
324 | } |
325 | } |
326 | if (isset($this->config['Catalog']['preferred_pick_up_locations'])) { |
327 | $this->preferredPickUpLocations = explode( |
328 | ',', |
329 | $this->config['Catalog']['preferred_pick_up_locations'] |
330 | ); |
331 | } |
332 | if (isset($this->config['Catalog']['default_patron_id'])) { |
333 | $this->defaultPatronId = $this->config['Catalog']['default_patron_id']; |
334 | } |
335 | |
336 | $this->addressMappings = $this->getDefaultAddressMappings(); |
337 | |
338 | if (isset($this->config['AddressMappings'])) { |
339 | foreach ($this->config['AddressMappings'] as $key => $val) { |
340 | $this->addressMappings[$key] = $val; |
341 | } |
342 | } |
343 | |
344 | if (isset($this->config['Catalog']['queue_position_regex'])) { |
345 | $this->queuePositionRegex |
346 | = $this->config['Catalog']['queue_position_regex']; |
347 | } |
348 | |
349 | if (isset($this->config['Languages'])) { |
350 | foreach ($this->config['Languages'] as $locale => $lang) { |
351 | $this->languages[$locale] = $lang; |
352 | } |
353 | } |
354 | } |
355 | |
356 | /** |
357 | * Return default mapping of z304 address elements in Aleph |
358 | * to getMyProfile attributes. |
359 | * |
360 | * @return array |
361 | */ |
362 | protected function getDefaultAddressMappings() |
363 | { |
364 | return [ |
365 | 'fullname' => 'z304-address-1', |
366 | 'address1' => 'z304-address-2', |
367 | 'address2' => 'z304-address-3', |
368 | 'city' => 'z304-address-4', |
369 | 'zip' => 'z304-zip', |
370 | 'email' => 'z304-email-address', |
371 | 'phone' => 'z304-telephone-1', |
372 | ]; |
373 | } |
374 | |
375 | /** |
376 | * Perform an XServer request. |
377 | * |
378 | * @param string $op Operation |
379 | * @param array $params Parameters |
380 | * @param bool $auth Include authentication? |
381 | * |
382 | * @return \SimpleXMLElement |
383 | */ |
384 | protected function doXRequest($op, $params, $auth = false) |
385 | { |
386 | if (!$this->xserver_enabled) { |
387 | throw new \Exception( |
388 | 'Call to doXRequest without X-Server configuration in Aleph.ini' |
389 | ); |
390 | } |
391 | $url = "http://$this->host:$this->xport/X?op=$op"; |
392 | $url = $this->appendQueryString($url, $params); |
393 | if ($auth) { |
394 | $url = $this->appendQueryString( |
395 | $url, |
396 | [ |
397 | 'user_name' => $this->wwwuser, |
398 | 'user_password' => $this->wwwpasswd, |
399 | ] |
400 | ); |
401 | } |
402 | $result = $this->doHTTPRequest($url); |
403 | if ($result->error) { |
404 | if ($this->debug_enabled) { |
405 | $this->debug( |
406 | "XServer error, URL is $url, error message: $result->error." |
407 | ); |
408 | } |
409 | throw new ILSException("XServer error: $result->error."); |
410 | } |
411 | return $result; |
412 | } |
413 | |
414 | /** |
415 | * Perform a RESTful DLF request. |
416 | * |
417 | * @param array $path_elements URL path elements |
418 | * @param array $params GET parameters (null for none) |
419 | * @param string $method HTTP method |
420 | * @param string $body HTTP body |
421 | * |
422 | * @return \SimpleXMLElement |
423 | */ |
424 | protected function doRestDLFRequest( |
425 | $path_elements, |
426 | $params = null, |
427 | $method = 'GET', |
428 | $body = null |
429 | ) { |
430 | $path = implode('/', $path_elements); |
431 | if ($this->dlfbaseurl === null) { |
432 | $url = "http://$this->host:$this->dlfport/rest-dlf/" . $path; |
433 | } else { |
434 | $url = $this->dlfbaseurl . $path; |
435 | } |
436 | if ($params == null) { |
437 | $params = []; |
438 | } |
439 | if (!empty($this->languages) && $this->translator != null) { |
440 | $locale = $this->translator->getLocale(); |
441 | if (isset($this->languages[$locale])) { |
442 | $params['lang'] = $this->languages[$locale]; |
443 | } |
444 | } |
445 | $url = $this->appendQueryString($url, $params); |
446 | $result = $this->doHTTPRequest($url, $method, $body); |
447 | $replyCode = (string)$result->{'reply-code'}; |
448 | if ($replyCode != '0000') { |
449 | $replyText = (string)$result->{'reply-text'}; |
450 | $this->logError( |
451 | 'DLF request failed', |
452 | [ |
453 | 'url' => $url, 'reply-code' => $replyCode, |
454 | 'reply-message' => $replyText, |
455 | ] |
456 | ); |
457 | $ex = new Aleph\RestfulException($replyText, $replyCode); |
458 | $ex->setXmlResponse($result); |
459 | throw $ex; |
460 | } |
461 | return $result; |
462 | } |
463 | |
464 | /** |
465 | * Add values to an HTTP query string. |
466 | * |
467 | * @param string $url URL so far |
468 | * @param array $params Parameters to add |
469 | * |
470 | * @return string |
471 | */ |
472 | protected function appendQueryString($url, $params) |
473 | { |
474 | $sep = (!str_contains($url, '?')) ? '?' : '&'; |
475 | if ($params != null) { |
476 | foreach ($params as $key => $value) { |
477 | $url .= $sep . $key . '=' . urlencode($value); |
478 | $sep = '&'; |
479 | } |
480 | } |
481 | return $url; |
482 | } |
483 | |
484 | /** |
485 | * Perform an HTTP request. |
486 | * |
487 | * @param string $url URL of request |
488 | * @param string $method HTTP method |
489 | * @param string $body HTTP body (null for none) |
490 | * |
491 | * @return \SimpleXMLElement |
492 | */ |
493 | protected function doHTTPRequest($url, $method = 'GET', $body = null) |
494 | { |
495 | if ($this->debug_enabled) { |
496 | $this->debug("URL: '$url'"); |
497 | } |
498 | |
499 | $result = null; |
500 | try { |
501 | $client = $this->httpService->createClient($url); |
502 | $client->setMethod($method); |
503 | if ($body != null) { |
504 | $client->setRawBody($body); |
505 | } |
506 | $result = $client->send(); |
507 | } catch (\Exception $e) { |
508 | $this->throwAsIlsException($e); |
509 | } |
510 | if (!$result->isSuccess()) { |
511 | throw new ILSException('HTTP error'); |
512 | } |
513 | $answer = $result->getBody(); |
514 | if ($this->debug_enabled) { |
515 | $this->debug("url: $url response: $answer"); |
516 | } |
517 | $answer = str_replace('xmlns=', 'ns=', $answer); |
518 | $result = @simplexml_load_string($answer); |
519 | if (!$result) { |
520 | if ($this->debug_enabled) { |
521 | $this->debug("XML is not valid, URL: $url"); |
522 | } |
523 | throw new ILSException( |
524 | "XML is not valid, URL: $url method: $method answer: $answer." |
525 | ); |
526 | } |
527 | return $result; |
528 | } |
529 | |
530 | /** |
531 | * Convert an ID string into an array of bibliographic base and ID within |
532 | * the base. |
533 | * |
534 | * @param string $id ID to parse. |
535 | * |
536 | * @return array |
537 | */ |
538 | protected function parseId($id) |
539 | { |
540 | $result = null; |
541 | if (str_contains($id, self::RECORD_ID_BASE_SEPARATOR)) { |
542 | $result = explode(self::RECORD_ID_BASE_SEPARATOR, $id); |
543 | $base = $result[0]; |
544 | if (!in_array($base, $this->bib)) { |
545 | throw new \Exception("Unknown library base '$base'"); |
546 | } |
547 | } elseif (count($this->bib) == 1) { |
548 | $result = [$this->bib[0], $id]; |
549 | } else { |
550 | throw new \Exception( |
551 | "Invalid record identifier '$id' " |
552 | . 'without library base' |
553 | ); |
554 | } |
555 | return $result; |
556 | } |
557 | |
558 | /** |
559 | * Get Status |
560 | * |
561 | * This is responsible for retrieving the status information of a certain |
562 | * record. |
563 | * |
564 | * @param string $id The record id to retrieve the holdings for |
565 | * |
566 | * @throws ILSException |
567 | * @return mixed On success, an associative array with the following keys: |
568 | * id, availability (boolean), status, location, reserve, callnumber. |
569 | */ |
570 | public function getStatus($id) |
571 | { |
572 | $statuses = $this->getHolding($id); |
573 | foreach ($statuses as &$status) { |
574 | $status['status'] |
575 | = ($status['availability'] == 1) ? 'available' : 'unavailable'; |
576 | } |
577 | return $statuses; |
578 | } |
579 | |
580 | /** |
581 | * Support method for getStatuses -- load ID information from a particular |
582 | * bibliographic library. |
583 | * |
584 | * @param string $bib Library to search |
585 | * @param array $ids IDs to search within library |
586 | * |
587 | * @return array |
588 | * |
589 | * Description of AVA tag: |
590 | * http://igelu.org/wp-content/uploads/2011/09/Staff-vs-Public-Data-views.pdf |
591 | * (page 28) |
592 | * |
593 | * a ADM code - Institution Code |
594 | * b Sublibrary code - Library Code |
595 | * c Collection (first found) - Collection Code |
596 | * d Call number (first found) |
597 | * e Availability status - If it is on loan (it has a Z36), if it is on hold |
598 | * shelf (it has Z37=S) or if it has a processing status. |
599 | * f Number of items (for entire sublibrary) |
600 | * g Number of unavailable loans |
601 | * h Multi-volume flag (Y/N) If first Z30-ENUMERATION-A is not blank or 0, then |
602 | * the flag=Y, otherwise the flag=N. |
603 | * i Number of loans (for ranking/sorting) |
604 | * j Collection code |
605 | */ |
606 | public function getStatusesX($bib, $ids) |
607 | { |
608 | $doc_nums = ''; |
609 | $sep = ''; |
610 | foreach ($ids as $id) { |
611 | $doc_nums .= $sep . $id; |
612 | $sep = ','; |
613 | } |
614 | $xml = $this->doXRequest( |
615 | 'publish_avail', |
616 | ['library' => $bib, 'doc_num' => $doc_nums], |
617 | false |
618 | ); |
619 | $holding = []; |
620 | foreach ($xml->xpath('/publish-avail/OAI-PMH') as $rec) { |
621 | $identifier = $rec->xpath('.//identifier/text()'); |
622 | $id = ((count($this->bib) > 1) ? $bib . '-' : '') |
623 | . substr($identifier[0], strrpos($identifier[0], ':') + 1); |
624 | $temp = []; |
625 | foreach ($rec->xpath(".//datafield[@tag='AVA']") as $datafield) { |
626 | $status = $datafield->xpath('./subfield[@code="e"]/text()'); |
627 | $location = $datafield->xpath('./subfield[@code="a"]/text()'); |
628 | $signature = $datafield->xpath('./subfield[@code="d"]/text()'); |
629 | $availability |
630 | = ($status[0] == 'available' || $status[0] == 'check_holdings'); |
631 | $reserve = true; |
632 | $temp[] = [ |
633 | 'id' => $id, |
634 | 'availability' => $availability, |
635 | 'status' => (string)$status[0], |
636 | 'location' => (string)$location[0], |
637 | 'signature' => (string)$signature[0], |
638 | 'reserve' => $reserve, |
639 | 'callnumber' => (string)$signature[0], |
640 | ]; |
641 | } |
642 | $holding[] = $temp; |
643 | } |
644 | return $holding; |
645 | } |
646 | |
647 | /** |
648 | * Get Statuses |
649 | * |
650 | * This is responsible for retrieving the status information for a |
651 | * collection of records. |
652 | * |
653 | * @param array $idList The array of record ids to retrieve the status for |
654 | * |
655 | * @throws ILSException |
656 | * @return array An array of getStatus() return values on success. |
657 | */ |
658 | public function getStatuses($idList) |
659 | { |
660 | if (!$this->xserver_enabled) { |
661 | if (!$this->quick_availability) { |
662 | return []; |
663 | } |
664 | $result = []; |
665 | foreach ($idList as $id) { |
666 | $items = $this->getStatus($id); |
667 | $result[] = $items; |
668 | } |
669 | return $result; |
670 | } |
671 | $ids = []; |
672 | $holdings = []; |
673 | foreach ($idList as $id) { |
674 | [$bib, $sys_no] = $this->parseId($id); |
675 | $ids[$bib][] = $sys_no; |
676 | } |
677 | foreach ($ids as $key => $values) { |
678 | $holds = $this->getStatusesX($key, $values); |
679 | foreach ($holds as $hold) { |
680 | $holdings[] = $hold; |
681 | } |
682 | } |
683 | return $holdings; |
684 | } |
685 | |
686 | /** |
687 | * Get Holding |
688 | * |
689 | * This is responsible for retrieving the holding information of a certain |
690 | * record. |
691 | * |
692 | * @param string $id The record id to retrieve the holdings for |
693 | * @param array $patron Patron data |
694 | * @param array $options Extra options (not currently used) |
695 | * |
696 | * @throws DateException |
697 | * @throws ILSException |
698 | * @return array On success, an associative array with the following |
699 | * keys: id, availability (boolean), status, location, reserve, callnumber, |
700 | * duedate, number, barcode. |
701 | * |
702 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
703 | */ |
704 | public function getHolding($id, array $patron = null, array $options = []) |
705 | { |
706 | $holding = []; |
707 | [$bib, $sys_no] = $this->parseId($id); |
708 | $resource = $bib . $sys_no; |
709 | $params = ['view' => 'full']; |
710 | if (!empty($patron['id'])) { |
711 | $params['patron'] = $patron['id']; |
712 | } elseif (isset($this->defaultPatronId)) { |
713 | $params['patron'] = $this->defaultPatronId; |
714 | } |
715 | $xml = $this->doRestDLFRequest(['record', $resource, 'items'], $params); |
716 | if (!empty($xml->{'items'})) { |
717 | $items = $xml->{'items'}->{'item'}; |
718 | } else { |
719 | $items = []; |
720 | } |
721 | foreach ($items as $item) { |
722 | $item_status = (string)$item->{'z30-item-status-code'}; // $isc |
723 | // $ipsc: |
724 | $item_process_status = (string)$item->{'z30-item-process-status-code'}; |
725 | $sub_library_code = (string)$item->{'z30-sub-library-code'}; // $slc |
726 | $z30 = $item->z30; |
727 | if ($this->alephTranslator) { |
728 | $item_status = $this->alephTranslator->tab15Translate( |
729 | $sub_library_code, |
730 | $item_status, |
731 | $item_process_status |
732 | ); |
733 | } else { |
734 | $item_status = [ |
735 | 'opac' => 'Y', |
736 | 'request' => 'C', |
737 | 'desc' => (string)$z30->{'z30-item-status'}, |
738 | 'sub_lib_desc' => (string)$z30->{'z30-sub-library'}, |
739 | ]; |
740 | } |
741 | if ($item_status['opac'] != 'Y') { |
742 | continue; |
743 | } |
744 | $availability = false; |
745 | //$reserve = ($item_status['request'] == 'C')?'N':'Y'; |
746 | $collection = (string)$z30->{'z30-collection'}; |
747 | $collection_desc = ['desc' => $collection]; |
748 | if ($this->alephTranslator) { |
749 | $collection_code = (string)$item->{'z30-collection-code'}; |
750 | $collection_desc = $this->alephTranslator->tab40Translate( |
751 | $collection_code, |
752 | $sub_library_code |
753 | ); |
754 | } |
755 | $requested = false; |
756 | $duedate = ''; |
757 | $addLink = false; |
758 | $status = (string)$item->{'status'}; |
759 | if (in_array($status, $this->available_statuses)) { |
760 | $availability = true; |
761 | } |
762 | if ($item_status['request'] == 'Y' && $availability == false) { |
763 | $addLink = true; |
764 | } |
765 | if (!empty($patron)) { |
766 | $hold_request = $item->xpath('info[@type="HoldRequest"]/@allowed'); |
767 | $addLink = ($hold_request[0] == 'Y'); |
768 | } |
769 | $matches = []; |
770 | $dueDateWithStatusRegEx |
771 | = '/([0-9]*\\/[a-zA-Z0-9]*\\/[0-9]*);([a-zA-Z ]*)/'; |
772 | $dueDateRegEx = '/([0-9]*\\/[a-zA-Z0-9]*\\/[0-9]*)/'; |
773 | if (preg_match($dueDateWithStatusRegEx, $status, $matches)) { |
774 | $duedate = $this->parseDate($matches[1]); |
775 | $requested = (trim($matches[2]) == 'Requested'); |
776 | } elseif (preg_match($dueDateRegEx, $status, $matches)) { |
777 | $duedate = $this->parseDate($matches[1]); |
778 | } else { |
779 | $duedate = null; |
780 | } |
781 | $item_id = $item->attributes()->href; |
782 | $item_id = substr($item_id, strrpos($item_id, '/') + 1); |
783 | $note = (string)$z30->{'z30-note-opac'}; |
784 | $holding[] = [ |
785 | 'id' => $id, |
786 | 'item_id' => $item_id, |
787 | 'availability' => $availability, |
788 | 'status' => (string)$item_status['desc'], |
789 | 'location' => $sub_library_code, |
790 | 'reserve' => 'N', |
791 | 'callnumber' => (string)$z30->{'z30-call-no'}, |
792 | 'duedate' => (string)$duedate, |
793 | 'number' => (string)$z30->{'z30-inventory-number'}, |
794 | 'barcode' => (string)$z30->{'z30-barcode'}, |
795 | 'description' => (string)$z30->{'z30-description'}, |
796 | 'notes' => ($note == null) ? null : [$note], |
797 | 'is_holdable' => true, |
798 | 'addLink' => $addLink, |
799 | 'holdtype' => 'hold', |
800 | /* below are optional attributes*/ |
801 | 'collection' => (string)$collection, |
802 | 'collection_desc' => (string)$collection_desc['desc'], |
803 | 'callnumber_second' => (string)$z30->{'z30-call-no-2'}, |
804 | 'sub_lib_desc' => (string)$item_status['sub_lib_desc'], |
805 | 'no_of_loans' => (string)$z30->{'$no_of_loans'}, |
806 | 'requested' => (string)$requested, |
807 | ]; |
808 | } |
809 | return $holding; |
810 | } |
811 | |
812 | /** |
813 | * Get Patron Loan History |
814 | * |
815 | * @param array $user The patron array from patronLogin |
816 | * @param array $params Parameters |
817 | * |
818 | * @throws DateException |
819 | * @throws ILSException |
820 | * @return array Array of the patron's historic loans on success. |
821 | */ |
822 | public function getMyTransactionHistory($user, $params = null) |
823 | { |
824 | return $this->getMyTransactions($user, $params, true); |
825 | } |
826 | |
827 | /** |
828 | * Get Patron Transactions |
829 | * |
830 | * This is responsible for retrieving all transactions (i.e. checked out items) |
831 | * by a specific patron. |
832 | * |
833 | * @param array $user The patron array from patronLogin |
834 | * @param array $params Parameters |
835 | * @param boolean $history History |
836 | * |
837 | * @throws DateException |
838 | * @throws ILSException |
839 | * @return array Array of the patron's transactions on success. |
840 | */ |
841 | public function getMyTransactions($user, $params = [], $history = false) |
842 | { |
843 | $userId = $user['id']; |
844 | |
845 | $alephParams = []; |
846 | if ($history) { |
847 | $alephParams['type'] = 'history'; |
848 | } |
849 | |
850 | // total count without details is fast |
851 | $totalCount = count( |
852 | $this->doRestDLFRequest( |
853 | ['patron', $userId, 'circulationActions', 'loans'], |
854 | $alephParams |
855 | )->xpath('//loan') |
856 | ); |
857 | |
858 | // with full details and paging |
859 | $pageSize = $params['limit'] ?? 50; |
860 | $itemsNoKey = $history ? 'no_loans' : 'noItems'; |
861 | $alephParams += [ |
862 | 'view' => 'full', |
863 | 'startPos' => isset($params['page']) |
864 | ? ($params['page'] - 1) * $pageSize : 0, |
865 | $itemsNoKey => $pageSize, |
866 | ]; |
867 | |
868 | $xml = $this->doRestDLFRequest( |
869 | ['patron', $userId, 'circulationActions', 'loans'], |
870 | $alephParams |
871 | ); |
872 | |
873 | $transList = []; |
874 | foreach ($xml->xpath('//loan') as $item) { |
875 | $z36 = ($history) ? $item->z36h : $item->z36; |
876 | $prefix = ($history) ? 'z36h-' : 'z36-'; |
877 | $z13 = $item->z13; |
878 | $z30 = $item->z30; |
879 | $group = $item->xpath('@href'); |
880 | $group = substr(strrchr($group[0], '/'), 1); |
881 | $renew = $item->xpath('@renew'); |
882 | |
883 | $location = (string)$z36->{$prefix . 'pickup_location'}; |
884 | $reqnum = (string)$z36->{$prefix . 'doc-number'} |
885 | . (string)$z36->{$prefix . 'item-sequence'} |
886 | . (string)$z36->{$prefix . 'sequence'}; |
887 | |
888 | $due = (string)$z36->{$prefix . 'due-date'}; |
889 | $title = (string)$z13->{'z13-title'}; |
890 | $author = (string)$z13->{'z13-author'}; |
891 | $isbn = (string)$z13->{'z13-isbn-issn'}; |
892 | $barcode = (string)$z30->{'z30-barcode'}; |
893 | // Secondary, Aleph-specific identifier that may be useful for |
894 | // local customizations |
895 | $adm_id = (string)$z30->{'z30-doc-number'}; |
896 | |
897 | $transaction = [ |
898 | 'id' => $this->barcodeToID($barcode), |
899 | 'adm_id' => $adm_id, |
900 | 'item_id' => $group, |
901 | 'location' => $location, |
902 | 'title' => $title, |
903 | 'author' => $author, |
904 | 'isbn' => $isbn, |
905 | 'reqnum' => $reqnum, |
906 | 'barcode' => $barcode, |
907 | 'duedate' => $this->parseDate($due), |
908 | 'renewable' => $renew[0] == 'Y', |
909 | ]; |
910 | if ($history) { |
911 | $issued = (string)$z36->{$prefix . 'loan-date'}; |
912 | $returned = (string)$z36->{$prefix . 'returned-date'}; |
913 | $transaction['checkoutDate'] = $this->parseDate($issued); |
914 | $transaction['returnDate'] = $this->parseDate($returned); |
915 | } |
916 | $transList[] = $transaction; |
917 | } |
918 | |
919 | $key = ($history) ? 'transactions' : 'records'; |
920 | |
921 | return [ |
922 | 'count' => $totalCount, |
923 | $key => $transList, |
924 | ]; |
925 | } |
926 | |
927 | /** |
928 | * Get Renew Details |
929 | * |
930 | * In order to renew an item, Voyager requires the patron details and an item |
931 | * id. This function returns the item id as a string which is then used |
932 | * as submitted form data in checkedOut.php. This value is then extracted by |
933 | * the RenewMyItems function. |
934 | * |
935 | * @param array $details An array of item data |
936 | * |
937 | * @return string Data for use in a form field |
938 | */ |
939 | public function getRenewDetails($details) |
940 | { |
941 | return $details['item_id']; |
942 | } |
943 | |
944 | /** |
945 | * Renew My Items |
946 | * |
947 | * Function for attempting to renew a patron's items. The data in |
948 | * $details['details'] is determined by getRenewDetails(). |
949 | * |
950 | * @param array $details An array of data required for renewing items |
951 | * including the Patron ID and an array of renewal IDS |
952 | * |
953 | * @return array An array of renewal information keyed by item ID |
954 | */ |
955 | public function renewMyItems($details) |
956 | { |
957 | $patron = $details['patron']; |
958 | $result = []; |
959 | foreach ($details['details'] as $id) { |
960 | try { |
961 | $xml = $this->doRestDLFRequest( |
962 | [ |
963 | 'patron', $patron['id'], 'circulationActions', 'loans', $id, |
964 | ], |
965 | null, |
966 | 'POST', |
967 | null |
968 | ); |
969 | $due = (string)current($xml->xpath('//new-due-date')); |
970 | $result[$id] = [ |
971 | 'success' => true, 'new_date' => $this->parseDate($due), |
972 | ]; |
973 | } catch (Aleph\RestfulException $ex) { |
974 | $result[$id] = [ |
975 | 'success' => false, 'sysMessage' => $ex->getMessage(), |
976 | ]; |
977 | } |
978 | } |
979 | return ['blocks' => false, 'details' => $result]; |
980 | } |
981 | |
982 | /** |
983 | * Get Patron Holds |
984 | * |
985 | * This is responsible for retrieving all holds by a specific patron. |
986 | * |
987 | * @param array $user The patron array from patronLogin |
988 | * |
989 | * @throws DateException |
990 | * @throws ILSException |
991 | * @return array Array of the patron's holds on success. |
992 | */ |
993 | public function getMyHolds($user) |
994 | { |
995 | $userId = $user['id']; |
996 | $holdList = []; |
997 | $xml = $this->doRestDLFRequest( |
998 | ['patron', $userId, 'circulationActions', 'requests', 'holds'], |
999 | ['view' => 'full'] |
1000 | ); |
1001 | foreach ($xml->xpath('//hold-request') as $item) { |
1002 | $z37 = $item->z37; |
1003 | $z13 = $item->z13; |
1004 | $z30 = $item->z30; |
1005 | $delete = $item->xpath('@delete'); |
1006 | $href = $item->xpath('@href'); |
1007 | $item_id = substr($href[0], strrpos($href[0], '/') + 1); |
1008 | $type = 'hold'; |
1009 | $location = (string)$z37->{'z37-pickup-location'}; |
1010 | $reqnum = (string)$z37->{'z37-doc-number'} |
1011 | . (string)$z37->{'z37-item-sequence'} |
1012 | . (string)$z37->{'z37-sequence'}; |
1013 | $expire = (string)$z37->{'z37-end-request-date'}; |
1014 | $create = (string)$z37->{'z37-open-date'}; |
1015 | $holddate = (string)$z37->{'z37-hold-date'}; |
1016 | $title = (string)$z13->{'z13-title'}; |
1017 | $author = (string)$z13->{'z13-author'}; |
1018 | $isbn = (string)$z13->{'z13-isbn-issn'}; |
1019 | $barcode = (string)$z30->{'z30-barcode'}; |
1020 | // remove superfluous spaces in status |
1021 | $status = preg_replace("/\s[\s]+/", ' ', $item->status); |
1022 | $position = null; |
1023 | // Extract position in the hold queue from item status |
1024 | if (preg_match($this->queuePositionRegex, $status, $matches)) { |
1025 | $position = $matches['position']; |
1026 | } |
1027 | if ($holddate == '00000000') { |
1028 | $holddate = null; |
1029 | } else { |
1030 | $holddate = $this->parseDate($holddate); |
1031 | } |
1032 | $delete = ($delete[0] == 'Y'); |
1033 | // Secondary, Aleph-specific identifier that may be useful for |
1034 | // local customizations |
1035 | $adm_id = (string)$z30->{'z30-doc-number'}; |
1036 | |
1037 | $holdList[] = [ |
1038 | 'type' => $type, |
1039 | 'item_id' => $item_id, |
1040 | 'adm_id' => $adm_id, |
1041 | 'location' => $location, |
1042 | 'title' => $title, |
1043 | 'author' => $author, |
1044 | 'isbn' => $isbn, |
1045 | 'reqnum' => $reqnum, |
1046 | 'barcode' => $barcode, |
1047 | 'id' => $this->barcodeToID($barcode), |
1048 | 'expire' => $this->parseDate($expire), |
1049 | 'holddate' => $holddate, |
1050 | 'delete' => $delete, |
1051 | 'create' => $this->parseDate($create), |
1052 | 'status' => $status, |
1053 | 'position' => $position, |
1054 | ]; |
1055 | } |
1056 | return $holdList; |
1057 | } |
1058 | |
1059 | /** |
1060 | * Get Cancel Hold Details |
1061 | * |
1062 | * @param array $holdDetails A single hold array from getMyHolds |
1063 | * @param array $patron Patron information from patronLogin |
1064 | * |
1065 | * @return string Data for use in a form field |
1066 | * |
1067 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1068 | */ |
1069 | public function getCancelHoldDetails($holdDetails, $patron = []) |
1070 | { |
1071 | if ($holdDetails['delete']) { |
1072 | return $holdDetails['item_id']; |
1073 | } else { |
1074 | return ''; |
1075 | } |
1076 | } |
1077 | |
1078 | /** |
1079 | * Cancel Holds |
1080 | * |
1081 | * Attempts to Cancel a hold or recall on a particular item. The |
1082 | * data in $cancelDetails['details'] is determined by getCancelHoldDetails(). |
1083 | * |
1084 | * @param array $details An array of item and patron data |
1085 | * |
1086 | * @return array An array of data on each request including |
1087 | * whether or not it was successful and a system message (if available) |
1088 | */ |
1089 | public function cancelHolds($details) |
1090 | { |
1091 | $patron = $details['patron']; |
1092 | $patronId = $patron['id']; |
1093 | $count = 0; |
1094 | $items = []; |
1095 | foreach ($details['details'] as $id) { |
1096 | try { |
1097 | $result = $this->doRestDLFRequest( |
1098 | [ |
1099 | 'patron', $patronId, 'circulationActions', 'requests', |
1100 | 'holds', $id, |
1101 | ], |
1102 | null, |
1103 | 'DELETE' |
1104 | ); |
1105 | $count++; |
1106 | $items[$id] = ['success' => true, 'status' => 'cancel_hold_ok']; |
1107 | } catch (Aleph\RestfulException $e) { |
1108 | $items[$id] = [ |
1109 | 'success' => false, |
1110 | 'status' => 'cancel_hold_failed', |
1111 | 'sysMessage' => $e->getMessage(), |
1112 | ]; |
1113 | } |
1114 | } |
1115 | return ['count' => $count, 'items' => $items]; |
1116 | } |
1117 | |
1118 | /** |
1119 | * Get Patron Fines |
1120 | * |
1121 | * This is responsible for retrieving all fines by a specific patron. |
1122 | * |
1123 | * @param array $user The patron array from patronLogin |
1124 | * |
1125 | * @throws DateException |
1126 | * @throws ILSException |
1127 | * @return mixed Array of the patron's fines on success. |
1128 | */ |
1129 | public function getMyFines($user) |
1130 | { |
1131 | $finesList = []; |
1132 | |
1133 | $xml = $this->doRestDLFRequest( |
1134 | ['patron', $user['id'], 'circulationActions', 'cash'], |
1135 | ['view' => 'full'] |
1136 | ); |
1137 | |
1138 | foreach ($xml->xpath('//cash') as $item) { |
1139 | $z31 = $item->z31; |
1140 | $z13 = $item->z13; |
1141 | $z30 = $item->z30; |
1142 | $title = (string)$z13->{'z13-title'}; |
1143 | $description = (string)$z31->{'z31-description'}; |
1144 | $transactiondate = date('d-m-Y', strtotime((string)$z31->{'z31-date'})); |
1145 | $transactiontype = (string)$z31->{'z31-credit-debit'}; |
1146 | $id = (string)$z13->{'z13-doc-number'}; |
1147 | $barcode = (string)$z30->{'z30-barcode'}; |
1148 | $checkout = (string)$z31->{'z31-date'}; |
1149 | $id = $this->barcodeToID($barcode); |
1150 | $cachetype = strtolower((string)($item->attributes()->type ?? '')); |
1151 | $mult = $cachetype == 'debit' ? -100 : 100; |
1152 | $amount |
1153 | = (float)(preg_replace("/[\(\)]/", '', (string)$z31->{'z31-sum'})) |
1154 | * $mult; |
1155 | $cashref = (string)$z31->{'z31-sequence'}; |
1156 | |
1157 | $finesList["$cashref"] = [ |
1158 | 'title' => $title, |
1159 | 'barcode' => $barcode, |
1160 | 'amount' => $amount, |
1161 | 'transactiondate' => $transactiondate, |
1162 | 'transactiontype' => $transactiontype, |
1163 | 'checkout' => $this->parseDate($checkout), |
1164 | 'balance' => $amount, |
1165 | 'id' => $id, |
1166 | 'printLink' => 'test', |
1167 | 'fine' => $description, |
1168 | ]; |
1169 | } |
1170 | ksort($finesList); |
1171 | return array_values($finesList); |
1172 | } |
1173 | |
1174 | /** |
1175 | * Get Patron Profile |
1176 | * |
1177 | * This is responsible for retrieving the profile for a specific patron. |
1178 | * |
1179 | * @param array $user The patron array |
1180 | * |
1181 | * @throws ILSException |
1182 | * @return array Array of the patron's profile data on success. |
1183 | */ |
1184 | public function getMyProfile($user) |
1185 | { |
1186 | if ($this->xserver_enabled) { |
1187 | $profile = $this->getMyProfileX($user); |
1188 | } else { |
1189 | $profile = $this->getMyProfileDLF($user); |
1190 | } |
1191 | $profile['cat_username'] ??= $user['id']; |
1192 | return $profile; |
1193 | } |
1194 | |
1195 | /** |
1196 | * Get profile information using X-server. |
1197 | * |
1198 | * @param array $user The patron array |
1199 | * |
1200 | * @throws ILSException |
1201 | * @return array Array of the patron's profile data on success. |
1202 | */ |
1203 | public function getMyProfileX($user) |
1204 | { |
1205 | if (!isset($user['college'])) { |
1206 | $user['college'] = $this->useradm; |
1207 | } |
1208 | $xml = $this->doXRequest( |
1209 | 'bor-info', |
1210 | [ |
1211 | 'loans' => 'N', 'cash' => 'N', 'hold' => 'N', |
1212 | 'library' => $user['college'], 'bor_id' => $user['id'], |
1213 | ], |
1214 | true |
1215 | ); |
1216 | $id = (string)$xml->z303->{'z303-id'}; |
1217 | $address1 = (string)$xml->z304->{'z304-address-2'}; |
1218 | $address2 = (string)$xml->z304->{'z304-address-3'}; |
1219 | $zip = (string)$xml->z304->{'z304-zip'}; |
1220 | $phone = (string)$xml->z304->{'z304-telephone'}; |
1221 | $barcode = (string)$xml->z304->{'z304-address-0'}; |
1222 | $group = (string)$xml->z305->{'z305-bor-status'}; |
1223 | $expiry = (string)$xml->z305->{'z305-expiry-date'}; |
1224 | $credit_sum = (string)$xml->z305->{'z305-sum'}; |
1225 | $credit_sign = (string)$xml->z305->{'z305-credit-debit'}; |
1226 | $name = (string)$xml->z303->{'z303-name'}; |
1227 | if (strstr($name, ',')) { |
1228 | [$lastname, $firstname] = explode(',', $name); |
1229 | } else { |
1230 | $lastname = $name; |
1231 | $firstname = ''; |
1232 | } |
1233 | if ($credit_sign == null) { |
1234 | $credit_sign = 'C'; |
1235 | } |
1236 | $recordList = compact('firstname', 'lastname'); |
1237 | if (isset($user['email'])) { |
1238 | $recordList['email'] = $user['email']; |
1239 | } |
1240 | $recordList['address1'] = $address1; |
1241 | $recordList['address2'] = $address2; |
1242 | $recordList['zip'] = $zip; |
1243 | $recordList['phone'] = $phone; |
1244 | $recordList['group'] = $group; |
1245 | $recordList['barcode'] = $barcode; |
1246 | $recordList['expire'] = $this->parseDate($expiry); |
1247 | $recordList['credit'] = $expiry; |
1248 | $recordList['credit_sum'] = $credit_sum; |
1249 | $recordList['credit_sign'] = $credit_sign; |
1250 | $recordList['id'] = $id; |
1251 | return $recordList; |
1252 | } |
1253 | |
1254 | /** |
1255 | * Get profile information using DLF service. |
1256 | * |
1257 | * @param array $user The patron array |
1258 | * |
1259 | * @throws ILSException |
1260 | * @return array Array of the patron's profile data on success. |
1261 | */ |
1262 | public function getMyProfileDLF($user) |
1263 | { |
1264 | $recordList = []; |
1265 | $xml = $this->doRestDLFRequest( |
1266 | ['patron', $user['id'], 'patronInformation', 'address'] |
1267 | ); |
1268 | $profile = []; |
1269 | $profile['id'] = $user['id']; |
1270 | $profile['cat_username'] = $user['id']; |
1271 | $address = $xml->xpath('//address-information')[0]; |
1272 | foreach ($this->addressMappings as $key => $value) { |
1273 | if (!empty($value)) { |
1274 | $profile[$key] = (string)$address->{$value}; |
1275 | } |
1276 | } |
1277 | $fullName = $profile['fullname']; |
1278 | if (!str_contains($fullName, ',')) { |
1279 | $profile['lastname'] = $fullName; |
1280 | $profile['firstname'] = ''; |
1281 | } else { |
1282 | [$profile['lastname'], $profile['firstname']] |
1283 | = explode(',', $fullName); |
1284 | } |
1285 | $xml = $this->doRestDLFRequest( |
1286 | ['patron', $user['id'], 'patronStatus', 'registration'] |
1287 | ); |
1288 | $status = $xml->xpath('//institution/z305-bor-status'); |
1289 | $expiry = $xml->xpath('//institution/z305-expiry-date'); |
1290 | $profile['expiration_date'] = $this->parseDate($expiry[0]); |
1291 | $profile['group'] = $status[0]; |
1292 | return $profile; |
1293 | } |
1294 | |
1295 | /** |
1296 | * Patron Login |
1297 | * |
1298 | * This is responsible for authenticating a patron against the catalog. |
1299 | * |
1300 | * @param string $user The patron username |
1301 | * @param string $password The patron's password |
1302 | * |
1303 | * @throws ILSException |
1304 | * @return mixed Associative array of patron info on successful login, |
1305 | * null on unsuccessful login. |
1306 | */ |
1307 | public function patronLogin($user, $password) |
1308 | { |
1309 | if ($password == null) { |
1310 | $temp = ['id' => $user]; |
1311 | $temp['college'] = $this->useradm; |
1312 | return $this->getMyProfile($temp); |
1313 | } |
1314 | try { |
1315 | $xml = $this->doXRequest( |
1316 | 'bor-auth', |
1317 | [ |
1318 | 'library' => $this->useradm, 'bor_id' => $user, |
1319 | 'verification' => $password, |
1320 | ], |
1321 | true |
1322 | ); |
1323 | } catch (\Exception $ex) { |
1324 | if (str_contains($ex->getMessage(), 'Error in Verification')) { |
1325 | return null; |
1326 | } |
1327 | $this->throwAsIlsException($ex); |
1328 | } |
1329 | $patron = []; |
1330 | $name = $xml->z303->{'z303-name'}; |
1331 | if (strstr($name, ',')) { |
1332 | [$lastName, $firstName] = explode(',', $name); |
1333 | } else { |
1334 | $lastName = $name; |
1335 | $firstName = ''; |
1336 | } |
1337 | $email_addr = $xml->z304->{'z304-email-address'}; |
1338 | $id = $xml->z303->{'z303-id'}; |
1339 | $home_lib = $xml->z303->z303_home_library; |
1340 | // Default the college to the useradm library and overwrite it if the |
1341 | // home_lib exists |
1342 | $patron['college'] = $this->useradm; |
1343 | if (($home_lib != '') && (array_key_exists("$home_lib", $this->sublibadm))) { |
1344 | if ($this->sublibadm["$home_lib"] != '') { |
1345 | $patron['college'] = $this->sublibadm["$home_lib"]; |
1346 | } |
1347 | } |
1348 | $patron['id'] = (string)$id; |
1349 | $patron['barcode'] = (string)$user; |
1350 | $patron['firstname'] = (string)$firstName; |
1351 | $patron['lastname'] = (string)$lastName; |
1352 | $patron['cat_username'] = (string)$user; |
1353 | $patron['cat_password'] = $password; |
1354 | $patron['email'] = (string)$email_addr; |
1355 | $patron['major'] = null; |
1356 | return $patron; |
1357 | } |
1358 | |
1359 | /** |
1360 | * Support method for placeHold -- get holding info for an item. |
1361 | * |
1362 | * @param string $patronId Patron ID |
1363 | * @param string $id Bib ID |
1364 | * @param string $group Item ID |
1365 | * |
1366 | * @return array |
1367 | */ |
1368 | public function getHoldingInfoForItem($patronId, $id, $group) |
1369 | { |
1370 | [$bib, $sys_no] = $this->parseId($id); |
1371 | $resource = $bib . $sys_no; |
1372 | $xml = $this->doRestDLFRequest( |
1373 | ['patron', $patronId, 'record', $resource, 'items', $group] |
1374 | ); |
1375 | $locations = []; |
1376 | $part = $xml->xpath('//pickup-locations'); |
1377 | if ($part) { |
1378 | foreach ($part[0]->children() as $node) { |
1379 | $arr = $node->attributes(); |
1380 | $code = (string)$arr['code']; |
1381 | $loc_name = (string)$node; |
1382 | $locations[$code] = $loc_name; |
1383 | } |
1384 | } else { |
1385 | throw new ILSException('No pickup locations'); |
1386 | } |
1387 | $requests = 0; |
1388 | $str = $xml->xpath('//item/queue/text()'); |
1389 | if ($str != null) { |
1390 | [$requests] = explode(' ', trim($str[0])); |
1391 | } |
1392 | $date = $xml->xpath('//last-interest-date/text()'); |
1393 | $date = $date[0]; |
1394 | $date = '' . substr($date, 6, 2) . '.' . substr($date, 4, 2) . '.' |
1395 | . substr($date, 0, 4); |
1396 | return [ |
1397 | 'pickup-locations' => $locations, 'last-interest-date' => $date, |
1398 | 'order' => $requests + 1, |
1399 | ]; |
1400 | } |
1401 | |
1402 | /** |
1403 | * Get Default "Hold Required By" Date (as Unix timestamp) or null if unsupported |
1404 | * |
1405 | * @param array $patron Patron information returned by the patronLogin method. |
1406 | * @param array $holdInfo Contains most of the same values passed to |
1407 | * placeHold, minus the patron data. |
1408 | * |
1409 | * @return int|null |
1410 | */ |
1411 | public function getHoldDefaultRequiredDate($patron, $holdInfo) |
1412 | { |
1413 | $details = []; |
1414 | if ($holdInfo != null) { |
1415 | $details = $this->getHoldingInfoForItem( |
1416 | $patron['id'], |
1417 | $holdInfo['id'], |
1418 | $holdInfo['item_id'] |
1419 | ); |
1420 | } |
1421 | if (isset($details['last-interest-date'])) { |
1422 | try { |
1423 | return $this->dateConverter |
1424 | ->convert('d.m.Y', 'U', $details['last-interest-date']); |
1425 | } catch (DateException $e) { |
1426 | // If we couldn't convert the date, fail gracefully. |
1427 | $this->debug( |
1428 | 'Could not convert date: ' . $details['last-interest-date'] |
1429 | ); |
1430 | } |
1431 | } |
1432 | return null; |
1433 | } |
1434 | |
1435 | /** |
1436 | * Place Hold |
1437 | * |
1438 | * Attempts to place a hold or recall on a particular item and returns |
1439 | * an array with result details or throws an exception on failure of support |
1440 | * classes |
1441 | * |
1442 | * @param array $details An array of item and patron data |
1443 | * |
1444 | * @throws ILSException |
1445 | * @return mixed An array of data on the request including |
1446 | * whether or not it was successful and a system message (if available) |
1447 | */ |
1448 | public function placeHold($details) |
1449 | { |
1450 | [$bib, $sys_no] = $this->parseId($details['id']); |
1451 | $recordId = $bib . $sys_no; |
1452 | $itemId = $details['item_id']; |
1453 | $patron = $details['patron']; |
1454 | $pickupLocation = $details['pickUpLocation']; |
1455 | if (!$pickupLocation) { |
1456 | $pickupLocation = $this->getDefaultPickUpLocation($patron, $details); |
1457 | } |
1458 | $comment = $details['comment']; |
1459 | if (strlen($comment) <= 50) { |
1460 | $comment1 = $comment; |
1461 | $comment2 = null; |
1462 | } else { |
1463 | $comment1 = substr($comment, 0, 50); |
1464 | $comment2 = substr($comment, 50, 50); |
1465 | } |
1466 | try { |
1467 | $requiredBy = $this->dateConverter |
1468 | ->convertFromDisplayDate('Ymd', $details['requiredBy']); |
1469 | } catch (DateException $de) { |
1470 | return [ |
1471 | 'success' => false, |
1472 | 'sysMessage' => 'hold_date_invalid', |
1473 | ]; |
1474 | } |
1475 | $patronId = $patron['id']; |
1476 | $body = new \SimpleXMLElement( |
1477 | '<?xml version="1.0" encoding="UTF-8"?>' |
1478 | . '<hold-request-parameters></hold-request-parameters>' |
1479 | ); |
1480 | $body->addChild('pickup-location', $pickupLocation); |
1481 | $body->addChild('last-interest-date', $requiredBy); |
1482 | $body->addChild('note-1', $comment1); |
1483 | if (isset($comment2)) { |
1484 | $body->addChild('note-2', $comment2); |
1485 | } |
1486 | $body = 'post_xml=' . $body->asXML(); |
1487 | try { |
1488 | $this->doRestDLFRequest( |
1489 | [ |
1490 | 'patron', $patronId, 'record', $recordId, 'items', $itemId, |
1491 | 'hold', |
1492 | ], |
1493 | null, |
1494 | 'PUT', |
1495 | $body |
1496 | ); |
1497 | } catch (Aleph\RestfulException $exception) { |
1498 | $message = $exception->getMessage(); |
1499 | $note = $exception->getXmlResponse() |
1500 | ->xpath('/put-item-hold/create-hold/note[@type="error"]'); |
1501 | $note = $note[0]; |
1502 | return [ |
1503 | 'success' => false, |
1504 | 'sysMessage' => "$message ($note)", |
1505 | ]; |
1506 | } |
1507 | return ['success' => true]; |
1508 | } |
1509 | |
1510 | /** |
1511 | * Convert a barcode to an item ID. |
1512 | * |
1513 | * @param string $bar Barcode |
1514 | * |
1515 | * @return string|null |
1516 | */ |
1517 | public function barcodeToID($bar) |
1518 | { |
1519 | if (!$this->xserver_enabled) { |
1520 | return null; |
1521 | } |
1522 | foreach ($this->bib as $base) { |
1523 | try { |
1524 | $xml = $this->doXRequest( |
1525 | 'find', |
1526 | ['base' => $base, 'request' => "BAR=$bar"], |
1527 | false |
1528 | ); |
1529 | $docs = (int)$xml->{'no_records'}; |
1530 | if ($docs == 1) { |
1531 | $set = (string)$xml->{'set_number'}; |
1532 | $result = $this->doXRequest( |
1533 | 'present', |
1534 | ['set_number' => $set, 'set_entry' => '1'], |
1535 | false |
1536 | ); |
1537 | $id = $result->xpath('//doc_number/text()'); |
1538 | $idString = (string)$id[0]; |
1539 | if (count($this->bib) == 1) { |
1540 | return $idString; |
1541 | } else { |
1542 | return $base . '-' . $idString; |
1543 | } |
1544 | } |
1545 | } catch (\Exception $ex) { |
1546 | } |
1547 | } |
1548 | throw new ILSException('barcode not found'); |
1549 | } |
1550 | |
1551 | /** |
1552 | * Parse a date. |
1553 | * |
1554 | * @param string $date Date to parse |
1555 | * |
1556 | * @return string |
1557 | */ |
1558 | public function parseDate($date) |
1559 | { |
1560 | if ($date == null || $date == '') { |
1561 | return ''; |
1562 | } elseif (preg_match('/^[0-9]{8}$/', $date) === 1) { // 20120725 |
1563 | return $this->dateConverter->convertToDisplayDate('Ynd', $date); |
1564 | } elseif (preg_match("/^[0-9]+\/[A-Za-z]{3}\/[0-9]{4}$/", $date) === 1) { |
1565 | // 13/jan/2012 |
1566 | return $this->dateConverter->convertToDisplayDate('d/M/Y', $date); |
1567 | } elseif (preg_match("/^[0-9]+\/[0-9]+\/[0-9]{4}$/", $date) === 1) { |
1568 | // 13/7/2012 |
1569 | return $this->dateConverter->convertToDisplayDate('d/m/Y', $date); |
1570 | } elseif (preg_match("/^[0-9]+\/[0-9]+\/[0-9]{2}$/", $date) === 1) { |
1571 | // 13/7/12 |
1572 | return $this->dateConverter->convertToDisplayDate('d/m/y', $date); |
1573 | } else { |
1574 | throw new \Exception("Invalid date: $date"); |
1575 | } |
1576 | } |
1577 | |
1578 | /** |
1579 | * Helper method to determine whether or not a certain method can be |
1580 | * called on this driver. Required method for any smart drivers. |
1581 | * |
1582 | * @param string $method The name of the called method. |
1583 | * @param array $params Array of passed parameters |
1584 | * |
1585 | * @return bool True if the method can be called with the given parameters, |
1586 | * false otherwise. |
1587 | * |
1588 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1589 | */ |
1590 | public function supportsMethod($method, $params) |
1591 | { |
1592 | // Loan history is only available if properly configured |
1593 | if ($method == 'getMyTransactionHistory') { |
1594 | return !empty($this->config['TransactionHistory']['enabled']); |
1595 | } |
1596 | return is_callable([$this, $method]); |
1597 | } |
1598 | |
1599 | /** |
1600 | * Public Function which retrieves historic loan, renew, hold and cancel |
1601 | * settings from the driver ini file. |
1602 | * |
1603 | * @param string $func The name of the feature to be checked |
1604 | * @param array $params Optional feature-specific parameters (array) |
1605 | * |
1606 | * @return array An array with key-value pairs. |
1607 | * |
1608 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1609 | */ |
1610 | public function getConfig($func, $params = []) |
1611 | { |
1612 | if ($func == 'Holds') { |
1613 | $holdsConfig = $this->config['Holds'] ?? []; |
1614 | $defaults = [ |
1615 | 'HMACKeys' => 'id:item_id', |
1616 | 'extraHoldFields' => 'comments:requiredByDate:pickUpLocation', |
1617 | 'defaultRequiredDate' => '0:1:0', |
1618 | ]; |
1619 | return $holdsConfig + $defaults; |
1620 | } elseif ('getMyTransactionHistory' === $func) { |
1621 | if (empty($this->config['TransactionHistory']['enabled'])) { |
1622 | return false; |
1623 | } |
1624 | return [ |
1625 | 'max_results' => 10000, |
1626 | ]; |
1627 | } else { |
1628 | return []; |
1629 | } |
1630 | } |
1631 | |
1632 | /** |
1633 | * Get Pick Up Locations |
1634 | * |
1635 | * This is responsible for getting a list of valid library locations for |
1636 | * holds / recall retrieval |
1637 | * |
1638 | * @param array $patron Patron information returned by the patronLogin method. |
1639 | * @param array $holdInfo Optional array, only passed in when getting a list |
1640 | * in the context of placing or editing a hold. When placing a hold, it contains |
1641 | * most of the same values passed to placeHold, minus the patron data. When |
1642 | * editing a hold it contains all the hold information returned by getMyHolds. |
1643 | * May be used to limit the pickup options or may be ignored. The driver must |
1644 | * not add new options to the return array based on this data or other areas of |
1645 | * VuFind may behave incorrectly. |
1646 | * |
1647 | * @throws ILSException |
1648 | * @return array An array of associative arrays with locationID and |
1649 | * locationDisplay keys |
1650 | */ |
1651 | public function getPickUpLocations($patron, $holdInfo = null) |
1652 | { |
1653 | $pickupLocations = []; |
1654 | if ($holdInfo != null) { |
1655 | $details = $this->getHoldingInfoForItem( |
1656 | $patron['id'], |
1657 | $holdInfo['id'], |
1658 | $holdInfo['item_id'] |
1659 | ); |
1660 | foreach ($details['pickup-locations'] as $key => $value) { |
1661 | $pickupLocations[] = [ |
1662 | 'locationID' => $key, |
1663 | 'locationDisplay' => $value, |
1664 | ]; |
1665 | } |
1666 | } else { |
1667 | $default = $this->getDefaultPickUpLocation($patron); |
1668 | if (!empty($default)) { |
1669 | $pickupLocations[] = [ |
1670 | 'locationID' => $default, |
1671 | 'locationDisplay' => $default, |
1672 | ]; |
1673 | } |
1674 | } |
1675 | return $pickupLocations; |
1676 | } |
1677 | |
1678 | /** |
1679 | * Get Default Pick Up Location |
1680 | * |
1681 | * Returns the default pick up location set in VoyagerRestful.ini |
1682 | * |
1683 | * @param array $patron Patron information returned by the patronLogin method. |
1684 | * @param array $holdInfo Optional array, only passed in when getting a list |
1685 | * in the context of placing a hold; contains most of the same values passed to |
1686 | * placeHold, minus the patron data. May be used to limit the pickup options |
1687 | * or may be ignored. |
1688 | * |
1689 | * @return string The default pickup location for the patron. |
1690 | */ |
1691 | public function getDefaultPickUpLocation($patron, $holdInfo = null) |
1692 | { |
1693 | if ($holdInfo != null) { |
1694 | $details = $this->getHoldingInfoForItem( |
1695 | $patron['id'], |
1696 | $holdInfo['id'], |
1697 | $holdInfo['item_id'] |
1698 | ); |
1699 | $pickupLocations = $details['pickup-locations']; |
1700 | if (isset($this->preferredPickUpLocations)) { |
1701 | foreach (array_keys($details['pickup-locations']) as $locationID) { |
1702 | if (in_array($locationID, $this->preferredPickUpLocations)) { |
1703 | return $locationID; |
1704 | } |
1705 | } |
1706 | } |
1707 | // nothing found or preferredPickUpLocations is empty? Return the first |
1708 | // locationId in pickupLocations array |
1709 | return array_key_first($pickupLocations); |
1710 | } elseif (isset($this->preferredPickUpLocations)) { |
1711 | return $this->preferredPickUpLocations[0]; |
1712 | } else { |
1713 | throw new ILSException( |
1714 | 'Missing Catalog/preferredPickUpLocations config setting.' |
1715 | ); |
1716 | } |
1717 | } |
1718 | |
1719 | /** |
1720 | * Get Purchase History |
1721 | * |
1722 | * This is responsible for retrieving the acquisitions history data for the |
1723 | * specific record (usually recently received issues of a serial). |
1724 | * |
1725 | * @param string $id The record id to retrieve the info for |
1726 | * |
1727 | * @throws ILSException |
1728 | * @return array An array with the acquisitions data on success. |
1729 | * |
1730 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1731 | */ |
1732 | public function getPurchaseHistory($id) |
1733 | { |
1734 | // TODO |
1735 | return []; |
1736 | } |
1737 | |
1738 | /** |
1739 | * Get New Items |
1740 | * |
1741 | * Retrieve the IDs of items recently added to the catalog. |
1742 | * |
1743 | * @param int $page Page number of results to retrieve (counting starts at 1) |
1744 | * @param int $limit The size of each page of results to retrieve |
1745 | * @param int $daysOld The maximum age of records to retrieve in days (max. 30) |
1746 | * @param int $fundId optional fund ID to use for limiting results (use a value |
1747 | * returned by getFunds, or exclude for no limit); note that "fund" may be a |
1748 | * misnomer - if funds are not an appropriate way to limit your new item |
1749 | * results, you can return a different set of values from getFunds. The |
1750 | * important thing is that this parameter supports an ID returned by getFunds, |
1751 | * whatever that may mean. |
1752 | * |
1753 | * @throws ILSException |
1754 | * @return array Associative array with 'count' and 'results' keys |
1755 | * |
1756 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1757 | */ |
1758 | public function getNewItems($page, $limit, $daysOld, $fundId = null) |
1759 | { |
1760 | // TODO |
1761 | $items = []; |
1762 | return $items; |
1763 | } |
1764 | |
1765 | /** |
1766 | * Get Departments |
1767 | * |
1768 | * Obtain a list of departments for use in limiting the reserves list. |
1769 | * |
1770 | * @throws ILSException |
1771 | * @return array An associative array with key = dept. ID, value = dept. name. |
1772 | */ |
1773 | public function getDepartments() |
1774 | { |
1775 | // TODO |
1776 | return []; |
1777 | } |
1778 | |
1779 | /** |
1780 | * Get Instructors |
1781 | * |
1782 | * Obtain a list of instructors for use in limiting the reserves list. |
1783 | * |
1784 | * @throws ILSException |
1785 | * @return array An associative array with key = ID, value = name. |
1786 | */ |
1787 | public function getInstructors() |
1788 | { |
1789 | // TODO |
1790 | return []; |
1791 | } |
1792 | |
1793 | /** |
1794 | * Get Courses |
1795 | * |
1796 | * Obtain a list of courses for use in limiting the reserves list. |
1797 | * |
1798 | * @throws ILSException |
1799 | * @return array An associative array with key = ID, value = name. |
1800 | */ |
1801 | public function getCourses() |
1802 | { |
1803 | // TODO |
1804 | return []; |
1805 | } |
1806 | |
1807 | /** |
1808 | * Find Reserves |
1809 | * |
1810 | * Obtain information on course reserves. |
1811 | * |
1812 | * @param string $course ID from getCourses (empty string to match all) |
1813 | * @param string $inst ID from getInstructors (empty string to match all) |
1814 | * @param string $dept ID from getDepartments (empty string to match all) |
1815 | * |
1816 | * @throws ILSException |
1817 | * @return array An array of associative arrays representing reserve items. |
1818 | * |
1819 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1820 | */ |
1821 | public function findReserves($course, $inst, $dept) |
1822 | { |
1823 | // TODO |
1824 | return []; |
1825 | } |
1826 | } |