Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 224
0.00% covered (danger)
0.00%
0 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
Holds
0.00% covered (danger)
0.00%
0 / 224
0.00% covered (danger)
0.00%
0 / 11
8190
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 formatHoldings
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
342
 getHoldings
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
132
 standardHoldings
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 driverHoldings
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 generateHoldings
0.00% covered (danger)
0.00%
0 / 49
0.00% covered (danger)
0.00%
0 / 1
420
 processStorageRetrievalRequests
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
72
 processILLRequests
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
 getRequestDetails
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 getHoldingsGroupKey
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 getSuppressedLocations
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
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
31namespace VuFind\ILS\Logic;
32
33use VuFind\Exception\ILS as ILSException;
34use VuFind\ILS\Connection as ILSConnection;
35
36use function in_array;
37use 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 */
49class 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}