Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.27% covered (danger)
0.27%
1 / 367
0.00% covered (danger)
0.00%
0 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
HorizonXMLAPI
0.27% covered (danger)
0.27%
1 / 367
0.00% covered (danger)
0.00%
0 / 21
5505.36
0.00% covered (danger)
0.00%
0 / 1
 init
11.11% covered (danger)
11.11%
1 / 9
0.00% covered (danger)
0.00%
0 / 1
1.70
 getConfig
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 processHoldingRow
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 determineRenewability
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 processTransactionsRow
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getPickUpLocations
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
42
 getDefaultPickUpLocation
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
30
 makeRequest
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
42
 getSession
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 registerUser
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 checkRequestIsValid
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 getItems
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 renewItems
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 placeRequest
0.00% covered (danger)
0.00%
0 / 51
0.00% covered (danger)
0.00%
0 / 1
30
 cancelRequest
0.00% covered (danger)
0.00%
0 / 43
0.00% covered (danger)
0.00%
0 / 1
110
 placeHold
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 cancelHolds
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
12
 processRenewals
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
30
 renewMyItems
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
30
 getRenewDetails
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCancelHoldDetails
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * Horizon ILS Driver (w/ XML API support)
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_Drivers
25 * @author   Matt Mackey <vufind-tech@lists.sourceforge.net>
26 * @author   Ray Cummins <vufind-tech@lists.sourceforge.net>
27 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
28 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
29 */
30
31namespace VuFind\ILS\Driver;
32
33use VuFind\Exception\ILS as ILSException;
34
35use function in_array;
36use function is_array;
37
38/**
39 * Horizon ILS Driver (w/ XML API support)
40 *
41 * @category VuFind
42 * @package  ILS_Drivers
43 * @author   Matt Mackey <vufind-tech@lists.sourceforge.net>
44 * @author   Ray Cummins <vufind-tech@lists.sourceforge.net>
45 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
46 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
47 */
48class HorizonXMLAPI extends Horizon implements \VuFindHttp\HttpServiceAwareInterface
49{
50    use \VuFindHttp\HttpServiceAwareTrait;
51
52    /**
53     * API profile
54     *
55     * @var string
56     */
57    protected $wsProfile;
58
59    /**
60     * API URL
61     *
62     * @var string
63     */
64    protected $wsURL;
65
66    /**
67     * Available pickup locations for holds
68     *
69     * @var array
70     */
71    protected $wsPickUpLocations;
72
73    /**
74     * Defaut pickup location for holds
75     *
76     * @var string
77     */
78    protected $wsDefaultPickUpLocation;
79
80    /**
81     * Date format used by API
82     *
83     * @var string
84     */
85    protected $wsDateFormat;
86
87    /**
88     * Initialize the driver.
89     *
90     * Validate configuration and perform all resource-intensive tasks needed to
91     * make the driver active.
92     *
93     * @throws ILSException
94     * @return void
95     */
96    public function init()
97    {
98        parent::init();
99
100        // Process Config
101        $this->wsProfile = $this->config['Webservices']['profile'];
102        $this->wsURL = $this->config['Webservices']['HIPurl'];
103        $this->wsPickUpLocations
104            = $this->config['pickUpLocations'] ?? false;
105
106        $this->wsDefaultPickUpLocation
107            = $this->config['Holds']['defaultPickUpLocation'] ?? false;
108
109        $this->wsDateFormat
110            = $this->config['Webservices']['dateformat'] ?? 'd/m/Y';
111    }
112
113    /**
114     * Public Function which retrieves renew, hold and cancel settings from the
115     * driver ini file.
116     *
117     * @param string $function The name of the feature to be checked
118     * @param array  $params   Optional feature-specific parameters (array)
119     *
120     * @return array An array with key-value pairs.
121     *
122     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
123     */
124    public function getConfig($function, $params = [])
125    {
126        if (isset($this->config[$function])) {
127            $functionConfig = $this->config[$function];
128        } else {
129            $functionConfig = false;
130        }
131        return $functionConfig;
132    }
133
134    /**
135     * Protected support method for getHolding.
136     *
137     * @param string $id     Bib Id
138     * @param array  $row    SQL Row Data
139     * @param array  $patron Patron Array
140     *
141     * @return array Keyed data
142     */
143    protected function processHoldingRow($id, $row, $patron)
144    {
145        $itemData = [
146            'id' => $row['ITEM_ID'],
147            'level' => 'item',
148        ];
149
150        $holding = parent::processHoldingRow($id, $row, $patron);
151        $holding += [
152            'addLink' => $this->checkRequestIsValid($id, $itemData, $patron),
153         ];
154        return $holding;
155    }
156
157    /**
158     * Determine Renewability
159     *
160     * This is responsible for determining if an item is renewable
161     *
162     * @param string $requested The number of times an item has been requested
163     *
164     * @return array $renewData Array of the renewability status and associated
165     * message
166     */
167    protected function determineRenewability($requested)
168    {
169        $renewData = [];
170
171        $renewData['renewable'] = ($requested == 0) ? true : false;
172
173        if (!$renewData['renewable']) {
174            $renewData['message'] = 'renew_item_requested';
175        } else {
176            $renewData['message'] = false;
177        }
178
179        return $renewData;
180    }
181
182    /**
183     * Protected support method for getMyTransactions.
184     *
185     * @param array $row An array of keyed data
186     *
187     * @return array Keyed data for display by template files
188     */
189    protected function processTransactionsRow($row)
190    {
191        $transactions = parent::processTransactionsRow($row);
192        $renewData = $this->determineRenewability($row['REQUEST']);
193        $transactions['renewable'] = $renewData['renewable'];
194        $transactions['message'] = $renewData['message'];
195        return $transactions;
196    }
197
198    /* Horizon XML API Functions */
199
200    /**
201     * Get Pick Up Locations
202     *
203     * This is responsible for getting a list of valid library locations for
204     * holds / recall retrieval
205     *
206     * @param array $patron      Patron information returned by the patronLogin
207     * method.
208     * @param array $holdDetails Optional array, only passed in when getting a list
209     * in the context of placing or editing a hold. When placing a hold, it contains
210     * most of the same values passed to placeHold, minus the patron data. When
211     * editing a hold it contains all the hold information returned by getMyHolds.
212     * May be used to limit the pickup options or may be ignored. The driver must
213     * not add new options to the return array based on this data or other areas of
214     * VuFind may behave incorrectly.
215     *
216     * @throws ILSException
217     * @return array        An array of associative arrays with locationID and
218     * locationDisplay keys
219     *
220     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
221     */
222    public function getPickUpLocations($patron, $holdDetails = null)
223    {
224        $pickresponse = [];
225        if ($this->wsPickUpLocations == false) {
226            // Select
227            $sqlSelect = [
228                    'l.location LOCATIONID',
229                    'l.name LOCATIONDISPLAY',
230            ];
231
232            // From
233            $sqlFrom = ['pickup_location_sort pls'];
234
235            // Join
236            $sqlJoin = [
237                    'location l on l.location = pls.pickup_location',
238                    'borrower b on b.location = pls.location',
239                    'borrower_barcode bb on bb.borrower# = b.borrower#',
240            ];
241
242            // Where
243            $sqlWhere = [
244                    'pls.display = 1',
245                    'bb.bbarcode="' . addslashes($patron['id']) . '"',
246            ];
247
248            // Order by
249            $sqlOrder = ['l.name'];
250
251            $sqlArray = [
252                    'expressions' => $sqlSelect,
253                    'from'        => $sqlFrom,
254                    'join'        => $sqlJoin,
255                    'where'       => $sqlWhere,
256                    'order'       => $sqlOrder,
257            ];
258
259            $sql = $this->buildSqlFromArray($sqlArray);
260
261            try {
262                $sqlStmt = $this->db->query($sql);
263
264                foreach ($sqlStmt as $row) {
265                    $pickresponse[] = [
266                        'locationID'      => $row['LOCATIONID'],
267                        'locationDisplay' => $row['LOCATIONDISPLAY'],
268                    ];
269                }
270            } catch (\Exception $e) {
271                $this->throwAsIlsException($e);
272            }
273        } elseif (isset($this->wsPickUpLocations)) {
274            foreach ($this->wsPickUpLocations as $code => $library) {
275                $pickresponse[] = [
276                    'locationID' => $code,
277                    'locationDisplay' => $library,
278                ];
279            }
280        }
281        return $pickresponse;
282    }
283
284    /**
285     * Get Default Pick Up Location
286     *
287     * This is responsible for retrieving the pickup location for a logged in patron.
288     *
289     * @param array $patron      Patron information returned by the patronLogin
290     * method.
291     * @param array $holdDetails Optional array, only passed in when getting a list
292     * in the context of placing a hold; contains most of the same values passed to
293     * placeHold, minus the patron data. May be used to limit the pickup options
294     * or may be ignored.
295     *
296     * @return string       The default pickup location for the patron.
297     *
298     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
299     */
300    public function getDefaultPickUpLocation($patron = false, $holdDetails = null)
301    {
302        if ($this->wsDefaultPickUpLocation == false) {
303            // Select
304            $sqlSelect = ['b.location LOCATION'];
305
306            // From
307            $sqlFrom = ['borrower b'];
308
309            // Join
310            $sqlJoin = ['borrower_barcode bb on bb.borrower# = b.borrower#'];
311
312            // Where
313            $sqlWhere = ['bb.bbarcode="' . addslashes($patron['id']) . '"'];
314
315            $sqlArray = [
316                    'expressions' => $sqlSelect,
317                    'from'        => $sqlFrom,
318                    'join'        => $sqlJoin,
319                    'where'       => $sqlWhere,
320            ];
321
322            $sql = $this->buildSqlFromArray($sqlArray);
323
324            try {
325                $sqlStmt = $this->db->query($sql);
326
327                foreach ($sqlStmt as $row) {
328                    $defaultPickUpLocation = $row['LOCATION'];
329                    return $defaultPickUpLocation;
330                }
331            } catch (\Exception $e) {
332                $this->throwAsIlsException($e);
333            }
334        } elseif (isset($this->wsDefaultPickUpLocation)) {
335            return $this->wsDefaultPickUpLocation;
336        }
337        // If we didn't return above, there were no values.
338        return null;
339    }
340
341    /**
342     * Make Request
343     *
344     * Makes a request to the Horizon API
345     *
346     * @param array  $params A keyed array of query data
347     * @param string $mode   The http request method to use (Default of GET)
348     *
349     * @return obj  A Simple XML Object loaded with the xml data returned by the API
350     */
351    protected function makeRequest($params = false, $mode = 'GET')
352    {
353        $queryString = [];
354        // Build Url Base
355        $urlParams = $this->wsURL;
356
357        // Add Params
358        foreach ($params as $key => $param) {
359            if (is_array($param)) {
360                foreach ($param as $sub) {
361                    $queryString[] = $key . '=' . urlencode($sub);
362                }
363            } else {
364                // This is necessary as Horizon expects spaces to be represented by
365                // "+" rather than the url_encode "%20" for Pick Up Locations
366                $queryString[] = $key . '=' .
367                    str_replace('%20', '+', urlencode($param));
368            }
369        }
370
371        // Build Params
372        $urlParams .= '?' . implode('&', $queryString);
373
374        // Create Proxy Request
375        $client = $this->httpService->createClient($urlParams, $mode);
376
377        // Send Request and Retrieve Response
378        $result = $client->send();
379        if (!$result->isSuccess()) {
380            throw new ILSException('Problem with XML API.');
381        }
382        $xmlResponse = $result->getBody();
383
384        $oldLibXML = libxml_use_internal_errors();
385        libxml_use_internal_errors(true);
386        $simpleXML = simplexml_load_string($xmlResponse);
387        libxml_use_internal_errors($oldLibXML);
388
389        if ($simpleXML === false) {
390            return false;
391        }
392        return $simpleXML;
393    }
394
395    /**
396     *  Get Session
397     *
398     * Gets a Horizon session
399     *
400     * @return mixed A session string on success, boolean false on failure
401     */
402    protected function getSession()
403    {
404        $params = ['profile' => $this->wsProfile,
405                        'menu' => 'account',
406                        'GetXML' => 'true',
407                        ];
408
409        $response = $this->makeRequest($params);
410
411        if ($response && $response->session) {
412            $session = (string)$response->session;
413            return $session;
414        }
415
416        return false;
417    }
418
419    /**
420     *  Register User
421     *
422     * Associates a user with a session
423     *
424     * @param string $userBarcode  A valid Horizon user barcode
425     * @param string $userPassword A valid Horizon user password (pin)
426     *
427     * @return bool true on success, false on failure
428     */
429    protected function registerUser($userBarcode, $userPassword)
430    {
431        // Get Session
432        $session = $this->getSession();
433
434        $params = ['session' => $session,
435                        'profile' => $this->wsProfile,
436                        'menu' => 'account',
437                        'sec1' => $userBarcode,
438                        'sec2' => $userPassword,
439                        'GetXML' => 'true',
440                        ];
441
442        $response = $this->makeRequest($params);
443
444        $auth = (string)$response->security->auth;
445
446        if ($auth == 'true') {
447            return $session;
448        }
449
450        return false;
451    }
452
453    /**
454     * Check if Request is Valid
455     *
456     * Determines if a user can place a hold or recall on a specific item
457     *
458     * @param string $bibId    An item's Bib ID
459     * @param string $itemData Array containing item id and hold level
460     * @param array  $patron   Patron Array Data
461     *
462     * @return bool true if the request can be made, false if it cannot
463     */
464    public function checkRequestIsValid($bibId, $itemData, $patron)
465    {
466        // Register Account
467        $session = $this->registerUser(
468            $patron['cat_username'],
469            $patron['cat_password']
470        );
471        if ($session) {
472            $params = [
473                'session' => $session,
474                'profile' => $this->wsProfile,
475                'bibkey'  => $bibId,
476                'aspect'  => 'submenu13',
477                'lang'    => 'eng',
478                'menu'    => 'request',
479                'submenu' => 'none',
480                'source'  => '~!horizon',
481                'uri'     => '',
482                'GetXML'  => 'true',
483            ];
484
485            // set itemkey only if available and level is not title-level
486            if ($itemData['item_id'] != '' && $itemData['level'] != 'title') {
487                $params += ['itemkey' => $itemData['item_id']];
488            }
489
490            $initResponse = $this->makeRequest($params);
491
492            if ($initResponse->request_confirm) {
493                return true;
494            }
495        }
496        return false;
497    }
498
499    /**
500     *  Get Items
501     *
502     * Gets a list of items on loan
503     *
504     * @param string $session A valid Horizon session key
505     *
506     * @return obj A Simple XML Object
507     */
508    protected function getItems($session)
509    {
510        $params = ['session' => $session,
511                        'profile' => $this->wsProfile,
512                        'menu' => 'account',
513                        'submenu' => 'itemsout',
514                        'GetXML' => 'true',
515                        ];
516
517        $response = $this->makeRequest($params);
518
519        if ($response->itemsoutdata) {
520            return $response->itemsoutdata;
521        }
522
523        return false;
524    }
525
526    /**
527     *  Renew Items
528     *
529     * Submits a renewal request to the Horizon API and returns the results
530     *
531     * @param string $session A valid Horizon session key
532     * @param array  $items   A list of items to be renewed
533     *
534     * @return obj A Simple XML Object
535     */
536    protected function renewItems($session, $items)
537    {
538        $params = ['session' => $session,
539                        'profile' => $this->wsProfile,
540                        'menu' => 'account',
541                        'submenu' => 'itemsout',
542                        'renewitemkeys' => $items,
543                        'renewitems' => 'Renew',
544                        'GetXML' => 'true',
545                        ];
546
547        $response = $this->makeRequest($params);
548
549        if ($response->itemsoutdata) {
550            return $response->itemsoutdata;
551        }
552
553        return false;
554    }
555
556    /**
557     * Place Request
558     *
559     * Submits a hold request to the Horizon XML API and processes the result
560     *
561     * @param string $session        A valid Horizon session key
562     * @param array  $requestDetails An array of request details
563     *
564     * @return array  An array witk keys indicating the a success (boolean),
565     * status (string) and sysMessage (string) if available
566     */
567    protected function placeRequest($session, $requestDetails)
568    {
569        $params = ['session' => $session,
570                        'profile' => $this->wsProfile,
571                        'bibkey' => $requestDetails['bibId'],
572                        'aspect' => 'submenu13',
573                        'lang' => 'eng',
574                        'menu' => 'request',
575                        'submenu' => 'none',
576                        'source' => '~!horizon',
577                        'uri' => '',
578                        'GetXML' => 'true',
579                        ];
580
581        // set itemkey only if available
582        if ($requestDetails['itemId'] != '') {
583            $params += ['itemkey' => $requestDetails['itemId']];
584        }
585
586        $initResponse = $this->makeRequest($params);
587
588        if ($initResponse->request_confirm) {
589            $confirmParams = [
590                'session' => $session,
591                'profile' => $this->wsProfile,
592                'bibkey' => $requestDetails['bibId'],
593                'aspect' => 'advanced',
594                'lang' => 'eng',
595                'menu' => 'request',
596                'submenu' => 'none',
597                'source' => '~!horizon',
598                'uri' => '',
599                'link' => 'direct',
600                'request_finish' => 'Request',
601                'cl' => 'PlaceRequestjsp',
602                'pickuplocation' => $requestDetails['pickuplocation'],
603                'notifyby' => $requestDetails['notify'],
604                'GetXML' => 'true',
605            ];
606
607            $request = $this->makeRequest($confirmParams);
608
609            if ($request->request_success) {
610                $response = [
611                    'success' => true,
612                    'status' => 'hold_success',
613                ];
614            } else {
615                $response = [
616                    'success' => false,
617                    'status' => 'hold_error_fail',
618                ];
619            }
620        } else {
621            $sysMessage = false;
622            if ($initResponse->alert->message) {
623                $sysMessage = (string)$initResponse->alert->message;
624            }
625            $response = [
626                'success' => false,
627                'status' => 'hold_error_fail',
628                'sysMessage' => $sysMessage,
629            ];
630        }
631        return $response;
632    }
633
634    /**
635     * Cancel Request
636     *
637     * Submits a cancel request to the Horizon API and processes the result
638     *
639     * @param string $session A valid Horizon session key
640     * @param Array  $data    An array of item data
641     *
642     * @return array  An array of cancel information keyed by item ID plus
643     * the number of successful cancels
644     */
645    protected function cancelRequest($session, $data)
646    {
647        $responseItems = [];
648
649        $params = ['session'    => $session,
650                        'profile'    => $this->wsProfile,
651                        'lang'       => 'eng',
652                        'menu'       => 'account',
653                        'submenu'    => 'holds',
654                        'cancelhold' => 'Cancel Request',
655                        'GetXML'     => 'true',
656                        ];
657
658        $cancelData = [];
659        foreach ($data as $values) {
660            $cancelData[] = $values['bib_id'] . ':' . $values['item_id'];
661        }
662
663        $params += ['waitingholdselected' => $cancelData];
664
665        $response = $this->makeRequest($params);
666
667        $count = 0;
668        // No Indication of Success or Failure
669        if ($response !== false && !$response->error->message) {
670            $keys = [];
671            // Get a list of bib keys from waiting items
672            $currentHolds = $response->holdsdata->waiting->waitingitem;
673            foreach ($currentHolds as $hold) {
674                foreach ($hold->key as $key) {
675                    $keys[] = (string)$key;
676                }
677            }
678
679            // Go through the submited bib ids and look for a match
680            foreach ($data as $values) {
681                $itemID = $values['item_id'];
682                // If the bib id is matched, the cancel must have failed
683                if (in_array($values['bib_id'], $keys)) {
684                    $responseItems[$itemID] = [
685                        'success' => false, 'status' => 'hold_cancel_fail',
686                    ];
687                } else {
688                    $responseItems[$itemID] = [
689                        'success' => true, 'status' => 'hold_cancel_success',
690
691                    ];
692                    $count = $count + 1;
693                }
694            }
695        } else {
696            $message = false;
697            if ($response->error->message) {
698                $message = (string)$response->error->message;
699            }
700            foreach ($data as $values) {
701                $itemID = $values['item_id'];
702                $responseItems[$itemID] = [
703                    'success' => false,
704                    'status' => 'hold_cancel_fail',
705                    'sysMessage' => $message,
706                ];
707            }
708        }
709        $result = ['count' => $count, 'items' => $responseItems];
710        return $result;
711    }
712
713    /**
714     * Place Hold
715     *
716     * Attempts to place a hold or recall on a particular item and returns
717     * an array with result details or throws an exception on failure of support
718     * classes
719     *
720     * @param array $holdDetails An array of item and patron data
721     *
722     * @throws ILSException
723     * @return mixed An array of data on the request including
724     * whether or not it was successful and a system message (if available)
725     */
726    public function placeHold($holdDetails)
727    {
728        $userBarcode      = $holdDetails['patron']['id'];
729        $userPassword     = $holdDetails['patron']['cat_password'];
730        $bibId            = $holdDetails['id'];
731        $itemId           = $holdDetails['item_id'];
732        $level            = $holdDetails['level'];
733        $pickUpLocationID = !empty($holdDetails['pickUpLocation'])
734                          ? $holdDetails['pickUpLocation']
735                          : $this->getDefaultPickUpLocation();
736        $notify           = $this->config['Holds']['notify'];
737
738        $requestDetails = [
739            'bibId'          => $bibId,
740            'pickuplocation' => strtoupper($pickUpLocationID),
741            'notify'         => $notify,
742        ];
743
744        if ($level != 'title' && $itemId != '') {
745            $requestDetails += ['itemId' => $itemId];
746        }
747
748        // Register Account
749        $session = $this->registerUser($userBarcode, $userPassword);
750        if ($session) {
751            $response = $this->placeRequest($session, $requestDetails);
752        } else {
753            $response = [
754                'success' => false, 'status' => 'authentication_error_admin',
755            ];
756        }
757
758        return $response;
759    }
760
761    /**
762     * Cancel Holds
763     *
764     * Attempts to Cancel a hold or recall on a particular item. The
765     * data in $cancelDetails['details'] is determined by getCancelHoldDetails().
766     *
767     * @param array $cancelDetails An array of item and patron data
768     *
769     * @return array               An array of data on each request including
770     * whether or not it was successful and a system message (if available)
771     */
772    public function cancelHolds($cancelDetails)
773    {
774        $cancelIDs = [];
775        $details = $cancelDetails['details'];
776        $userBarcode = $cancelDetails['patron']['id'];
777        $userPassword = $cancelDetails['patron']['cat_password'];
778
779        foreach ($details as $cancelItem) {
780            [$bibID, $itemID] = explode('|', $cancelItem);
781            $cancelIDs[]  = ['bib_id' =>  $bibID, 'item_id' => $itemID];
782        }
783
784        // Register Account
785        $session = $this->registerUser($userBarcode, $userPassword);
786        if ($session) {
787            $response = $this->cancelRequest($session, $cancelIDs);
788        } else {
789            $response = [
790                'success' => false, 'sysMessage' => 'authentication_error_admin',
791            ];
792        }
793        return $response;
794    }
795
796    /**
797     * Process Renewals
798     *
799     * This is responsible for processing renewals and is necessary
800     * as result of renew attempt is not returned
801     *
802     * @param array $renewIDs  A list of the items being renewed
803     * @param array $origData  A Simple XML array of loan data before the
804     * renewal attempt
805     * @param array $renewData A Simple XML array of loan data after the
806     * renewal attempt
807     *
808     * @return array        An Array specifying the results of each renewal attempt
809     */
810    protected function processRenewals($renewIDs, $origData, $renewData)
811    {
812        $response = ['ids' => $renewIDs];
813        $i = 0;
814        foreach ($origData->itemout as $item) {
815            $ikey = (string)$item->ikey;
816            if (in_array($ikey, $renewIDs)) {
817                $response['details'][$ikey]['item_id'] = $ikey;
818                $origRenewals = (string)$item->numrenewals;
819                $currentRenewals = (string)$renewData->itemout[$i]->numrenewals;
820
821                $dueDate = (string)$renewData->itemout[$i]->duedate;
822                $renewerror = (string)$renewData->itemout[$i]->renewerror;
823
824                // Convert Horizon Format to display format
825                if (!empty($dueDate)) {
826                    $dueDate = $this->dateFormat->convertToDisplayDate(
827                        $this->wsDateFormat,
828                        $dueDate
829                    );
830                }
831
832                if ($currentRenewals > $origRenewals) {
833                    $response['details'][$ikey] = [
834                        'item_id' => $ikey,
835                        'new_date' =>  $dueDate,
836                        'success' => true,
837                    ];
838                } else {
839                    $response['details'][$ikey] = [
840                    'item_id' => $ikey,
841                    'new_date' => '',
842                        'success'    => false,
843                        'sysMessage' => $renewerror,
844                    ];
845                }
846            }
847            $i++;
848        }
849        return $response;
850    }
851
852    /**
853     * Renew My Items
854     *
855     * Function for attempting to renew a patron's items. The data in
856     * $renewDetails['details'] is determined by getRenewDetails().
857     *
858     * @param array $renewDetails An array of data required for renewing items
859     * including the Patron ID and an array of renewal IDS
860     *
861     * @return array              An array of renewal information keyed by item ID
862     */
863    public function renewMyItems($renewDetails)
864    {
865        $renewItemKeys = [];
866        $renewIDs = [];
867        $renewals = $renewDetails['details'];
868        $userBarcode = $renewDetails['patron']['id'];
869        $userPassword = $renewDetails['patron']['cat_password'];
870
871        $session = $this->registerUser($userBarcode, $userPassword);
872        if ($session) {
873            // Get Items
874            $origData = $this->getItems($session);
875            if ($origData) {
876                // Build Params
877                foreach ($renewals as $item) {
878                    [$itemID, $barcode] = explode('|', $item);
879                    $renewItemKeys[] = $barcode;
880                    $renewIDs[] = $itemID;
881                }
882                // Renew Items
883                $renewData = $this->renewItems($session, $renewItemKeys);
884                if ($renewData) {
885                    $response = $this->processRenewals(
886                        $renewIDs,
887                        $origData,
888                        $renewData
889                    );
890                    return $response;
891                }
892            }
893        }
894
895        return ['blocks' => ['authentication_error_admin']];
896    }
897
898    /**
899     * Get Renew Details
900     *
901     * In order to renew an item, Voyager requires the patron details and an item
902     * id. This function returns the item id as a string which is then used
903     * as submitted form data in checkedOut.php. This value is then extracted by
904     * the RenewMyItems function.
905     *
906     * @param array $checkOutDetails An array of item data
907     *
908     * @return string Data for use in a form field
909     */
910    public function getRenewDetails($checkOutDetails)
911    {
912        return $checkOutDetails['item_id'] . '|' . $checkOutDetails['barcode'];
913    }
914
915    /**
916     * Get Cancel Hold Details
917     *
918     * In order to cancel a hold, Voyager requires the patron details an item ID
919     * and a recall ID. This function returns the item id and recall id as a string
920     * separated by a pipe, which is then submitted as form data in Hold.php. This
921     * value is then extracted by the CancelHolds function.
922     *
923     * @param array $holdDetails A single hold array from getMyHolds
924     * @param array $patron      Patron information from patronLogin
925     *
926     * @return string Data for use in a form field
927     *
928     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
929     */
930    public function getCancelHoldDetails($holdDetails, $patron = [])
931    {
932        $cancelDetails = $holdDetails['id'] . '|' . $holdDetails['item_id'];
933        return $cancelDetails;
934    }
935}