Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 224 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
Holds | |
0.00% |
0 / 224 |
|
0.00% |
0 / 11 |
8190 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
formatHoldings | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
342 | |||
getHoldings | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
132 | |||
standardHoldings | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
20 | |||
driverHoldings | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
generateHoldings | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
420 | |||
processStorageRetrievalRequests | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
72 | |||
processILLRequests | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
72 | |||
getRequestDetails | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
getHoldingsGroupKey | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
42 | |||
getSuppressedLocations | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | /** |
4 | * Hold Logic Class |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2007. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package ILS_Logic |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @author Luke O'Sullivan <l.osullivan@swansea.ac.uk> |
27 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
28 | * @link https://vufind.org/wiki/development Wiki |
29 | */ |
30 | |
31 | namespace VuFind\ILS\Logic; |
32 | |
33 | use VuFind\Exception\ILS as ILSException; |
34 | use VuFind\ILS\Connection as ILSConnection; |
35 | |
36 | use function in_array; |
37 | use function is_array; |
38 | |
39 | /** |
40 | * Hold Logic Class |
41 | * |
42 | * @category VuFind |
43 | * @package ILS_Logic |
44 | * @author Demian Katz <demian.katz@villanova.edu> |
45 | * @author Luke O'Sullivan <l.osullivan@swansea.ac.uk> |
46 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
47 | * @link https://vufind.org/wiki/development Wiki |
48 | */ |
49 | class Holds |
50 | { |
51 | /** |
52 | * ILS authenticator |
53 | * |
54 | * @var \VuFind\Auth\ILSAuthenticator |
55 | */ |
56 | protected $ilsAuth; |
57 | |
58 | /** |
59 | * Catalog connection object |
60 | * |
61 | * @var ILSConnection |
62 | */ |
63 | protected $catalog; |
64 | |
65 | /** |
66 | * HMAC generator |
67 | * |
68 | * @var \VuFind\Crypt\HMAC |
69 | */ |
70 | protected $hmac; |
71 | |
72 | /** |
73 | * VuFind configuration |
74 | * |
75 | * @var \Laminas\Config\Config |
76 | */ |
77 | protected $config; |
78 | |
79 | /** |
80 | * Holding locations to hide from display |
81 | * |
82 | * @var array |
83 | */ |
84 | protected $hideHoldings = []; |
85 | |
86 | /** |
87 | * Constructor |
88 | * |
89 | * @param \VuFind\Auth\ILSAuthenticator $ilsAuth ILS authenticator |
90 | * @param ILSConnection $ils A catalog connection |
91 | * @param \VuFind\Crypt\HMAC $hmac HMAC generator |
92 | * @param \Laminas\Config\Config $config VuFind configuration |
93 | */ |
94 | public function __construct( |
95 | \VuFind\Auth\ILSAuthenticator $ilsAuth, |
96 | ILSConnection $ils, |
97 | \VuFind\Crypt\HMAC $hmac, |
98 | \Laminas\Config\Config $config |
99 | ) { |
100 | $this->ilsAuth = $ilsAuth; |
101 | $this->hmac = $hmac; |
102 | $this->config = $config; |
103 | |
104 | if (isset($this->config->Record->hide_holdings)) { |
105 | foreach ($this->config->Record->hide_holdings as $current) { |
106 | $this->hideHoldings[] = $current; |
107 | } |
108 | } |
109 | |
110 | $this->catalog = $ils; |
111 | } |
112 | |
113 | /** |
114 | * Support method to rearrange the holdings array for displaying convenience. |
115 | * |
116 | * @param array $holdings An associative array of location => item array |
117 | * |
118 | * @return array An associative array keyed by location with each |
119 | * entry being an array with 'notes', 'summary' and 'items' keys. The 'notes' |
120 | * and 'summary' arrays are note/summary information collected from within the |
121 | * items. |
122 | */ |
123 | protected function formatHoldings($holdings) |
124 | { |
125 | $retVal = []; |
126 | |
127 | $textFieldNames = $this->catalog->getHoldingsTextFieldNames(); |
128 | |
129 | foreach ($holdings as $groupKey => $items) { |
130 | $retVal[$groupKey] = [ |
131 | 'items' => $items, |
132 | 'location' => $items[0]['location'] ?? '', |
133 | 'locationhref' => $items[0]['locationhref'] ?? '', |
134 | ]; |
135 | // Copy all text fields from the item to the holdings level |
136 | foreach ($items as $item) { |
137 | foreach ($textFieldNames as $fieldName) { |
138 | if (in_array($fieldName, ['notes', 'holdings_notes'])) { |
139 | if (empty($item[$fieldName])) { |
140 | // begin aliasing |
141 | if ( |
142 | $fieldName == 'notes' |
143 | && !empty($item['holdings_notes']) |
144 | ) { |
145 | // using notes as alias for holdings_notes |
146 | $item[$fieldName] = $item['holdings_notes']; |
147 | } elseif ( |
148 | $fieldName == 'holdings_notes' |
149 | && !empty($item['notes']) |
150 | ) { |
151 | // using holdings_notes as alias for notes |
152 | $item[$fieldName] = $item['notes']; |
153 | } |
154 | } |
155 | } |
156 | |
157 | if (!empty($item[$fieldName])) { |
158 | $targetRef = & $retVal[$groupKey]['textfields'][$fieldName]; |
159 | foreach ((array)$item[$fieldName] as $field) { |
160 | if (empty($targetRef) || !in_array($field, $targetRef)) { |
161 | $targetRef[] = $field; |
162 | } |
163 | } |
164 | } |
165 | } |
166 | |
167 | // Handle purchase history |
168 | if (!empty($item['purchase_history'])) { |
169 | $targetRef = & $retVal[$groupKey]['purchase_history']; |
170 | foreach ((array)$item['purchase_history'] as $field) { |
171 | if (empty($targetRef) || !in_array($field, $targetRef)) { |
172 | $targetRef[] = $field; |
173 | } |
174 | } |
175 | } |
176 | } |
177 | } |
178 | |
179 | return $retVal; |
180 | } |
181 | |
182 | /** |
183 | * Public method for getting item holdings from the catalog and selecting which |
184 | * holding method to call |
185 | * |
186 | * @param string $id A Bib ID |
187 | * @param array $ids A list of Source Records (if catalog is for a |
188 | * consortium) |
189 | * @param array $options Optional options to pass on to getHolding() |
190 | * |
191 | * @return array A sorted results set |
192 | */ |
193 | public function getHoldings($id, $ids = null, $options = []) |
194 | { |
195 | if (!$this->catalog) { |
196 | return []; |
197 | } |
198 | // Retrieve stored patron credentials; it is the responsibility of the |
199 | // controller and view to inform the user that these credentials are |
200 | // needed for hold data. |
201 | try { |
202 | $patron = $this->ilsAuth->storedCatalogLogin(); |
203 | |
204 | // Does this ILS Driver handle consortial holdings? |
205 | $config = $this->catalog->checkFunction( |
206 | 'Holds', |
207 | compact('id', 'patron') |
208 | ); |
209 | } catch (ILSException $e) { |
210 | $patron = false; |
211 | $config = []; |
212 | } |
213 | |
214 | if (isset($config['consortium']) && $config['consortium'] == true) { |
215 | $result = $this->catalog->getConsortialHoldings( |
216 | $id, |
217 | $patron ? $patron : null, |
218 | $ids |
219 | ); |
220 | } else { |
221 | $result = $this->catalog |
222 | ->getHolding($id, $patron ? $patron : null, $options); |
223 | } |
224 | |
225 | $grb = 'getRequestBlocks'; // use variable to shorten line below: |
226 | $blocks |
227 | = $patron && $this->catalog->checkCapability($grb, compact('patron')) |
228 | ? $this->catalog->getRequestBlocks($patron) : false; |
229 | |
230 | $mode = $this->catalog->getHoldsMode(); |
231 | |
232 | if ($mode == 'disabled') { |
233 | $holdings = $this->standardHoldings($result); |
234 | } elseif ($mode == 'driver') { |
235 | $holdings = $this->driverHoldings($result, $config, !empty($blocks)); |
236 | } else { |
237 | $holdings = $this->generateHoldings($result, $mode, $config); |
238 | } |
239 | |
240 | $holdings = $this->processStorageRetrievalRequests( |
241 | $holdings, |
242 | $id, |
243 | $patron, |
244 | !empty($blocks) |
245 | ); |
246 | $holdings = $this->processILLRequests( |
247 | $holdings, |
248 | $id, |
249 | $patron, |
250 | !empty($blocks) |
251 | ); |
252 | |
253 | $result['blocks'] = $blocks; |
254 | $result['holdings'] = $this->formatHoldings($holdings); |
255 | |
256 | return $result; |
257 | } |
258 | |
259 | /** |
260 | * Protected method for standard (i.e. No Holds) holdings |
261 | * |
262 | * @param array $result A result set returned from a driver |
263 | * |
264 | * @return array A sorted results set |
265 | */ |
266 | protected function standardHoldings($result) |
267 | { |
268 | $holdings = []; |
269 | if ($result['total']) { |
270 | foreach ($result['holdings'] as $copy) { |
271 | $show = !in_array($copy['location'], $this->hideHoldings); |
272 | if ($show) { |
273 | $groupKey = $this->getHoldingsGroupKey($copy); |
274 | $holdings[$groupKey][] = $copy; |
275 | } |
276 | } |
277 | } |
278 | return $holdings; |
279 | } |
280 | |
281 | /** |
282 | * Protected method for driver defined holdings |
283 | * |
284 | * @param array $result A result set returned from a driver |
285 | * @param array $holdConfig Hold configuration from driver |
286 | * @param bool $requestsBlocked Are user requests blocked? |
287 | * |
288 | * @return array A sorted results set |
289 | */ |
290 | protected function driverHoldings($result, $holdConfig, $requestsBlocked) |
291 | { |
292 | $holdings = []; |
293 | |
294 | if ($result['total']) { |
295 | foreach ($result['holdings'] as $copy) { |
296 | $show = !in_array($copy['location'], $this->hideHoldings); |
297 | if ($show) { |
298 | if ($holdConfig) { |
299 | // Is this copy holdable / linkable |
300 | if ( |
301 | !$requestsBlocked |
302 | && ($copy['addLink'] ?? false) |
303 | && ($copy['is_holdable'] ?? true) |
304 | ) { |
305 | $copy['link'] = $this->getRequestDetails( |
306 | $copy, |
307 | $holdConfig['HMACKeys'], |
308 | 'Hold' |
309 | ); |
310 | $copy['linkLightbox'] = true; |
311 | // If we are unsure whether hold options are available, |
312 | // set a flag so we can check later via AJAX: |
313 | $copy['check'] = $copy['addLink'] === 'check'; |
314 | } |
315 | } |
316 | |
317 | $groupKey = $this->getHoldingsGroupKey($copy); |
318 | $holdings[$groupKey][] = $copy; |
319 | } |
320 | } |
321 | } |
322 | return $holdings; |
323 | } |
324 | |
325 | /** |
326 | * Protected method for vufind (i.e. User) defined holdings |
327 | * |
328 | * @param array $result A result set returned from a driver |
329 | * @param string $type The holds mode to be applied from: |
330 | * (all, holds, recalls, availability) |
331 | * @param array $holdConfig Hold configuration from driver |
332 | * |
333 | * @return array A sorted results set |
334 | */ |
335 | protected function generateHoldings($result, $type, $holdConfig) |
336 | { |
337 | $holdings = []; |
338 | $any_available = false; |
339 | |
340 | $holds_override = $this->config->Catalog->allow_holds_override ?? false; |
341 | |
342 | if ($result['total']) { |
343 | foreach ($result['holdings'] as $copy) { |
344 | $show = !in_array($copy['location'], $this->hideHoldings); |
345 | if ($show) { |
346 | $groupKey = $this->getHoldingsGroupKey($copy); |
347 | $holdings[$groupKey][] = $copy; |
348 | // Are any copies available? |
349 | if ($copy['availability']->isAvailable()) { |
350 | $any_available = true; |
351 | } |
352 | } |
353 | } |
354 | |
355 | if ($holdConfig && is_array($holdings)) { |
356 | // Generate Links |
357 | // Loop through each holding |
358 | foreach ($holdings as $location_key => $location) { |
359 | foreach ($location as $copy_key => $copy) { |
360 | // Override the default hold behavior with a value from |
361 | // the ILS driver if allowed and applicable: |
362 | $currentType |
363 | = ($holds_override && isset($copy['holdOverride'])) |
364 | ? $copy['holdOverride'] : $type; |
365 | |
366 | switch ($currentType) { |
367 | case 'all': |
368 | $addlink = true; // always provide link |
369 | break; |
370 | case 'holds': |
371 | $addlink = $copy['availability']->isAvailable(); |
372 | break; |
373 | case 'recalls': |
374 | $addlink = !$copy['availability']->isAvailable(); |
375 | break; |
376 | case 'availability': |
377 | $addlink = !$copy['availability']->isAvailable() |
378 | && ($any_available == false); |
379 | break; |
380 | default: |
381 | $addlink = false; |
382 | break; |
383 | } |
384 | // If a valid holdable status has been set, use it to |
385 | // determine if a hold link is created |
386 | if ($addlink && ($copy['is_holdable'] ?? true)) { |
387 | if ($holdConfig['function'] == 'getHoldLink') { |
388 | /* Build opac link */ |
389 | $holdings[$location_key][$copy_key]['link'] |
390 | = $this->catalog->getHoldLink( |
391 | $copy['id'], |
392 | $copy |
393 | ); |
394 | $holdings[$location_key][$copy_key]['linkLightbox'] |
395 | = false; |
396 | } else { |
397 | /* Build non-opac link */ |
398 | $holdings[$location_key][$copy_key]['link'] |
399 | = $this->getRequestDetails( |
400 | $copy, |
401 | $holdConfig['HMACKeys'], |
402 | 'Hold' |
403 | ); |
404 | $holdings[$location_key][$copy_key]['linkLightbox'] |
405 | = true; |
406 | } |
407 | } |
408 | } |
409 | } |
410 | } |
411 | } |
412 | return $holdings; |
413 | } |
414 | |
415 | /** |
416 | * Process storage retrieval request information in holdings and set the links |
417 | * accordingly. |
418 | * |
419 | * @param array $holdings Holdings |
420 | * @param string $id Record ID |
421 | * @param array $patron Patron |
422 | * @param bool $requestsBlocked Are user requests blocked? |
423 | * |
424 | * @return array Modified holdings |
425 | */ |
426 | protected function processStorageRetrievalRequests( |
427 | $holdings, |
428 | $id, |
429 | $patron, |
430 | $requestsBlocked |
431 | ) { |
432 | if (!is_array($holdings)) { |
433 | return $holdings; |
434 | } |
435 | |
436 | // Are storage retrieval requests allowed? |
437 | $requestConfig = $this->catalog->checkFunction( |
438 | 'StorageRetrievalRequests', |
439 | compact('id', 'patron') |
440 | ); |
441 | |
442 | if (!$requestConfig) { |
443 | return $holdings; |
444 | } |
445 | |
446 | // Generate Links |
447 | // Loop through each holding |
448 | foreach ($holdings as &$location) { |
449 | foreach ($location as &$copy) { |
450 | // Is this copy requestable |
451 | if ( |
452 | !$requestsBlocked |
453 | && isset($copy['addStorageRetrievalRequestLink']) |
454 | && $copy['addStorageRetrievalRequestLink'] |
455 | ) { |
456 | $copy['storageRetrievalRequestLink'] = $this->getRequestDetails( |
457 | $copy, |
458 | $requestConfig['HMACKeys'], |
459 | 'StorageRetrievalRequest' |
460 | ); |
461 | // If we are unsure whether request options are |
462 | // available, set a flag so we can check later via AJAX: |
463 | $copy['checkStorageRetrievalRequest'] |
464 | = $copy['addStorageRetrievalRequestLink'] === 'check'; |
465 | } |
466 | } |
467 | } |
468 | return $holdings; |
469 | } |
470 | |
471 | /** |
472 | * Process ILL request information in holdings and set the links accordingly. |
473 | * |
474 | * @param array $holdings Holdings |
475 | * @param string $id Record ID |
476 | * @param array $patron Patron |
477 | * @param bool $requestsBlocked Are user requests blocked? |
478 | * |
479 | * @return array Modified holdings |
480 | */ |
481 | protected function processILLRequests($holdings, $id, $patron, $requestsBlocked) |
482 | { |
483 | if (!is_array($holdings)) { |
484 | return $holdings; |
485 | } |
486 | |
487 | // Are storage retrieval requests allowed? |
488 | $requestConfig = $this->catalog->checkFunction( |
489 | 'ILLRequests', |
490 | compact('id', 'patron') |
491 | ); |
492 | |
493 | if (!$requestConfig) { |
494 | return $holdings; |
495 | } |
496 | |
497 | // Generate Links |
498 | // Loop through each holding |
499 | foreach ($holdings as &$location) { |
500 | foreach ($location as &$copy) { |
501 | // Is this copy requestable |
502 | if ( |
503 | !$requestsBlocked && isset($copy['addILLRequestLink']) |
504 | && $copy['addILLRequestLink'] |
505 | ) { |
506 | $copy['ILLRequestLink'] = $this->getRequestDetails( |
507 | $copy, |
508 | $requestConfig['HMACKeys'], |
509 | 'ILLRequest' |
510 | ); |
511 | // If we are unsure whether request options are |
512 | // available, set a flag so we can check later via AJAX: |
513 | $copy['checkILLRequest'] |
514 | = $copy['addILLRequestLink'] === 'check'; |
515 | } |
516 | } |
517 | } |
518 | return $holdings; |
519 | } |
520 | |
521 | /** |
522 | * Get Hold Form |
523 | * |
524 | * Supplies holdLogic with the form details required to place a request |
525 | * |
526 | * @param array $details An array of item data |
527 | * @param array $HMACKeys An array of keys to hash |
528 | * @param string $action The action for which the details are built |
529 | * |
530 | * @return array Details for generating URL |
531 | */ |
532 | protected function getRequestDetails($details, $HMACKeys, $action) |
533 | { |
534 | // Include request type in the details |
535 | $details['requestType'] = $action; |
536 | |
537 | // Generate HMAC |
538 | $HMACkey = $this->hmac->generate($HMACKeys, $details); |
539 | |
540 | // Add Params |
541 | $queryString = []; |
542 | foreach ($details as $key => $param) { |
543 | $needle = in_array($key, $HMACKeys); |
544 | if ($needle) { |
545 | $queryString[] = $key . '=' . urlencode($param); |
546 | } |
547 | } |
548 | |
549 | // Add HMAC |
550 | $queryString[] = 'hashKey=' . urlencode($HMACkey); |
551 | $queryString = implode('&', $queryString); |
552 | |
553 | // Build Params |
554 | return [ |
555 | 'action' => $action, 'record' => $details['id'], |
556 | 'source' => $details['source'] ?? DEFAULT_SEARCH_BACKEND, |
557 | 'query' => $queryString, 'anchor' => '#tabnav', |
558 | ]; |
559 | } |
560 | |
561 | /** |
562 | * Get a grouping key for a holdings item |
563 | * |
564 | * @param array $copy Item information |
565 | * |
566 | * @return string Grouping key |
567 | */ |
568 | protected function getHoldingsGroupKey($copy) |
569 | { |
570 | // Group by holdings id and location unless configured otherwise |
571 | $grouping = $this->config->Catalog->holdings_grouping |
572 | ?? 'holdings_id,location'; |
573 | |
574 | $groupKey = ''; |
575 | |
576 | // Multiple keys may be used here (delimited by comma) |
577 | foreach (array_map('trim', explode(',', $grouping)) as $key) { |
578 | // backwards-compatibility: |
579 | // The config.ini file originally expected only |
580 | // two possible settings: holdings_id and location_name. |
581 | // However, when location_name was set, the code actually |
582 | // used the value of 'location' instead. |
583 | // From now on, we will expect (via config.ini documentation) |
584 | // the value of 'location', but still continue to honor |
585 | // 'location_name'. |
586 | if ($key == 'location_name') { |
587 | $key = 'location'; |
588 | } |
589 | |
590 | if (isset($copy[$key])) { |
591 | if ($groupKey != '') { |
592 | $groupKey .= '|'; |
593 | } |
594 | $groupKey .= $copy[$key]; |
595 | } |
596 | } |
597 | |
598 | // default: |
599 | if ($groupKey == '') { |
600 | $groupKey = $copy['location']; |
601 | } |
602 | |
603 | return $groupKey; |
604 | } |
605 | |
606 | /** |
607 | * Get an array of suppressed location names. |
608 | * |
609 | * @return array |
610 | */ |
611 | public function getSuppressedLocations() |
612 | { |
613 | return $this->hideHoldings; |
614 | } |
615 | } |