Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
14.07% covered (danger)
14.07%
46 / 327
15.00% covered (danger)
15.00%
6 / 40
CRAP
0.00% covered (danger)
0.00%
0 / 1
Connection
14.07% covered (danger)
14.07%
46 / 327
15.00% covered (danger)
15.00%
6 / 40
15007.52
0.00% covered (danger)
0.00%
0 / 1
 __construct
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
 setHoldConfig
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getDriverClass
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 initializeDriver
42.86% covered (danger)
42.86%
3 / 7
0.00% covered (danger)
0.00%
0 / 1
4.68
 hasNoILSFailover
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 failOverToNoILS
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 getDriver
50.00% covered (danger)
50.00%
4 / 8
0.00% covered (danger)
0.00%
0 / 1
10.50
 setDriver
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getDriverConfig
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 checkFunction
53.33% covered (warning)
53.33%
8 / 15
0.00% covered (danger)
0.00%
0 / 1
5.63
 checkMethodHolds
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
110
 checkMethodcancelHolds
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
90
 checkMethodRenewals
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
90
 checkMethodStorageRetrievalRequests
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 checkMethodcancelStorageRetrievalRequests
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
 checkMethodILLRequests
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 checkMethodcancelILLRequests
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
 checkMethodchangePassword
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 checkMethodgetMyTransactions
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 checkMethodgetMyTransactionHistory
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 checkMethodpurgeTransactionHistory
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 checkMethodpatronLogin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHelpText
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 checkRequestIsValid
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 checkStorageRetrievalRequestIsValid
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 checkILLRequestIsValid
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getHoldsMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getOfflineMode
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
30
 getTitleHoldsMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasHoldings
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 loginIsHidden
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 checkCapability
46.15% covered (danger)
46.15%
6 / 13
0.00% covered (danger)
0.00%
0 / 1
8.90
 getHoldingsTextFieldNames
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getPasswordPolicy
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 getMyTransactions
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getHolding
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
132
 getStatus
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getStatuses
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getStatusParser
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 __call
41.67% covered (danger)
41.67%
5 / 12
0.00% covered (danger)
0.00%
0 / 1
7.18
1<?php
2
3/**
4 * Catalog Connection Class
5 *
6 * This wrapper works with a driver class to pass information from the ILS to
7 * VuFind.
8 *
9 * PHP version 8
10 *
11 * Copyright (C) Villanova University 2007.
12 *
13 * This program is free software; you can redistribute it and/or modify
14 * it under the terms of the GNU General Public License version 2,
15 * as published by the Free Software Foundation.
16 *
17 * This program is distributed in the hope that it will be useful,
18 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 * GNU General Public License for more details.
21 *
22 * You should have received a copy of the GNU General Public License
23 * along with this program; if not, write to the Free Software
24 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
25 *
26 * @category VuFind
27 * @package  ILS_Drivers
28 * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
29 * @author   Demian Katz <demian.katz@villanova.edu>
30 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
31 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
32 */
33
34namespace VuFind\ILS;
35
36use Laminas\Log\LoggerAwareInterface;
37use VuFind\Exception\BadConfig;
38use VuFind\Exception\ILS as ILSException;
39use VuFind\I18n\Translator\TranslatorAwareInterface;
40use VuFind\ILS\Driver\DriverInterface;
41use VuFind\ILS\Logic\AvailabilityStatus;
42
43use function call_user_func_array;
44use function count;
45use function func_get_args;
46use function get_class;
47use function intval;
48use function is_array;
49use function is_callable;
50use function is_object;
51
52/**
53 * Catalog Connection Class
54 *
55 * This wrapper works with a driver class to pass information from the ILS to
56 * VuFind.
57 *
58 * @category VuFind
59 * @package  ILS_Drivers
60 * @author   Andrew S. Nagy <vufind-tech@lists.sourceforge.net>
61 * @author   Demian Katz <demian.katz@villanova.edu>
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 */
65class Connection implements TranslatorAwareInterface, LoggerAwareInterface
66{
67    use \VuFind\I18n\Translator\TranslatorAwareTrait;
68    use \VuFind\Log\LoggerAwareTrait;
69
70    /**
71     * Has the driver been initialized yet?
72     *
73     * @var bool
74     */
75    protected $driverInitialized = false;
76
77    /**
78     * The object of the appropriate driver.
79     *
80     * @var object
81     */
82    protected $driver = null;
83
84    /**
85     * ILS configuration
86     *
87     * @var \Laminas\Config\Config
88     */
89    protected $config;
90
91    /**
92     * Holds mode
93     *
94     * @var string
95     */
96    protected $holdsMode = 'disabled';
97
98    /**
99     * Title-level holds mode
100     *
101     * @var string
102     */
103    protected $titleHoldsMode = 'disabled';
104
105    /**
106     * Driver plugin manager
107     *
108     * @var \VuFind\ILS\Driver\PluginManager
109     */
110    protected $driverManager;
111
112    /**
113     * Configuration loader
114     *
115     * @var \VuFind\Config\PluginManager
116     */
117    protected $configReader;
118
119    /**
120     * Is the current ILS driver failing?
121     *
122     * @var bool
123     */
124    protected $failing = false;
125
126    /**
127     * Request object
128     *
129     * @var \Laminas\Http\Request
130     */
131    protected $request;
132
133    /**
134     * Constructor
135     *
136     * @param \Laminas\Config\Config           $config        Configuration
137     * representing the [Catalog] section of config.ini
138     * @param \VuFind\ILS\Driver\PluginManager $driverManager Driver plugin manager
139     * @param \VuFind\Config\PluginManager     $configReader  Configuration loader
140     * @param \Laminas\Http\Request            $request       Request object
141     */
142    public function __construct(
143        \Laminas\Config\Config $config,
144        \VuFind\ILS\Driver\PluginManager $driverManager,
145        \VuFind\Config\PluginManager $configReader,
146        \Laminas\Http\Request $request = null
147    ) {
148        if (!isset($config->driver)) {
149            throw new \Exception('ILS driver setting missing.');
150        }
151        if (!$driverManager->has($config->driver)) {
152            throw new \Exception('ILS driver missing: ' . $config->driver);
153        }
154        $this->config = $config;
155        $this->configReader = $configReader;
156        $this->driverManager = $driverManager;
157        $this->request = $request;
158    }
159
160    /**
161     * Set the hold configuration for the connection.
162     *
163     * @param \VuFind\ILS\HoldSettings $settings Hold settings
164     *
165     * @return Connection
166     */
167    public function setHoldConfig($settings)
168    {
169        $this->holdsMode = $settings->getHoldsMode();
170        $this->titleHoldsMode = $settings->getTitleHoldsMode();
171        return $this;
172    }
173
174    /**
175     * Get class name of the driver object.
176     *
177     * @return string
178     */
179    public function getDriverClass()
180    {
181        return get_class($this->getDriver(false));
182    }
183
184    /**
185     * Initialize the ILS driver.
186     *
187     * @return void
188     */
189    protected function initializeDriver()
190    {
191        try {
192            $this->driver->setConfig($this->getDriverConfig());
193        } catch (\Exception $e) {
194            // Any errors thrown during configuration should be cast to BadConfig
195            // so we can handle them differently from other runtime problems.
196            throw $e instanceof BadConfig
197                ? $e
198                : new BadConfig('Failure during configuration.', 0, $e);
199        }
200        $this->driver->init();
201        $this->driverInitialized = true;
202    }
203
204    /**
205     * Are we configured to fail over to the NoILS driver on error?
206     *
207     * @return bool
208     */
209    protected function hasNoILSFailover()
210    {
211        // If we're configured to fail over to the NoILS driver, do so now:
212        return isset($this->config->loadNoILSOnFailure)
213            && $this->config->loadNoILSOnFailure;
214    }
215
216    /**
217     * If configured, fail over to the NoILS driver and return true; otherwise,
218     * return false.
219     *
220     * @param \Exception $e The exception that triggered the failover.
221     *
222     * @return bool
223     */
224    protected function failOverToNoILS(\Exception $e = null)
225    {
226        // If the exception is caused by a configuration error, the administrator
227        // needs to fix it, but failing over to NoILS will mask the error and cause
228        // confusion. We shouldn't do that!
229        if ($e instanceof BadConfig) {
230            return false;
231        }
232
233        // If we got this far, we want to proceed with failover...
234        $this->failing = true;
235
236        // Only fail over if we're configured to allow it and we haven't already
237        // done so!
238        if ($this->hasNoILSFailover()) {
239            $noILS = $this->driverManager->get('NoILS');
240            if ($noILS::class != $this->getDriverClass()) {
241                $this->setDriver($noILS);
242                $this->initializeDriver();
243                return true;
244            }
245        }
246        return false;
247    }
248
249    /**
250     * Get access to the driver object.
251     *
252     * @param bool $init Should we initialize the driver (if necessary), or load it
253     * "as-is"?
254     *
255     * @throws \Exception
256     * @return object
257     */
258    public function getDriver($init = true)
259    {
260        if (null === $this->driver) {
261            $this->setDriver($this->driverManager->get($this->config->driver));
262        }
263        if (!$this->driverInitialized && $init) {
264            try {
265                $this->initializeDriver();
266            } catch (\Exception $e) {
267                if (!$this->failOverToNoILS($e)) {
268                    throw $e;
269                }
270            }
271        }
272        return $this->driver;
273    }
274
275    /**
276     * Set a driver object.
277     *
278     * @param DriverInterface $driver      Driver to set.
279     * @param bool            $initialized Is this driver already initialized?
280     *
281     * @return void
282     */
283    public function setDriver(DriverInterface $driver, $initialized = false)
284    {
285        $this->driverInitialized = $initialized;
286        $this->driver = $driver;
287    }
288
289    /**
290     * Get configuration for the ILS driver. We will load an .ini file named
291     * after the driver class if it exists; otherwise we will return an empty
292     * array.
293     *
294     * @return array
295     */
296    public function getDriverConfig()
297    {
298        // Determine config file name based on class name:
299        $parts = explode('\\', $this->getDriverClass());
300        $config = $this->configReader->get(end($parts));
301        return is_object($config) ? $config->toArray() : [];
302    }
303
304    /**
305     * Check Function
306     *
307     * This is responsible for checking the driver configuration to determine
308     * if the system supports a particular function.
309     *
310     * @param string $function The name of the function to check.
311     * @param array  $params   (optional) An array of function-specific parameters
312     *
313     * @return mixed On success, an associative array with specific function keys
314     * and values; on failure, false.
315     */
316    public function checkFunction($function, $params = null)
317    {
318        try {
319            // Extract the configuration from the driver if available:
320            $functionConfig = $this->checkCapability(
321                'getConfig',
322                [$function, $params],
323                true
324            ) ? $this->getDriver()->getConfig($function, $params) : false;
325
326            // See if we have a corresponding check method to analyze the response:
327            $checkMethod = 'checkMethod' . $function;
328            if (!method_exists($this, $checkMethod)) {
329                return false;
330            }
331
332            // Send back the settings:
333            return $this->$checkMethod($functionConfig, $params);
334        } catch (ILSException $e) {
335            $this->logError(
336                "checkFunction($function) with params: " . $this->varDump($params)
337                . ' failed: ' . $e->getMessage()
338            );
339            return false;
340        }
341    }
342
343    /**
344     * Check Holds
345     *
346     * A support method for checkFunction(). This is responsible for checking
347     * the driver configuration to determine if the system supports Holds.
348     *
349     * @param array $functionConfig The Hold configuration values
350     * @param array $params         An array of function-specific params (or null)
351     *
352     * @return mixed On success, an associative array with specific function keys
353     * and values either for placing holds via a form or a URL; on failure, false.
354     */
355    protected function checkMethodHolds($functionConfig, $params)
356    {
357        $response = false;
358
359        // We pass an array containing $params to checkCapability since $params
360        // should contain 'id' and 'patron' keys; this isn't exactly the same as
361        // the full parameter expected by placeHold() but should contain the
362        // necessary details for determining eligibility.
363        if (
364            $this->getHoldsMode() != 'none'
365            && $this->checkCapability('placeHold', [$params ?: []])
366            && isset($functionConfig['HMACKeys'])
367        ) {
368            $response = ['function' => 'placeHold'];
369            $response['HMACKeys'] = explode(':', $functionConfig['HMACKeys']);
370            if (isset($functionConfig['defaultRequiredDate'])) {
371                $response['defaultRequiredDate']
372                    = $functionConfig['defaultRequiredDate'];
373            }
374            if (isset($functionConfig['extraHoldFields'])) {
375                $response['extraHoldFields'] = $functionConfig['extraHoldFields'];
376            }
377            if (!empty($functionConfig['updateFields'])) {
378                $response['updateFields'] = array_map(
379                    'trim',
380                    explode(':', $functionConfig['updateFields'])
381                );
382            }
383            $response['helpText']
384                = $this->getHelpText($functionConfig['helpText'] ?? '');
385            $response['updateHelpText']
386                = $this->getHelpText($functionConfig['updateHelpText'] ?? '');
387            if (isset($functionConfig['consortium'])) {
388                $response['consortium'] = $functionConfig['consortium'];
389            }
390            $response['pickUpLocationCheckLimit']
391                = intval($functionConfig['pickUpLocationCheckLimit'] ?? 0);
392        } else {
393            $id = $params['id'] ?? null;
394            if ($this->checkCapability('getHoldLink', [$id, []])) {
395                $response = ['function' => 'getHoldLink'];
396            }
397        }
398        return $response;
399    }
400
401    /**
402     * Check Cancel Holds
403     *
404     * A support method for checkFunction(). This is responsible for checking
405     * the driver configuration to determine if the system supports Cancelling Holds.
406     *
407     * @param array $functionConfig The Cancel Hold configuration values
408     * @param array $params         An array of function-specific params (or null)
409     *
410     * @return mixed On success, an associative array with specific function keys
411     * and values either for cancelling holds via a form or a URL;
412     * on failure, false.
413     *
414     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
415     */
416    protected function checkMethodcancelHolds($functionConfig, $params)
417    {
418        $response = false;
419
420        // We can't pass exactly accurate parameters to checkCapability in this
421        // context, so we'll just pass along $params as the best available
422        // approximation.
423        if (
424            isset($this->config->cancel_holds_enabled)
425            && $this->config->cancel_holds_enabled == true
426            && $this->checkCapability('cancelHolds', [$params ?: []])
427        ) {
428            $response = ['function' => 'cancelHolds'];
429        } elseif (
430            isset($this->config->cancel_holds_enabled)
431            && $this->config->cancel_holds_enabled == true
432            && $this->checkCapability('getCancelHoldLink', [$params ?: []])
433        ) {
434            $response = ['function' => 'getCancelHoldLink'];
435        }
436        return $response;
437    }
438
439    /**
440     * Check Renewals
441     *
442     * A support method for checkFunction(). This is responsible for checking
443     * the driver configuration to determine if the system supports Renewing Items.
444     *
445     * @param array $functionConfig The Renewal configuration values
446     * @param array $params         An array of function-specific params (or null)
447     *
448     * @return mixed On success, an associative array with specific function keys
449     * and values either for renewing items via a form or a URL; on failure, false.
450     *
451     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
452     */
453    protected function checkMethodRenewals($functionConfig, $params)
454    {
455        $response = false;
456
457        // We can't pass exactly accurate parameters to checkCapability in this
458        // context, so we'll just pass along $params as the best available
459        // approximation.
460        if (
461            isset($this->config->renewals_enabled)
462            && $this->config->renewals_enabled == true
463            && $this->checkCapability('renewMyItems', [$params ?: []])
464        ) {
465            $response = ['function' => 'renewMyItems'];
466        } elseif (
467            isset($this->config->renewals_enabled)
468            && $this->config->renewals_enabled == true
469            && $this->checkCapability('renewMyItemsLink', [$params ?: []])
470        ) {
471            $response = ['function' => 'renewMyItemsLink'];
472        }
473        return $response;
474    }
475
476    /**
477     * Check Storage Retrieval Request
478     *
479     * A support method for checkFunction(). This is responsible for checking
480     * the driver configuration to determine if the system supports storage
481     * retrieval requests.
482     *
483     * @param array $functionConfig The storage retrieval request configuration
484     * values
485     * @param array $params         An array of function-specific params (or null)
486     *
487     * @return mixed On success, an associative array with specific function keys
488     * and values either for placing requests via a form; on failure, false.
489     */
490    protected function checkMethodStorageRetrievalRequests($functionConfig, $params)
491    {
492        $response = false;
493
494        // $params doesn't include all of the keys used by
495        // placeStorageRetrievalRequest, but it is the best we can do in the context.
496        $check = $this->checkCapability(
497            'placeStorageRetrievalRequest',
498            [$params ?: []]
499        );
500        if ($check && isset($functionConfig['HMACKeys'])) {
501            $response = ['function' => 'placeStorageRetrievalRequest'];
502            $response['HMACKeys'] = explode(':', $functionConfig['HMACKeys']);
503            if (isset($functionConfig['extraFields'])) {
504                $response['extraFields'] = $functionConfig['extraFields'];
505            }
506            $response['helpText']
507                = $this->getHelpText($functionConfig['helpText'] ?? '');
508        }
509        return $response;
510    }
511
512    /**
513     * Check Cancel Storage Retrieval Requests
514     *
515     * A support method for checkFunction(). This is responsible for checking
516     * the driver configuration to determine if the system supports Cancelling
517     * Storage Retrieval Requests.
518     *
519     * @param array $functionConfig The Cancel function configuration values
520     * @param array $params         An array of function-specific params (or null)
521     *
522     * @return mixed On success, an associative array with specific function keys
523     * and values either for cancelling requests via a form or a URL;
524     * on failure, false.
525     *
526     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
527     */
528    protected function checkMethodcancelStorageRetrievalRequests(
529        $functionConfig,
530        $params
531    ) {
532        $response = false;
533
534        if (
535            isset($this->config->cancel_storage_retrieval_requests_enabled)
536            && $this->config->cancel_storage_retrieval_requests_enabled
537        ) {
538            $check = $this->checkCapability(
539                'cancelStorageRetrievalRequests',
540                [$params ?: []]
541            );
542            if ($check) {
543                $response = ['function' => 'cancelStorageRetrievalRequests'];
544            } else {
545                $cancelParams = [
546                    $params ?: [],
547                    $params['patron'] ?? null,
548                ];
549                $check2 = $this->checkCapability(
550                    'getCancelStorageRetrievalRequestLink',
551                    $cancelParams
552                );
553                if ($check2) {
554                    $response = [
555                        'function' => 'getCancelStorageRetrievalRequestLink',
556                    ];
557                }
558            }
559        }
560        return $response;
561    }
562
563    /**
564     * Check ILL Request
565     *
566     * A support method for checkFunction(). This is responsible for checking
567     * the driver configuration to determine if the system supports storage
568     * retrieval requests.
569     *
570     * @param array $functionConfig The ILL request configuration values
571     * @param array $params         An array of function-specific params (or null)
572     *
573     * @return mixed On success, an associative array with specific function keys
574     * and values either for placing requests via a form; on failure, false.
575     */
576    protected function checkMethodILLRequests($functionConfig, $params)
577    {
578        $response = false;
579
580        // $params doesn't include all of the keys used by
581        // placeILLRequest, but it is the best we can do in the context.
582        if (
583            $this->checkCapability('placeILLRequest', [$params ?: []])
584            && isset($functionConfig['HMACKeys'])
585        ) {
586            $response = ['function' => 'placeILLRequest'];
587            if (isset($functionConfig['defaultRequiredDate'])) {
588                $response['defaultRequiredDate']
589                    = $functionConfig['defaultRequiredDate'];
590            }
591            $response['HMACKeys'] = explode(':', $functionConfig['HMACKeys']);
592            if (isset($functionConfig['extraFields'])) {
593                $response['extraFields'] = $functionConfig['extraFields'];
594            }
595            $response['helpText']
596                = $this->getHelpText($functionConfig['helpText']);
597        }
598        return $response;
599    }
600
601    /**
602     * Check Cancel ILL Requests
603     *
604     * A support method for checkFunction(). This is responsible for checking
605     * the driver configuration to determine if the system supports Cancelling
606     * ILL Requests.
607     *
608     * @param array $functionConfig The Cancel function configuration values
609     * @param array $params         An array of function-specific params (or null)
610     *
611     * @return mixed On success, an associative array with specific function keys
612     * and values either for cancelling requests via a form or a URL;
613     * on failure, false.
614     *
615     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
616     */
617    protected function checkMethodcancelILLRequests($functionConfig, $params)
618    {
619        $response = false;
620
621        if (
622            isset($this->config->cancel_ill_requests_enabled)
623            && $this->config->cancel_ill_requests_enabled
624        ) {
625            $check = $this->checkCapability(
626                'cancelILLRequests',
627                [$params ?: []]
628            );
629            if ($check) {
630                $response = ['function' => 'cancelILLRequests'];
631            } else {
632                $cancelParams = [
633                    $params ?: [],
634                    $params['patron'] ?? null,
635                ];
636                $check2 = $this->checkCapability(
637                    'getCancelILLRequestLink',
638                    $cancelParams
639                );
640                if ($check2) {
641                    $response = [
642                        'function' => 'getCancelILLRequestLink',
643                    ];
644                }
645            }
646        }
647        return $response;
648    }
649
650    /**
651     * Check Password Change
652     *
653     * A support method for checkFunction(). This is responsible for checking
654     * the driver configuration to determine if the system supports changing
655     * password.
656     *
657     * @param array $functionConfig The password change configuration values
658     * @param array $params         Patron data
659     *
660     * @return mixed On success, an associative array with specific function keys
661     * and values either for cancelling requests via a form or a URL;
662     * on failure, false.
663     *
664     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
665     */
666    protected function checkMethodchangePassword($functionConfig, $params)
667    {
668        if ($this->checkCapability('changePassword', [$params ?: []])) {
669            return ['function' => 'changePassword'];
670        }
671        return false;
672    }
673
674    /**
675     * Check Current Loans
676     *
677     * A support method for checkFunction(). This is responsible for checking
678     * the driver configuration to determine if the system supports current
679     * loans.
680     *
681     * @param array $functionConfig Function configuration
682     * @param array $params         Patron data
683     *
684     * @return mixed On success, an associative array with specific function keys
685     * and values; on failure, false.
686     *
687     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
688     */
689    protected function checkMethodgetMyTransactions($functionConfig, $params)
690    {
691        if ($this->checkCapability('getMyTransactions', [$params ?: []])) {
692            return $functionConfig;
693        }
694        return false;
695    }
696
697    /**
698     * Check Historic Loans
699     *
700     * A support method for checkFunction(). This is responsible for checking
701     * the driver configuration to determine if the system supports historic
702     * loans.
703     *
704     * @param array $functionConfig Function configuration
705     * @param array $params         Patron data
706     *
707     * @return mixed On success, an associative array with specific function keys
708     * and values; on failure, false.
709     *
710     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
711     */
712    protected function checkMethodgetMyTransactionHistory($functionConfig, $params)
713    {
714        if ($this->checkCapability('getMyTransactionHistory', [$params ?: []])) {
715            return $functionConfig;
716        }
717        return false;
718    }
719
720    /**
721     * Check Purge Historic Loans
722     *
723     * A support method for checkFunction(). This is responsible for checking
724     * the driver configuration to determine if the system supports purging of
725     * historic loans.
726     *
727     * @param array $functionConfig Function configuration
728     * @param array $params         Patron data
729     *
730     * @return mixed On success, an associative array with specific function keys
731     * and values; on failure, false.
732     *
733     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
734     */
735    protected function checkMethodpurgeTransactionHistory($functionConfig, $params)
736    {
737        if ($this->checkCapability('purgeTransactionHistory', [$params ?: []])) {
738            return $functionConfig;
739        }
740        return false;
741    }
742
743    /**
744     * Check Patron login
745     *
746     * A support method for checkFunction(). This is responsible for checking
747     * the driver configuration to determine if the system supports patron login.
748     * It is currently assumed that all drivers do.
749     *
750     * @param array $functionConfig The patronLogin configuration values
751     * @param array $params         An array of function-specific params (or null)
752     *
753     * @return mixed On success, an associative array with specific function keys
754     * and values for login; on failure, false.
755     */
756    protected function checkMethodpatronLogin($functionConfig, $params)
757    {
758        return $functionConfig;
759    }
760
761    /**
762     * Get proper help text from the function config
763     *
764     * @param string|array $helpText Help text(s)
765     *
766     * @return string Language-specific help text
767     */
768    protected function getHelpText($helpText)
769    {
770        if (is_array($helpText)) {
771            $lang = $this->getTranslatorLocale();
772            return $helpText[$lang] ?? $helpText['*'] ?? '';
773        }
774        return $helpText;
775    }
776
777    /**
778     * Check Request is Valid
779     *
780     * This is responsible for checking if a request is valid from hold.php
781     *
782     * @param string $id     A Bibliographic ID
783     * @param array  $data   Collected Holds Data
784     * @param array  $patron Patron related data
785     *
786     * @return mixed The result of the checkRequestIsValid function if it
787     * exists, true if it does not
788     */
789    public function checkRequestIsValid($id, $data, $patron)
790    {
791        try {
792            $params = [$id, $data, $patron];
793            if ($this->checkCapability('checkRequestIsValid', $params)) {
794                return $this->getDriver()->checkRequestIsValid($id, $data, $patron);
795            }
796        } catch (\Exception $e) {
797            if ($this->failOverToNoILS($e)) {
798                return call_user_func_array([$this, __METHOD__], func_get_args());
799            }
800            throw $e;
801        }
802        // If the driver has no checkRequestIsValid method, we will assume that
803        // all requests are valid - failure can be handled later after the user
804        // attempts to place an illegal hold
805        return true;
806    }
807
808    /**
809     * Check Storage Retrieval Request is Valid
810     *
811     * This is responsible for checking if a storage retrieval request is valid
812     *
813     * @param string $id     A Bibliographic ID
814     * @param array  $data   Collected Holds Data
815     * @param array  $patron Patron related data
816     *
817     * @return mixed The result of the checkStorageRetrievalRequestIsValid
818     * function if it exists, false if it does not
819     */
820    public function checkStorageRetrievalRequestIsValid($id, $data, $patron)
821    {
822        try {
823            $check = $this->checkCapability(
824                'checkStorageRetrievalRequestIsValid',
825                [$id, $data, $patron]
826            );
827            if ($check) {
828                return $this->getDriver()->checkStorageRetrievalRequestIsValid(
829                    $id,
830                    $data,
831                    $patron
832                );
833            }
834        } catch (\Exception $e) {
835            if ($this->failOverToNoILS($e)) {
836                return call_user_func_array([$this, __METHOD__], func_get_args());
837            }
838            throw $e;
839        }
840        // If the driver has no checkStorageRetrievalRequestIsValid method, we
841        // will assume that the request is not valid
842        return false;
843    }
844
845    /**
846     * Check ILL Request is Valid
847     *
848     * This is responsible for checking if an ILL request is valid
849     *
850     * @param string $id     A Bibliographic ID
851     * @param array  $data   Collected Holds Data
852     * @param array  $patron Patron related data
853     *
854     * @return mixed The result of the checkILLRequestIsValid
855     * function if it exists, false if it does not
856     */
857    public function checkILLRequestIsValid($id, $data, $patron)
858    {
859        try {
860            $params = [$id, $data, $patron];
861            if ($this->checkCapability('checkILLRequestIsValid', $params)) {
862                return $this->getDriver()->checkILLRequestIsValid(
863                    $id,
864                    $data,
865                    $patron
866                );
867            }
868        } catch (\Exception $e) {
869            if ($this->failOverToNoILS($e)) {
870                return call_user_func_array([$this, __METHOD__], func_get_args());
871            }
872            throw $e;
873        }
874        // If the driver has no checkILLRequestIsValid method, we
875        // will assume that the request is not valid
876        return false;
877    }
878
879    /**
880     * Get Holds Mode
881     *
882     * This is responsible for returning the holds mode
883     *
884     * @return string The Holds mode
885     */
886    public function getHoldsMode()
887    {
888        return $this->holdsMode;
889    }
890
891    /**
892     * Get Offline Mode
893     *
894     * This is responsible for returning the offline mode
895     *
896     * @param bool $healthCheck Perform a health check in addition to consulting
897     * the ILS status?
898     *
899     * @return string|bool "ils-offline" for systems where the main ILS is offline,
900     * "ils-none" for systems which do not use an ILS, false for online systems.
901     */
902    public function getOfflineMode($healthCheck = false)
903    {
904        // If we have NoILS failover configured, force driver initialization so
905        // we can know we are checking the offline mode against the correct driver.
906        if ($this->hasNoILSFailover()) {
907            $this->getDriver();
908        }
909
910        // If we need to perform a health check, try to do a random item lookup
911        // before proceeding.
912        if ($healthCheck) {
913            $this->getStatus($this->config->healthCheckId ?? '1');
914        }
915
916        // If we're encountering failures, let's go into ils-offline mode if
917        // the ILS driver does not natively support getOfflineMode().
918        $default = $this->failing ? 'ils-offline' : false;
919
920        // Graceful degradation -- return false if no method supported.
921        return $this->checkCapability('getOfflineMode')
922            ? $this->getDriver()->getOfflineMode() : $default;
923    }
924
925    /**
926     * Get Title Holds Mode
927     *
928     * This is responsible for returning the Title holds mode
929     *
930     * @return string The Title Holds mode
931     */
932    public function getTitleHoldsMode()
933    {
934        return $this->titleHoldsMode;
935    }
936
937    /**
938     * Has Holdings
939     *
940     * Obtain information on whether or not the item has holdings
941     *
942     * @param string $id A bibliographic id
943     *
944     * @return bool true on success, false on failure
945     */
946    public function hasHoldings($id)
947    {
948        // Graceful degradation -- return true if no method supported.
949        try {
950            return $this->checkCapability('hasHoldings', [$id])
951                ? $this->getDriver()->hasHoldings($id) : true;
952        } catch (\Exception $e) {
953            if ($this->failOverToNoILS($e)) {
954                return call_user_func_array([$this, __METHOD__], func_get_args());
955            }
956            throw $e;
957        }
958    }
959
960    /**
961     * Get Hidden Login Mode
962     *
963     * This is responsible for indicating whether login should be hidden.
964     *
965     * @return bool true if the login should be hidden, false if not
966     */
967    public function loginIsHidden()
968    {
969        // Graceful degradation -- return false if no method supported.
970        try {
971            return $this->checkCapability('loginIsHidden')
972                ? $this->getDriver()->loginIsHidden() : false;
973        } catch (\Exception $e) {
974            if ($this->failOverToNoILS($e)) {
975                return call_user_func_array([$this, __METHOD__], func_get_args());
976            }
977            throw $e;
978        }
979    }
980
981    /**
982     * Check driver capability -- return true if the driver supports the specified
983     * method; false otherwise.
984     *
985     * @param string $method Method to check
986     * @param array  $params Array of passed parameters (optional)
987     * @param bool   $throw  Whether to throw exceptions instead of returning false
988     *
989     * @return bool
990     * @throws ILSException
991     */
992    public function checkCapability($method, $params = [], $throw = false)
993    {
994        try {
995            // If we have NoILS failover disabled, we can check capabilities of
996            // the driver class without wasting time initializing it; if NoILS
997            // failover is enabled, we have to initialize the driver object now
998            // to be sure we are checking capabilities on the appropriate class.
999            $driverToCheck = $this->getDriver($this->hasNoILSFailover());
1000
1001            // First check that the function is callable:
1002            if (is_callable([$driverToCheck, $method])) {
1003                // At least drivers implementing the __call() magic method must also
1004                // implement supportsMethod() to verify that the method is actually
1005                // usable:
1006                if (method_exists($driverToCheck, 'supportsMethod')) {
1007                    return $this->getDriver()->supportsMethod($method, $params);
1008                }
1009                return true;
1010            }
1011        } catch (ILSException $e) {
1012            $this->logError(
1013                "checkCapability($method) with params: " . $this->varDump($params)
1014                . ' failed: ' . $e->getMessage()
1015            );
1016            if ($throw) {
1017                throw $e;
1018            }
1019        }
1020
1021        // If we got this far, the feature is unsupported:
1022        return false;
1023    }
1024
1025    /**
1026     * Get Names of Textual Holdings Fields
1027     *
1028     * Obtain information on which textual holdings fields should be displayed
1029     *
1030     * @return string[]
1031     */
1032    public function getHoldingsTextFieldNames()
1033    {
1034        return isset($this->config->holdings_text_fields)
1035            ? $this->config->holdings_text_fields->toArray()
1036            : ['holdings_notes', 'summary', 'supplements', 'indexes'];
1037    }
1038
1039    /**
1040     * Get the password policy from the driver
1041     *
1042     * @param array $patron Patron data
1043     *
1044     * @return bool|array Password policy array or false if unsupported
1045     */
1046    public function getPasswordPolicy($patron)
1047    {
1048        return $this->checkCapability(
1049            'getConfig',
1050            ['changePassword', compact('patron')]
1051        ) ? $this->getDriver()->getConfig('changePassword', compact('patron'))
1052            : false;
1053    }
1054
1055    /**
1056     * Get Patron Transactions
1057     *
1058     * This is responsible for retrieving all transactions (i.e. checked out items)
1059     * by a specific patron.
1060     *
1061     * @param array $patron The patron array from patronLogin
1062     * @param array $params Parameters
1063     *
1064     * @return mixed        Array of the patron's transactions
1065     */
1066    public function getMyTransactions($patron, $params = [])
1067    {
1068        $result = $this->__call('getMyTransactions', [$patron, $params]);
1069
1070        // Support also older driver return value:
1071        if (!isset($result['count'])) {
1072            $result = [
1073                'count' => count($result),
1074                'records' => $result,
1075            ];
1076        }
1077
1078        return $result;
1079    }
1080
1081    /**
1082     * Get holdings
1083     *
1084     * Retrieve holdings from ILS driver class and normalize result array and availability if needed.
1085     *
1086     * @param string $id      The record id to retrieve the holdings for
1087     * @param array  $patron  Patron data
1088     * @param array  $options Additional options
1089     *
1090     * @return array Array with holding data
1091     */
1092    public function getHolding($id, $patron = null, $options = [])
1093    {
1094        // Get pagination options for holdings tab:
1095        $params = compact('id', 'patron');
1096        $config = $this->checkCapability('getConfig', ['Holdings', $params])
1097            ? $this->getDriver()->getConfig('Holdings', $params) : [];
1098        if (empty($config['itemLimit'])) {
1099            // Use itemLimit in Holds as fallback for backward compatibility:
1100            $config
1101                = $this->checkCapability('getConfig', ['Holds', $params])
1102                ? $this->getDriver()->getConfig('Holds', $params) : [];
1103        }
1104        $itemLimit = !empty($config['itemLimit']) ? $config['itemLimit'] : null;
1105
1106        $page = $this->request ? $this->request->getQuery('page', 1) : 1;
1107        $offset = ($itemLimit && is_numeric($itemLimit))
1108            ? ($page * $itemLimit) - $itemLimit
1109            : null;
1110        $defaultOptions = compact('page', 'itemLimit', 'offset');
1111        $finalOptions = $options + $defaultOptions;
1112
1113        // Get the holdings from the ILS
1114        $holdings = $this->__call('getHolding', [$id, $patron, $finalOptions]);
1115
1116        // Return all the necessary details:
1117        if (!isset($holdings['holdings'])) {
1118            $holdings = [
1119                'total' => count($holdings),
1120                'holdings' => $holdings,
1121                'electronic_holdings' => [],
1122            ];
1123        } else {
1124            if (!isset($holdings['total'])) {
1125                $holdings['total'] = count($holdings['holdings']);
1126            }
1127            if (!isset($holdings['electronic_holdings'])) {
1128                $holdings['electronic_holdings'] = [];
1129            }
1130        }
1131
1132        // parse availability and status to AvailabilityStatus object
1133        $holdings['holdings'] = array_map($this->getStatusParser(), $holdings['holdings']);
1134        $holdings['electronic_holdings'] = array_map($this->getStatusParser(), $holdings['electronic_holdings']);
1135        $holdings['page'] = $finalOptions['page'];
1136        $holdings['itemLimit'] = $finalOptions['itemLimit'];
1137        return $holdings;
1138    }
1139
1140    /**
1141     * Get status
1142     *
1143     * Retrieve status from ILS driver class and normalize availability if needed.
1144     *
1145     * @param string $id The record id to retrieve the status for
1146     *
1147     * @return array Array with holding data
1148     */
1149    public function getStatus($id)
1150    {
1151        $status = $this->__call('getStatus', [$id]);
1152
1153        // parse availability and status to AvailabilityStatus object
1154        return array_map($this->getStatusParser(), $status);
1155    }
1156
1157    /**
1158     * Get statuses
1159     *
1160     * Retrieve statuses from ILS driver class and normalize availability if needed.
1161     *
1162     * @param string $ids The record ids to retrieve the statuses for
1163     *
1164     * @return array Array with holding data
1165     */
1166    public function getStatuses($ids)
1167    {
1168        $statuses = $this->__call('getStatuses', [$ids]);
1169
1170        return array_map(function ($status) {
1171            // parse availability and status to AvailabilityStatus object
1172            return array_map($this->getStatusParser(), $status);
1173        }, $statuses);
1174    }
1175
1176    /**
1177     * Get a function that parses availability and status to an AvailabilityStatus object if necessary.
1178     *
1179     * @return callable
1180     */
1181    public function getStatusParser()
1182    {
1183        return function ($item) {
1184            if (!(($item['availability'] ?? null) instanceof AvailabilityStatus)) {
1185                $availability = $item['availability'] ?? false;
1186                if ($item['use_unknown_message'] ?? false) {
1187                    $availability = Logic\AvailabilityStatusInterface::STATUS_UNKNOWN;
1188                }
1189                $item['availability'] = new AvailabilityStatus(
1190                    $availability,
1191                    $item['status'] ?? ''
1192                );
1193                unset($item['status']);
1194                unset($item['use_unknown_message']);
1195            }
1196            return $item;
1197        };
1198    }
1199
1200    /**
1201     * Default method -- pass along calls to the driver if available; return
1202     * false otherwise. This allows custom functions to be implemented in
1203     * the driver without constant modification to the connection class.
1204     *
1205     * @param string $methodName The name of the called method.
1206     * @param array  $params     Array of passed parameters.
1207     *
1208     * @throws ILSException
1209     * @return mixed             Varies by method (false if undefined method)
1210     */
1211    public function __call($methodName, $params)
1212    {
1213        try {
1214            if ($this->checkCapability($methodName, $params)) {
1215                return call_user_func_array(
1216                    [$this->getDriver(), $methodName],
1217                    $params
1218                );
1219            }
1220        } catch (\Exception $e) {
1221            if ($this->failOverToNoILS($e)) {
1222                return call_user_func_array([$this, __METHOD__], func_get_args());
1223            }
1224            throw $e;
1225        }
1226        throw new ILSException(
1227            'Cannot call method: ' . $this->getDriverClass() . '::' . $methodName
1228        );
1229    }
1230}