Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.89% covered (danger)
0.89%
3 / 336
5.56% covered (danger)
5.56%
1 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Koha
0.89% covered (danger)
0.89%
3 / 336
5.56% covered (danger)
5.56%
1 / 18
5698.66
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 init
8.70% covered (danger)
8.70%
2 / 23
0.00% covered (danger)
0.00%
0 / 1
24.03
 getHolding
0.00% covered (danger)
0.00%
0 / 67
0.00% covered (danger)
0.00%
0 / 1
210
 getHoldLink
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMyFines
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 getMyHolds
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 getMyProfile
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 getMyTransactions
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
12
 getAccountBlocks
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
56
 getMyTransactionHistory
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
132
 getPurchaseHistory
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStatus
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getStatuses
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getSuppressedRecords
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 patronLogin
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
56
 displayDate
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 displayDateTime
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getConfig
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3/**
4 * Koha ILS Driver
5 *
6 * PHP version 8
7 *
8 * Copyright (C) Ayesha Abed Library, BRAC University 2010.
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   Altaf Mahmud, System Programmer <altaf.mahmud@gmail.com>
26 * @author   David Maus <maus@hab.de>
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 PDO;
34use PDOException;
35use VuFind\Date\DateException;
36use VuFind\Exception\ILS as ILSException;
37
38use function count;
39
40/**
41 * VuFind Driver for Koha (version: 3.02)
42 *
43 * @category VuFind
44 * @package  ILS_Drivers
45 * @author   Altaf Mahmud, System Programmer <altaf.mahmud@gmail.com>
46 * @author   David Maus <maus@hab.de>
47 * @license  http://opensource.org/licenses/gpl-2.0.php GNU General Public License
48 * @link     https://vufind.org/wiki/development:plugins:ils_drivers Wiki
49 */
50class Koha extends AbstractBase
51{
52    /**
53     * Database connection
54     *
55     * @var PDO
56     */
57    protected $db;
58
59    /**
60     * ILS base URL
61     *
62     * @var string
63     */
64    protected $ilsBaseUrl;
65
66    /**
67     * Location codes
68     *
69     * @var array
70     */
71    protected $locCodes;
72
73    /**
74     * Date converter object
75     *
76     * @var \VuFind\Date\Converter
77     */
78    protected $dateConverter = null;
79
80    /**
81     * Should we validate passwords against Koha system?
82     *
83     * @var boolean
84     */
85    protected $validatePasswords;
86
87    /**
88     * Default terms for block types, can be overridden by configuration
89     *
90     * @var array
91     */
92    protected $blockTerms = [
93        'SUSPENSION' => 'Account Suspended',
94        'OVERDUES' => 'Account Blocked (Overdue Items)',
95        'MANUAL' => 'Account Blocked',
96        'DISCHARGE' => 'Account Blocked for Discharge',
97    ];
98
99    /**
100     * Display comments for patron debarments, see Koha.ini
101     *
102     * @var array
103     */
104    protected $showBlockComments;
105
106    /**
107     * Constructor
108     *
109     * @param \VuFind\Date\Converter $dateConverter Date converter
110     */
111    public function __construct(\VuFind\Date\Converter $dateConverter)
112    {
113        $this->dateConverter = $dateConverter;
114    }
115
116    /**
117     * Initialize the driver.
118     *
119     * Validate configuration and perform all resource-intensive tasks needed to
120     * make the driver active.
121     *
122     * @throws ILSException
123     * @return void
124     */
125    public function init()
126    {
127        if (empty($this->config)) {
128            throw new ILSException('Configuration needs to be set.');
129        }
130
131        //Connect to MySQL
132        $this->db = new PDO(
133            'mysql:host=' . $this->config['Catalog']['host'] .
134            ';port=' . $this->config['Catalog']['port'] .
135            ';dbname=' . $this->config['Catalog']['database'],
136            $this->config['Catalog']['username'],
137            $this->config['Catalog']['password'],
138            [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8']
139        );
140        // Throw PDOExceptions if something goes wrong
141        $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
142        // Return result set like mysql_fetch_assoc()
143        $this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
144
145        //Storing the base URL of ILS
146        $this->ilsBaseUrl = $this->config['Catalog']['url'];
147
148        // Location codes are defined in 'Koha.ini' file according to current
149        // version (3.02)
150        $this->locCodes = $this->config['Location_Codes'];
151
152        // If we are using SAML/Shibboleth for authentication for both ourselves
153        // and Koha then we can't validate the patrons passwords against Koha as
154        // they won't have one. (Double negative logic used so that if the config
155        // option isn't present in Koha.ini then ILS passwords will be validated)
156        $this->validatePasswords
157            = empty($this->config['Catalog']['dontValidatePasswords']);
158
159        // Now override the default with any defined in the `Koha.ini` config file
160        foreach (['SUSPENSION','OVERDUES','MANUAL','DISCHARGE'] as $blockType) {
161            if (!empty($this->config['Blocks'][$blockType])) {
162                $this->blockTerms[$blockType] = $this->config['Blocks'][$blockType];
163            }
164        }
165
166        // Allow the users to set if an account block's comments should be included
167        // by setting the block type to true or false () in the `Koha.ini` config
168        // file (defaults to false if not present)
169        $this->showBlockComments = [];
170
171        foreach (['SUSPENSION','OVERDUES','MANUAL','DISCHARGE'] as $blockType) {
172            $this->showBlockComments[$blockType]
173                = !empty($this->config['Show_Block_Comments'][$blockType]);
174        }
175    }
176
177    /**
178     * Get Holding
179     *
180     * This is responsible for retrieving the holding information of a certain
181     * record.
182     *
183     * @param string $id      The record id to retrieve the holdings for
184     * @param array  $patron  Patron data
185     * @param array  $options Extra options (not currently used)
186     *
187     * @throws DateException
188     * @throws ILSException
189     * @return array         On success, an associative array with the following
190     * keys: id, availability (boolean), status, location, reserve, callnumber,
191     * duedate, number, barcode.
192     *
193     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
194     */
195    public function getHolding($id, array $patron = null, array $options = [])
196    {
197        $holding = [];
198        $available = true;
199        $duedate = $status = '';
200        $inum = 0;
201        $loc = $shelf = '';
202        $sql = 'select itemnumber as ITEMNO, location as LOCATION, ' .
203            'holdingbranch as HLDBRNCH, reserves as RESERVES, itemcallnumber as ' .
204            'CALLNO, barcode as BARCODE, copynumber as COPYNO, ' .
205            'enumchron AS ENUMCHRON, notforloan as NOTFORLOAN' .
206            ' from items where biblionumber = :id' .
207            ' order by itemnumber';
208        try {
209            $itemSqlStmt = $this->db->prepare($sql);
210            $itemSqlStmt->execute([':id' => $id]);
211            foreach ($itemSqlStmt->fetchAll() as $rowItem) {
212                $inum = $rowItem['ITEMNO'];
213                $sql = 'select date_due as DUEDATE from issues ' .
214                    'where itemnumber = :inum';
215
216                switch ($rowItem['NOTFORLOAN']) {
217                    case 0:
218                        // If the item is available for loan, then check its current
219                        // status
220                        $issueSqlStmt = $this->db->prepare($sql);
221                        $issueSqlStmt->execute([':inum' => $inum]);
222                        $rowIssue = $issueSqlStmt->fetch();
223                        if ($rowIssue) {
224                            $available = false;
225                            $status = 'Checked out';
226                            $duedate = $this->displayDateTime($rowIssue['DUEDATE']);
227                        } else {
228                            $available = true;
229                            $status = 'Available';
230                            // No due date for an available item
231                            $duedate = '';
232                        }
233                        break;
234                    case 1: // The item is not available for loan
235                    default:
236                        $available = false;
237                        $status = 'Not for loan';
238                        $duedate = '';
239                        break;
240                }
241
242                //Retrieving the full branch name
243                if (null != ($loc = $rowItem['HLDBRNCH'])) {
244                    $sql = 'select branchname as BNAME from branches where ' .
245                        'branchcode = :loc';
246                    $locSqlStmt = $this->db->prepare($sql);
247                    $locSqlStmt->execute([':loc' => $loc]);
248                    $row = $locSqlStmt->fetch();
249                    if ($row) {
250                        $loc = $row['BNAME'];
251                    }
252                } else {
253                    $loc = 'Unknown';
254                }
255
256                //Retrieving the location (shelf types)
257                $shelf = $rowItem['LOCATION'];
258                $loc = (null != $shelf)
259                    ? $loc . ': ' . ($this->locCodes[$shelf] ?? $shelf)
260                    : $loc . ': ' . 'Unknown';
261
262                //A default value is stored for null
263                $holding[] = [
264                    'id' => $id,
265                    'availability' => $available,
266                    'item_num' => $rowItem['ITEMNO'],
267                    'status' => $status,
268                    'location' => $loc,
269                    'reserve' => (null == $rowItem['RESERVES'])
270                        ? 'Unknown' : $rowItem['RESERVES'],
271                    'callnumber' => (null == $rowItem['CALLNO'])
272                        ? 'Unknown' : $rowItem['CALLNO'],
273                    'duedate' => $duedate,
274                    'barcode' => (null == $rowItem['BARCODE'])
275                        ? 'Unknown' : $rowItem['BARCODE'],
276                    'number' => (null == $rowItem['COPYNO'])
277                        ? 'Unknown' : $rowItem['COPYNO'],
278                    'enumchron'    => $rowItem['ENUMCHRON'] ?? null,
279                ];
280            }
281        } catch (PDOException $e) {
282            $this->throwAsIlsException($e);
283        }
284        return $holding;
285    }
286
287    /**
288     * Get Hold Link
289     *
290     * The goal for this method is to return a URL to a "place hold" web page on
291     * the ILS OPAC. This is used for ILSs that do not support an API or method
292     * to place Holds.
293     *
294     * @param string $id      The id of the bib record
295     * @param array  $details Item details from getHoldings return array
296     *
297     * @return string         URL to ILS's OPAC's place hold screen.
298     *
299     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
300     */
301    public function getHoldLink($id, $details)
302    {
303        // Web link of the ILS for placing hold on the item
304        return $this->ilsBaseUrl . "/cgi-bin/koha/opac-reserve.pl?biblionumber=$id";
305    }
306
307    /**
308     * Get Patron Fines
309     *
310     * This is responsible for retrieving all fines by a specific patron.
311     *
312     * @param array $patron The patron array from patronLogin
313     *
314     * @throws DateException
315     * @throws ILSException
316     * @return mixed        Array of the patron's fines on success.
317     */
318    public function getMyFines($patron)
319    {
320        $sql = $sqlStmt = $row = '';
321        $id = 0;
322        $fineLst = [];
323        try {
324            $id = $patron['id'];
325            $sql = 'select round(accountlines.amount*100) as AMOUNT, ' .
326                'issues.issuedate as CHECKOUT, ' .
327                'accountlines.description as FINE, ' .
328                'round(accountlines.amountoutstanding*100) as BALANCE, ' .
329                'issues.date_due as DUEDATE, items.biblionumber as BIBNO ' .
330                'from accountlines join issues on ' .
331                'accountlines.borrowernumber = issues.borrowernumber and ' .
332                'accountlines.itemnumber = issues.itemnumber ' .
333                'join items on accountlines.itemnumber = items.itemnumber ' .
334                'where accountlines.borrowernumber = :id';
335            $sqlStmt = $this->db->prepare($sql);
336            $sqlStmt->execute([':id' => $id]);
337            foreach ($sqlStmt->fetchAll() as $row) {
338                $fineLst[] = [
339                    'amount' => (null == $row['AMOUNT']) ? 0 : $row['AMOUNT'],
340                    'checkout' => $this->displayDate($row['CHECKOUT']),
341                    'fine' => (null == $row['FINE']) ? 'Unknown' : $row['FINE'],
342                    'balance' => (null == $row['BALANCE']) ? 0 : $row['BALANCE'],
343                    'duedate' => $this->displayDate($row['DUEDATE']),
344                    'id' => $row['BIBNO'],
345                ];
346            }
347            return $fineLst;
348        } catch (PDOException $e) {
349            $this->throwAsIlsException($e);
350        }
351    }
352
353    /**
354     * Get Patron Holds
355     *
356     * This is responsible for retrieving all holds by a specific patron.
357     *
358     * @param array $patron The patron array from patronLogin
359     *
360     * @throws DateException
361     * @throws ILSException
362     * @return array        Array of the patron's holds on success.
363     */
364    public function getMyHolds($patron)
365    {
366        $sql = $sqlStmt = $row = '';
367        $id = 0;
368        $holdLst = [];
369        try {
370            $id = $patron['id'];
371            $sql = 'select reserves.biblionumber as BIBNO, ' .
372                'branches.branchname as BRNAME, ' .
373                'reserves.expirationdate as EXDATE, ' .
374                'reserves.reservedate as RSVDATE from reserves ' .
375                'join branches on reserves.branchcode = branches.branchcode ' .
376                'where reserves.borrowernumber = :id';
377            $sqlStmt = $this->db->prepare($sql);
378            $sqlStmt->execute([':id' => $id]);
379            foreach ($sqlStmt->fetchAll() as $row) {
380                $holdLst[] = [
381                    'id' => $row['BIBNO'],
382                    'location' => $row['BRNAME'],
383                    'expire' => $this->displayDate($row['EXDATE']),
384                    'create' => $this->displayDate($row['RSVDATE']),
385                ];
386            }
387        } catch (PDOException $e) {
388            $this->throwAsIlsException($e);
389        }
390        return $holdLst;
391    }
392
393    /**
394     * Get Patron Profile
395     *
396     * This is responsible for retrieving the profile for a specific patron.
397     *
398     * @param array $patron The patron array
399     *
400     * @throws ILSException
401     * @return array        Array of the patron's profile data on success.
402     */
403    public function getMyProfile($patron)
404    {
405        $id = 0;
406        $sql = $sqlStmt = $row = '';
407        $profile = [];
408        try {
409            $id = $patron['id'];
410            $sql = 'select address as ADDR1, address2 as ADDR2, zipcode as ZIP, ' .
411                'phone as PHONE, categorycode as GRP from borrowers ' .
412                'where borrowernumber = :id';
413            $sqlStmt = $this->db->prepare($sql);
414            $sqlStmt->execute([':id' => $id]);
415            $row = $sqlStmt->fetch();
416            if ($row) {
417                $profile = [
418                    'firstname' => $patron['firstname'],
419                    'lastname' => $patron['lastname'],
420                    'address1' => $row['ADDR1'],
421                    'address2' => $row['ADDR2'],
422                    'zip' => $row['ZIP'],
423                    'phone' => $row['PHONE'],
424                    'group' => $row['GRP'],
425                ];
426                return $profile;
427            }
428        } catch (PDOException $e) {
429            $this->throwAsIlsException($e);
430        }
431        return null;
432    }
433
434    /**
435     * Get Patron Transactions
436     *
437     * This is responsible for retrieving all transactions (i.e. checked out items)
438     * by a specific patron.
439     *
440     * @param array $patron The patron array from patronLogin
441     *
442     * @throws DateException
443     * @throws ILSException
444     * @return array        Array of the patron's transactions on success.
445     */
446    public function getMyTransactions($patron)
447    {
448        $id = 0;
449        $transactionLst = [];
450        $row = $sql = $sqlStmt = '';
451        try {
452            $id = $patron['id'];
453            $sql = 'select issues.date_due as DUEDATE, items.biblionumber as ' .
454                'BIBNO, items.barcode BARCODE, issues.renewals as RENEWALS ' .
455                'from issues join items on issues.itemnumber = items.itemnumber ' .
456                'where issues.borrowernumber = :id';
457            $sqlStmt = $this->db->prepare($sql);
458            $sqlStmt->execute([':id' => $id]);
459            foreach ($sqlStmt->fetchAll() as $row) {
460                $transactionLst[] = [
461                    'duedate' => $this->displayDateTime($row['DUEDATE']),
462                    'id' => $row['BIBNO'],
463                    'barcode' => $row['BARCODE'],
464                    'renew' => $row['RENEWALS'],
465                ];
466            }
467        } catch (PDOException $e) {
468            $this->throwAsIlsException($e);
469        }
470        return $transactionLst;
471    }
472
473    /**
474     * Check whether the patron has any blocks on their account.
475     *
476     * @param array $patron Patron data from patronLogin
477     *
478     * @throws ILSException
479     *
480     * @return mixed A boolean false if no blocks are in place and an array
481     * of block reasons if blocks are in place
482     */
483    public function getAccountBlocks($patron)
484    {
485        $blocks = [];
486
487        try {
488            $id = $patron['id'];
489            $sql = 'select type as TYPE, comment as COMMENT ' .
490                'from borrower_debarments ' .
491                'where (expiration is null or expiration >= NOW()) ' .
492                'and borrowernumber = :id';
493            $sqlStmt = $this->db->prepare($sql);
494            $sqlStmt->execute([':id' => $id]);
495
496            foreach ($sqlStmt->fetchAll() as $row) {
497                $block = empty($this->blockTerms[$row['TYPE']])
498                    ? [$row['TYPE']]
499                    : [$this->blockTerms[$row['TYPE']]];
500
501                if (
502                    !empty($this->showBlockComments[$row['TYPE']])
503                    && !empty($row['COMMENT'])
504                ) {
505                    $block[] = $row['COMMENT'];
506                }
507
508                $blocks[] = implode(' - ', $block);
509            }
510        } catch (PDOException $e) {
511            $this->throwAsIlsException($e);
512        }
513
514        return count($blocks) ? $blocks : false;
515    }
516
517    /**
518     * Get Patron Loan History
519     *
520     * This is responsible for retrieving all historic loans (i.e. items previously
521     * checked out and then returned), for a specific patron.
522     *
523     * @param array $patron The patron array from patronLogin
524     * @param array $params Parameters
525     *
526     * @throws DateException
527     * @throws ILSException
528     * @return array        Array of the patron's transactions on success.
529     */
530    public function getMyTransactionHistory($patron, $params)
531    {
532        $id = 0;
533        $historicLoans = [];
534        $row = $sql = $sqlStmt = '';
535        try {
536            $id = $patron['id'];
537
538            // Get total count first
539            $sql = 'select count(*) as cnt from old_issues ' .
540                'where old_issues.borrowernumber = :id';
541            $sqlStmt = $this->db->prepare($sql);
542            $sqlStmt->execute([':id' => $id]);
543            $totalCount = $sqlStmt->fetch()['cnt'];
544
545            // Get rows
546            $limit = isset($params['limit']) ? (int)$params['limit'] : 50;
547            $start = isset($params['page'])
548                ? ((int)$params['page'] - 1) * $limit : 0;
549            if (isset($params['sort'])) {
550                $parts = explode(' ', $params['sort'], 2);
551                switch ($parts[0]) {
552                    case 'return':
553                        $sort = 'RETURNED';
554                        break;
555                    case 'due':
556                        $sort = 'DUEDATE';
557                        break;
558                    default:
559                        $sort = 'ISSUEDATE';
560                        break;
561                }
562                $sort .= isset($parts[1]) && 'asc' === $parts[1] ? ' asc' : ' desc';
563            } else {
564                $sort = 'ISSUEDATE desc';
565            }
566            $sql = 'select old_issues.issuedate as ISSUEDATE, ' .
567                'old_issues.date_due as DUEDATE, items.biblionumber as ' .
568                'BIBNO, items.barcode BARCODE, old_issues.returndate as RETURNED, ' .
569                'biblio.title as TITLE ' .
570                'from old_issues join items ' .
571                'on old_issues.itemnumber = items.itemnumber ' .
572                'join biblio on items.biblionumber = biblio.biblionumber ' .
573                'where old_issues.borrowernumber = :id ' .
574                "order by $sort limit $start,$limit";
575            $sqlStmt = $this->db->prepare($sql);
576
577            $sqlStmt->execute([':id' => $id]);
578            foreach ($sqlStmt->fetchAll() as $row) {
579                $historicLoans[] = [
580                    'title' => $row['TITLE'],
581                    'checkoutDate' => $this->displayDateTime($row['ISSUEDATE']),
582                    'dueDate' => $this->displayDateTime($row['DUEDATE']),
583                    'id' => $row['BIBNO'],
584                    'barcode' => $row['BARCODE'],
585                    'returnDate' => $this->displayDateTime($row['RETURNED']),
586                ];
587            }
588        } catch (PDOException $e) {
589            $this->throwAsIlsException($e);
590        }
591        return [
592            'count' => $totalCount,
593            'transactions' => $historicLoans,
594        ];
595    }
596
597    /**
598     * Get Purchase History
599     *
600     * This is responsible for retrieving the acquisitions history data for the
601     * specific record (usually recently received issues of a serial).
602     *
603     * @param string $id The record id to retrieve the info for
604     *
605     * @throws ILSException
606     * @return array     An array with the acquisitions data on success.
607     *
608     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
609     */
610    public function getPurchaseHistory($id)
611    {
612        // TODO
613        return [];
614    }
615
616    /**
617     * Get Status
618     *
619     * This is responsible for retrieving the status information of a certain
620     * record.
621     *
622     * @param string $id The record id to retrieve the holdings for
623     *
624     * @throws ILSException
625     * @return mixed     On success, an associative array with the following keys:
626     * id, availability (boolean), status, location, reserve, callnumber.
627     */
628    public function getStatus($id)
629    {
630        return $this->getHolding($id);
631    }
632
633    /**
634     * Get Statuses
635     *
636     * This is responsible for retrieving the status information for a
637     * collection of records.
638     *
639     * @param array $idLst The array of record ids to retrieve the status for
640     *
641     * @throws ILSException
642     * @return array       An array of getStatus() return values on success.
643     */
644    public function getStatuses($idLst)
645    {
646        $statusLst = [];
647        foreach ($idLst as $id) {
648            $statusLst[] = $this->getStatus($id);
649        }
650        return $statusLst;
651    }
652
653    /**
654     * Get suppressed records.
655     *
656     * NOTE: This function needs to be modified only if Koha has
657     *       suppressed records in OPAC view
658     *
659     * @throws ILSException
660     * @return array ID numbers of suppressed records in the system.
661     */
662    public function getSuppressedRecords()
663    {
664        // TODO
665        return [];
666    }
667
668    /**
669     * Patron Login
670     *
671     * This is responsible for authenticating a patron against the catalog.
672     *
673     * @param string $username The patron username
674     * @param string $password The patron's password
675     *
676     * @throws ILSException
677     * @return mixed          Associative array of patron info on successful login,
678     * null on unsuccessful login.
679     */
680    public function patronLogin($username, $password)
681    {
682        $patron = [];
683        $row = '';
684
685        $stored_hash = '';
686        try {
687            $sql = 'select password from borrowers where userid = :username';
688            $sqlStmt = $this->db->prepare($sql);
689            $sqlStmt->execute([':username' => $username]);
690            $row = $sqlStmt->fetch();
691            if ($row) {
692                $stored_hash = $row['password'];
693            } else {
694                return null;
695            }
696        } catch (PDOException $e) {
697            $this->throwAsIlsException($e);
698        }
699
700        if (str_starts_with($stored_hash, '$2a$')) {
701            // Newer Koha version that uses bcrypt
702            $db_pwd = crypt($password, $stored_hash);
703        } else {
704            // Koha used to use MD5_BASE64 encoding to save borrowers' passwords,
705            // function 'rtrim' is used to discard trailing '=' signs, suitable for
706            // pushing into MySQL database
707            $db_pwd = rtrim(base64_encode(pack('H*', md5($password))), '=');
708        }
709
710        $sql = 'select borrowernumber as ID, firstname as FNAME, ' .
711            'surname as LNAME, email as EMAIL from borrowers ' .
712            'where userid = :username';
713
714        $parameters = [':username' => $username];
715
716        if ($this->validatePasswords) {
717            $sql .= ' and password = :db_pwd';
718            $parameters[':db_pwd'] = $db_pwd;
719        }
720
721        try {
722            $sqlStmt = $this->db->prepare($sql);
723            $sqlStmt->execute($parameters);
724
725            $row = $sqlStmt->fetch();
726            if ($row) {
727                // NOTE: Here, 'cat_password' => $password is used, password is
728                // saved in a clear text as user provided. If 'cat_password' =>
729                // $db_pwd was used, then password will be saved encrypted as in
730                // 'borrowers' table of 'koha' database
731                $patron = [
732                    'id' => $row['ID'],
733                    'firstname' => $row['FNAME'],
734                    'lastname' => $row['LNAME'],
735                    'cat_username' => $username,
736                    'cat_password' => $password,
737                    'email' => $row['EMAIL'],
738                    'major' => null,
739                    'college' => null,
740                ];
741
742                return $patron;
743            }
744            return null;
745        } catch (PDOException $e) {
746            $this->throwAsIlsException($e);
747        }
748    }
749
750    /**
751     * Convert a database date to a displayable date.
752     *
753     * @param string $date Date to convert
754     *
755     * @return string
756     */
757    public function displayDate($date)
758    {
759        if (empty($date)) {
760            return '';
761        } elseif (preg_match("/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/", $date) === 1) {
762            // YYYY-MM-DD HH:MM:SS
763            return $this->dateConverter->convertToDisplayDate('Y-m-d H:i:s', $date);
764        } elseif (preg_match("/^\d{4}-\d{2}-\d{2}$/", $date) === 1) { // YYYY-MM-DD
765            return $this->dateConverter->convertToDisplayDate('Y-m-d', $date);
766        } else {
767            error_log("Unexpected date format: $date");
768            return $date;
769        }
770    }
771
772    /**
773     * Convert a database datetime to a displayable date and time.
774     *
775     * @param string $date Datetime to convert
776     *
777     * @return string
778     */
779    public function displayDateTime($date)
780    {
781        if (empty($date)) {
782            return '';
783        } elseif (preg_match("/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/", $date) === 1) {
784            // YYYY-MM-DD HH:MM:SS
785            return
786                $this->dateConverter->convertToDisplayDateAndTime(
787                    'Y-m-d H:i:s',
788                    $date
789                );
790        } else {
791            error_log("Unexpected date format: $date");
792            return $date;
793        }
794    }
795
796    /**
797     * Public Function which retrieves renew, hold and cancel settings from the
798     * driver ini file.
799     *
800     * @param string $function The name of the feature to be checked
801     * @param array  $params   Optional feature-specific parameters (array)
802     *
803     * @return array An array with key-value pairs.
804     *
805     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
806     */
807    public function getConfig($function, $params = [])
808    {
809        if ('getMyTransactionHistory' === $function) {
810            if (empty($this->config['TransactionHistory']['enabled'])) {
811                return false;
812            }
813            return [
814                'max_results' => 100,
815                'sort' => [
816                    'checkout desc' => 'sort_checkout_date_desc',
817                    'checkout asc' => 'sort_checkout_date_asc',
818                    'return desc' => 'sort_return_date_desc',
819                    'return asc' => 'sort_return_date_asc',
820                    'due desc' => 'sort_due_date_desc',
821                    'due asc' => 'sort_due_date_asc',
822                ],
823                'default_sort' => 'checkout desc',
824            ];
825        }
826        return $this->config[$function] ?? false;
827    }
828}