Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.79% |
7 / 884 |
|
5.00% |
2 / 40 |
CRAP | |
0.00% |
0 / 1 |
KohaILSDI | |
0.79% |
7 / 884 |
|
5.00% |
2 / 40 |
37706.61 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
init | |
6.90% |
2 / 29 |
|
0.00% |
0 / 1 |
25.18 | |||
initDb | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
20 | |||
getDb | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
tableExists | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getCacheKey | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getField | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
makeRequest | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
20 | |||
makeIlsdiRequest | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
30 | |||
toKohaDate | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getConfig | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
12 | |||
getPickUpLocations | |
0.00% |
0 / 38 |
|
0.00% |
0 / 1 |
156 | |||
getDefaultPickUpLocation | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
placeHold | |
0.00% |
0 / 62 |
|
0.00% |
0 / 1 |
132 | |||
getHolding | |
0.00% |
0 / 147 |
|
0.00% |
0 / 1 |
930 | |||
getNewItems | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
getMyFines | |
0.00% |
0 / 98 |
|
0.00% |
0 / 1 |
702 | |||
getMyFinesILS | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
6 | |||
getMyHolds | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
12 | |||
getCancelHoldDetails | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
cancelHolds | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
12 | |||
getMyProfile | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
6 | |||
getAccountBlocks | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
getMyTransactionHistory | |
0.00% |
0 / 50 |
|
0.00% |
0 / 1 |
132 | |||
getMyTransactions | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
30 | |||
getRenewDetails | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renewMyItems | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
12 | |||
getPurchaseHistory | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getStatus | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getStatuses | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getSuppressedRecords | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
getDepartments | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getInstructors | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getCourses | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
findReserves | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
56 | |||
patronLogin | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
12 | |||
changePassword | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
20 | |||
displayDate | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
displayDateTime | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
supportsMethod | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | /** |
4 | * KohaILSDI ILS Driver |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Alex Sassmannshausen, PTFS Europe 2014. |
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 Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> |
26 | * @author Tom Misilo <misilot@fit.edu> |
27 | * @author Josef Moravec <josef.moravec@gmail.com> |
28 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
29 | * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki |
30 | */ |
31 | |
32 | namespace VuFind\ILS\Driver; |
33 | |
34 | use Laminas\Log\LoggerAwareInterface; |
35 | use PDO; |
36 | use PDOException; |
37 | use VuFind\Date\DateException; |
38 | use VuFind\Exception\ILS as ILSException; |
39 | use VuFindHttp\HttpServiceAwareInterface; |
40 | |
41 | use function array_slice; |
42 | use function count; |
43 | use function in_array; |
44 | use function intval; |
45 | use function is_callable; |
46 | |
47 | /** |
48 | * VuFind Driver for Koha, using web APIs (ILSDI) |
49 | * |
50 | * Minimum Koha Version: 3.18.6 |
51 | * |
52 | * @category VuFind |
53 | * @package ILS_Drivers |
54 | * @author Alex Sassmannshausen <alex.sassmannshausen@ptfs-europe.com> |
55 | * @author Tom Misilo <misilot@fit.edu> |
56 | * @author Josef Moravec <josef.moravec@gmail.com> |
57 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
58 | * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki |
59 | */ |
60 | class KohaILSDI extends AbstractBase implements HttpServiceAwareInterface, LoggerAwareInterface |
61 | { |
62 | use \VuFind\Cache\CacheTrait { |
63 | getCacheKey as protected getBaseCacheKey; |
64 | } |
65 | use \VuFindHttp\HttpServiceAwareTrait; |
66 | use \VuFind\Log\LoggerAwareTrait; |
67 | |
68 | /** |
69 | * Web services host |
70 | * |
71 | * @var string |
72 | */ |
73 | protected $host; |
74 | |
75 | /** |
76 | * ILS base URL |
77 | * |
78 | * @var string |
79 | */ |
80 | protected $ilsBaseUrl; |
81 | |
82 | /** |
83 | * Location codes |
84 | * |
85 | * @var array |
86 | */ |
87 | protected $locations; |
88 | |
89 | /** |
90 | * Codes of locations available for pickup |
91 | * |
92 | * @var array |
93 | */ |
94 | protected $pickupEnableBranchcodes; |
95 | |
96 | /** |
97 | * Codes of locations always should be available |
98 | * - For example reference material or material |
99 | * not for loan |
100 | * |
101 | * @var array |
102 | */ |
103 | protected $availableLocationsDefault; |
104 | |
105 | /** |
106 | * Default location code |
107 | * |
108 | * @var string |
109 | */ |
110 | protected $defaultLocation; |
111 | |
112 | /** |
113 | * Database connection |
114 | * |
115 | * @var PDO |
116 | */ |
117 | protected $db; |
118 | |
119 | /** |
120 | * Date converter object |
121 | * |
122 | * @var \VuFind\Date\Converter |
123 | */ |
124 | protected $dateConverter; |
125 | |
126 | /** |
127 | * Should validate passwords against Koha system? |
128 | * |
129 | * @var bool |
130 | */ |
131 | protected $validatePasswords; |
132 | |
133 | /** |
134 | * Authorised values category for location, defaults to 'LOC' |
135 | * |
136 | * @var string |
137 | */ |
138 | protected $locationAuthorisedValuesCategory; |
139 | |
140 | /** |
141 | * Default terms for block types, can be overridden by configuration |
142 | * |
143 | * @var array |
144 | */ |
145 | protected $blockTerms = [ |
146 | 'SUSPENSION' => 'Account Suspended', |
147 | 'OVERDUES' => 'Account Blocked (Overdue Items)', |
148 | 'MANUAL' => 'Account Blocked', |
149 | 'DISCHARGE' => 'Account Blocked for Discharge', |
150 | ]; |
151 | |
152 | /** |
153 | * Display comments for patron debarments, see KohaILSDI.ini |
154 | * |
155 | * @var array |
156 | */ |
157 | protected $showBlockComments; |
158 | |
159 | /** |
160 | * Should we show permanent location (or current) |
161 | * |
162 | * @var bool |
163 | */ |
164 | protected $showPermanentLocation; |
165 | |
166 | /** |
167 | * Should we show homebranch instead of holdingbranch |
168 | * |
169 | * @var bool |
170 | */ |
171 | protected $showHomebranch; |
172 | |
173 | /** |
174 | * Constructor |
175 | * |
176 | * @param \VuFind\Date\Converter $dateConverter Date converter object |
177 | */ |
178 | public function __construct(\VuFind\Date\Converter $dateConverter) |
179 | { |
180 | $this->dateConverter = $dateConverter; |
181 | } |
182 | |
183 | /** |
184 | * Initialize the driver. |
185 | * |
186 | * Validate configuration and perform all resource-intensive tasks needed to |
187 | * make the driver active. |
188 | * |
189 | * @throws ILSException |
190 | * @return void |
191 | */ |
192 | public function init() |
193 | { |
194 | if (empty($this->config)) { |
195 | throw new ILSException('Configuration needs to be set.'); |
196 | } |
197 | |
198 | // Base for API address |
199 | $this->host = $this->config['Catalog']['host'] ?? 'localhost'; |
200 | |
201 | // Storing the base URL of ILS |
202 | $this->ilsBaseUrl = $this->config['Catalog']['url'] ?? ''; |
203 | |
204 | // Default location defined in 'KohaILSDI.ini' |
205 | $this->defaultLocation |
206 | = $this->config['Holds']['defaultPickUpLocation'] ?? null; |
207 | |
208 | $this->pickupEnableBranchcodes |
209 | = $this->config['Holds']['pickupLocations'] ?? []; |
210 | |
211 | // Locations that should default to available, defined in 'KohaILSDI.ini' |
212 | $this->availableLocationsDefault |
213 | = $this->config['Other']['availableLocations'] ?? []; |
214 | |
215 | // If we are using SAML/Shibboleth for authentication for both ourselves |
216 | // and Koha then we can't validate the patrons passwords against Koha as |
217 | // they won't have one. (Double negative logic used so that if the config |
218 | // option isn't present in KohaILSDI.ini then ILS passwords will be |
219 | // validated) |
220 | $this->validatePasswords |
221 | = empty($this->config['Catalog']['dontValidatePasswords']); |
222 | |
223 | // The Authorised Values Category use for locations should default to 'LOC' |
224 | $this->locationAuthorisedValuesCategory |
225 | = $this->config['Catalog']['locationAuthorisedValuesCategory'] ?? 'LOC'; |
226 | |
227 | $this->showPermanentLocation |
228 | = $this->config['Catalog']['showPermanentLocation'] ?? false; |
229 | |
230 | $this->showHomebranch = $this->config['Catalog']['showHomebranch'] ?? false; |
231 | |
232 | $this->debug('Config Summary:'); |
233 | $this->debug('DB Host: ' . $this->host); |
234 | $this->debug('ILS URL: ' . $this->ilsBaseUrl); |
235 | $this->debug('Locations: ' . $this->locations); |
236 | $this->debug('Default Location: ' . $this->defaultLocation); |
237 | |
238 | // Now override the default with any defined in the `KohaILSDI.ini` config |
239 | // file |
240 | foreach (['SUSPENSION','OVERDUES','MANUAL','DISCHARGE'] as $blockType) { |
241 | if (!empty($this->config['Blocks'][$blockType])) { |
242 | $this->blockTerms[$blockType] = $this->config['Blocks'][$blockType]; |
243 | } |
244 | } |
245 | |
246 | // Allow the users to set if an account block's comments should be included |
247 | // by setting the block type to true or false () in the `KohaILSDI.ini` |
248 | // config file (defaults to false if not present) |
249 | $this->showBlockComments = []; |
250 | |
251 | foreach (['SUSPENSION','OVERDUES','MANUAL','DISCHARGE'] as $blockType) { |
252 | $this->showBlockComments[$blockType] |
253 | = !empty($this->config['Show_Block_Comments'][$blockType]); |
254 | } |
255 | } |
256 | |
257 | /** |
258 | * Initialize the DB driver. |
259 | * |
260 | * Validate configuration and perform all resource-intensive tasks needed to |
261 | * make the driver active. |
262 | * |
263 | * @throws ILSException |
264 | * @return void |
265 | */ |
266 | protected function initDb() |
267 | { |
268 | if (empty($this->config)) { |
269 | throw new ILSException('Configuration needs to be set.'); |
270 | } |
271 | |
272 | //Connect to MySQL |
273 | try { |
274 | $this->db = new PDO( |
275 | 'mysql:host=' . $this->host . |
276 | ';port=' . $this->config['Catalog']['port'] . |
277 | ';dbname=' . $this->config['Catalog']['database'], |
278 | $this->config['Catalog']['username'], |
279 | $this->config['Catalog']['password'], |
280 | [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8'] |
281 | ); |
282 | |
283 | // Throw PDOExceptions if something goes wrong |
284 | $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); |
285 | //Return result set like mysql_fetch_assoc() |
286 | $this->db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); |
287 | // set communication encoding to utf8 |
288 | $this->db->exec('SET NAMES utf8'); |
289 | |
290 | // Drop the ONLY_FULL_GROUP_BY entry from sql_mode as it breaks this |
291 | // ILS Driver on modern |
292 | $setSqlModes = $this->db->prepare('SET sql_mode = :sqlMode'); |
293 | |
294 | $sqlModes = $this->db->query('SELECT @@sql_mode'); |
295 | foreach ($sqlModes as $row) { |
296 | $sqlMode = implode( |
297 | ',', |
298 | array_filter( |
299 | explode(',', $row['@@sql_mode']), |
300 | function ($mode) { |
301 | return $mode != 'ONLY_FULL_GROUP_BY'; |
302 | } |
303 | ) |
304 | ); |
305 | $setSqlModes->execute(['sqlMode' => $sqlMode]); |
306 | } |
307 | } catch (PDOException $e) { |
308 | $this->debug('Connection failed: ' . $e->getMessage()); |
309 | $this->throwAsIlsException($e); |
310 | } |
311 | |
312 | $this->debug('Connected to DB'); |
313 | } |
314 | |
315 | /** |
316 | * Get the database connection (and make sure it is initialized). |
317 | * |
318 | * @return PDO |
319 | */ |
320 | protected function getDb() |
321 | { |
322 | if (!$this->db) { |
323 | $this->initDb(); |
324 | } |
325 | return $this->db; |
326 | } |
327 | |
328 | /** |
329 | * Check if a table exists in the current database. |
330 | * |
331 | * @param string $table Table to search for. |
332 | * |
333 | * @return bool |
334 | */ |
335 | protected function tableExists($table) |
336 | { |
337 | $cacheKey = "kohailsdi-tables-$table"; |
338 | $cachedValue = $this->getCachedData($cacheKey); |
339 | if ($cachedValue !== null) { |
340 | return $cachedValue; |
341 | } |
342 | |
343 | $returnValue = false; |
344 | |
345 | // Try a select statement against the table |
346 | // Run it in try/catch in case PDO is in ERRMODE_EXCEPTION. |
347 | try { |
348 | $result = $this->getDb()->query("SELECT 1 FROM $table LIMIT 1"); |
349 | // Result is FALSE (no table found) or PDOStatement Object (table found) |
350 | $returnValue = $result !== false; |
351 | } catch (PDOException $e) { |
352 | // We got an exception == table not found |
353 | $returnValue = false; |
354 | } |
355 | |
356 | $this->putCachedData($cacheKey, $returnValue); |
357 | return $returnValue; |
358 | } |
359 | |
360 | /** |
361 | * Koha ILS-DI driver specific override of method to ensure uniform cache keys |
362 | * for cached VuFind objects. |
363 | * |
364 | * @param string|null $suffix Optional suffix that will get appended to the |
365 | * object class name calling getCacheKey() |
366 | * |
367 | * @return string |
368 | */ |
369 | protected function getCacheKey($suffix = null) |
370 | { |
371 | return $this->getBaseCacheKey( |
372 | md5($this->ilsBaseUrl) . $suffix |
373 | ); |
374 | } |
375 | |
376 | /** |
377 | * Get Field |
378 | * |
379 | * Check $contents is not "", return it; else return $default. |
380 | * |
381 | * @param string $contents string to be checked |
382 | * @param string $default value to return if $contents is "" |
383 | * |
384 | * @return string |
385 | */ |
386 | protected function getField($contents, $default = 'Unknown') |
387 | { |
388 | if ((string)$contents != '') { |
389 | return (string)$contents; |
390 | } else { |
391 | return $default; |
392 | } |
393 | } |
394 | |
395 | /** |
396 | * Make Request |
397 | * |
398 | * Makes a request to the Koha ILSDI API |
399 | * |
400 | * @param string $api_query Query string for request |
401 | * @param string $http_method HTTP method (default = GET) |
402 | * |
403 | * @throws ILSException |
404 | * @return obj |
405 | */ |
406 | protected function makeRequest($api_query, $http_method = 'GET') |
407 | { |
408 | //$url = $this->host . $this->api_path . $api_query; |
409 | |
410 | $url = $this->ilsBaseUrl . '?service=' . $api_query; |
411 | |
412 | $this->debug("URL: '$url'"); |
413 | |
414 | $http_headers = [ |
415 | 'Accept: text/xml', |
416 | 'Accept-encoding: plain', |
417 | ]; |
418 | |
419 | try { |
420 | $client = $this->httpService->createClient($url); |
421 | |
422 | $client->setMethod($http_method); |
423 | $client->setHeaders($http_headers); |
424 | $result = $client->send(); |
425 | } catch (\Exception $e) { |
426 | $this->debug('Result is invalid.'); |
427 | $this->throwAsIlsException($e); |
428 | } |
429 | |
430 | if (!$result->isSuccess()) { |
431 | $this->debug('Result is invalid.'); |
432 | throw new ILSException('HTTP error'); |
433 | } |
434 | $answer = $result->getBody(); |
435 | //$answer = str_replace('xmlns=', 'ns=', $answer); |
436 | $result = simplexml_load_string($answer); |
437 | if (!$result) { |
438 | $this->debug("XML is not valid, URL: $url"); |
439 | |
440 | throw new ILSException( |
441 | "XML is not valid, URL: $url method: $http_method answer: $answer." |
442 | ); |
443 | } |
444 | return $result; |
445 | } |
446 | |
447 | /** |
448 | * Make Ilsdi Request Array |
449 | * |
450 | * Makes a request to the Koha ILSDI API |
451 | * |
452 | * @param string $service Called function (GetAvailability, |
453 | * GetRecords, |
454 | * GetAuthorityRecords, |
455 | * LookupPatron, |
456 | * AuthenticatePatron, |
457 | * GetPatronInfo, |
458 | * GetPatronStatus, |
459 | * GetServices, |
460 | * RenewLoan, |
461 | * HoldTitle, |
462 | * HoldItem, |
463 | * CancelHold) |
464 | * @param array $params Key is parameter name, value is parameter value |
465 | * @param string $http_method HTTP method (default = GET) |
466 | * |
467 | * @throws ILSException |
468 | * @return obj |
469 | */ |
470 | protected function makeIlsdiRequest($service, $params, $http_method = 'GET') |
471 | { |
472 | $start = microtime(true); |
473 | $url = $this->ilsBaseUrl . '?service=' . $service; |
474 | foreach ($params as $paramname => $paramvalue) { |
475 | $url .= "&$paramname=" . urlencode($paramvalue); |
476 | } |
477 | |
478 | $this->debug("URL: '$url'"); |
479 | |
480 | $http_headers = [ |
481 | 'Accept: text/xml', |
482 | 'Accept-encoding: plain', |
483 | ]; |
484 | |
485 | try { |
486 | $client = $this->httpService->createClient($url); |
487 | $client->setMethod($http_method); |
488 | $client->setHeaders($http_headers); |
489 | $result = $client->send(); |
490 | } catch (\Exception $e) { |
491 | $this->debug('Result is invalid.'); |
492 | $this->throwAsIlsException($e); |
493 | } |
494 | |
495 | if (!$result->isSuccess()) { |
496 | $this->debug('Result is invalid.'); |
497 | throw new ILSException('HTTP error'); |
498 | } |
499 | $end = microtime(true); |
500 | $time1 = $end - $start; |
501 | $start = microtime(true); |
502 | $result = simplexml_load_string($result->getBody()); |
503 | if (!$result) { |
504 | $this->debug("XML is not valid, URL: $url"); |
505 | |
506 | throw new ILSException( |
507 | "XML is not valid, URL: $url" |
508 | ); |
509 | } |
510 | $end = microtime(true); |
511 | $time2 = $end - $start; |
512 | $this->debug("Request times: $time1 - $time2"); |
513 | return $result; |
514 | } |
515 | |
516 | /** |
517 | * To Koha Date |
518 | * |
519 | * Turns a display date into a date format expected by Koha. |
520 | * |
521 | * @param ?string $display_date Date to be converted |
522 | * |
523 | * @throws ILSException |
524 | * @return ?string $koha_date |
525 | */ |
526 | protected function toKohaDate(?string $display_date): ?string |
527 | { |
528 | // Convert last interest date from display format to Koha format |
529 | $koha_date = !empty($display_date) |
530 | ? $this->dateConverter->convertFromDisplayDate('Y-m-d', $display_date) |
531 | : null; |
532 | return $koha_date; |
533 | } |
534 | |
535 | /** |
536 | * Public Function which retrieves renew, hold and cancel settings from the |
537 | * driver ini file. |
538 | * |
539 | * @param string $function The name of the feature to be checked |
540 | * @param array $params Optional feature-specific parameters (array) |
541 | * |
542 | * @return array An array with key-value pairs. |
543 | * |
544 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
545 | */ |
546 | public function getConfig($function, $params = []) |
547 | { |
548 | if ('getMyTransactionHistory' === $function) { |
549 | if (empty($this->config['TransactionHistory']['enabled'])) { |
550 | return false; |
551 | } |
552 | return [ |
553 | 'max_results' => 100, |
554 | 'sort' => [ |
555 | 'checkout desc' => 'sort_checkout_date_desc', |
556 | 'checkout asc' => 'sort_checkout_date_asc', |
557 | 'return desc' => 'sort_return_date_desc', |
558 | 'return asc' => 'sort_return_date_asc', |
559 | 'due desc' => 'sort_due_date_desc', |
560 | 'due asc' => 'sort_due_date_asc', |
561 | ], |
562 | 'default_sort' => 'checkout desc', |
563 | ]; |
564 | } |
565 | return $this->config[$function] ?? false; |
566 | } |
567 | |
568 | /** |
569 | * Get Pick Up Locations |
570 | * |
571 | * This is responsible for gettting a list of valid library locations for |
572 | * holds / recall retrieval |
573 | * |
574 | * @param array $patron Patron information returned by the patronLogin |
575 | * method. |
576 | * @param array $holdDetails Optional array, only passed in when getting a list |
577 | * in the context of placing or editing a hold. When placing a hold, it contains |
578 | * most of the same values passed to placeHold, minus the patron data. When |
579 | * editing a hold it contains all the hold information returned by getMyHolds. |
580 | * May be used to limit the pickup options or may be ignored. The driver must |
581 | * not add new options to the return array based on this data or other areas of |
582 | * VuFind may behave incorrectly. |
583 | * |
584 | * @throws ILSException |
585 | * @return array An array of associative arrays with locationID and |
586 | * locationDisplay keys |
587 | * |
588 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
589 | */ |
590 | public function getPickUpLocations($patron = false, $holdDetails = null) |
591 | { |
592 | if (!$this->locations) { |
593 | if (!$this->pickupEnableBranchcodes) { |
594 | // No defaultPickupLocation is defined in config |
595 | // AND no pickupLocations are defined either |
596 | if ( |
597 | isset($holdDetails['item_id']) && (empty($holdDetails['level']) |
598 | || $holdDetails['level'] == 'item') |
599 | ) { |
600 | // We try to get the actual branchcode the item is found at |
601 | $item_id = $holdDetails['item_id']; |
602 | $sql = "SELECT holdingbranch |
603 | FROM items |
604 | WHERE itemnumber=($item_id)"; |
605 | try { |
606 | $sqlSt = $this->getDb()->prepare($sql); |
607 | $sqlSt->execute(); |
608 | $this->pickupEnableBranchcodes = $sqlSt->fetch(); |
609 | } catch (PDOException $e) { |
610 | $this->debug('Connection failed: ' . $e->getMessage()); |
611 | $this->throwAsIlsException($e); |
612 | } |
613 | } elseif ( |
614 | !empty($holdDetails['level']) |
615 | && $holdDetails['level'] == 'title' |
616 | ) { |
617 | // We try to get the actual branchcodes the title is found at |
618 | $id = $holdDetails['id']; |
619 | $sql = "SELECT DISTINCT holdingbranch |
620 | FROM items |
621 | WHERE biblionumber=($id)"; |
622 | try { |
623 | $sqlSt = $this->getDb()->prepare($sql); |
624 | $sqlSt->execute(); |
625 | foreach ($sqlSt->fetchAll() as $row) { |
626 | $this->pickupEnableBranchcodes[] = $row['holdingbranch']; |
627 | } |
628 | } catch (PDOException $e) { |
629 | $this->debug('Connection failed: ' . $e->getMessage()); |
630 | $this->throwAsIlsException($e); |
631 | } |
632 | } |
633 | } |
634 | $branchcodes = "'" . implode( |
635 | "','", |
636 | $this->pickupEnableBranchcodes |
637 | ) . "'"; |
638 | $sql = "SELECT branchcode as locationID, |
639 | branchname as locationDisplay |
640 | FROM branches |
641 | WHERE branchcode IN ($branchcodes)"; |
642 | try { |
643 | $sqlSt = $this->getDb()->prepare($sql); |
644 | $sqlSt->execute(); |
645 | $this->locations = $sqlSt->fetchAll(); |
646 | } catch (PDOException $e) { |
647 | $this->debug('Connection failed: ' . $e->getMessage()); |
648 | $this->throwAsIlsException($e); |
649 | } |
650 | } |
651 | return $this->locations; |
652 | |
653 | // we get them from the API |
654 | // FIXME: Not yet possible: API incomplete. |
655 | // TODO: When API: pull locations dynamically from API. |
656 | /* $response = $this->makeRequest("organizations/branch"); */ |
657 | /* $locations_response_array = $response->OrganizationsGetRows; */ |
658 | /* foreach ($locations_response_array as $location_response) { */ |
659 | /* $locations[] = array( */ |
660 | /* 'locationID' => $location_response->OrganizationID, */ |
661 | /* 'locationDisplay' => $location_response->Name, */ |
662 | /* ); */ |
663 | /* } */ |
664 | } |
665 | |
666 | /** |
667 | * Get Default Pick Up Location |
668 | * |
669 | * Returns the default pick up location set in KohaILSDI.ini |
670 | * |
671 | * @param array $patron Patron information returned by the patronLogin |
672 | * method. |
673 | * @param array $holdDetails Optional array, only passed in when getting a list |
674 | * in the context of placing a hold; contains most of the same values passed to |
675 | * placeHold, minus the patron data. May be used to limit the pickup options |
676 | * or may be ignored. |
677 | * |
678 | * @return string The default pickup location for the patron. |
679 | * |
680 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
681 | */ |
682 | public function getDefaultPickUpLocation($patron = false, $holdDetails = null) |
683 | { |
684 | return $this->defaultLocation; |
685 | } |
686 | |
687 | /** |
688 | * Place Hold |
689 | * |
690 | * Attempts to place a hold or recall on a particular item and returns |
691 | * an array with result details or throws an exception on failure of support |
692 | * classes |
693 | * |
694 | * @param array $holdDetails An array of item and patron data |
695 | * |
696 | * @throws ILSException |
697 | * @return mixed An array of data on the request including |
698 | * whether or not it was successful and a system message (if available) |
699 | */ |
700 | public function placeHold($holdDetails) |
701 | { |
702 | $patron = $holdDetails['patron']; |
703 | $patron_id = $patron['id']; |
704 | $request_location = $patron['ip'] ?? '127.0.0.1'; |
705 | $bib_id = $holdDetails['id']; |
706 | $item_id = $holdDetails['item_id']; |
707 | $pickup_location = !empty($holdDetails['pickUpLocation']) |
708 | ? $holdDetails['pickUpLocation'] : $this->defaultLocation; |
709 | $level = isset($holdDetails['level']) |
710 | && !empty($holdDetails['level']) ? $holdDetails['level'] : 'item'; |
711 | |
712 | try { |
713 | $needed_before_date = $this->toKohaDate( |
714 | $holdDetails['requiredBy'] ?? null |
715 | ); |
716 | } catch (\Exception $e) { |
717 | return [ |
718 | 'success' => false, |
719 | 'sysMessage' => 'hold_date_invalid', |
720 | ]; |
721 | } |
722 | |
723 | $this->debug('patron: ' . $this->varDump($patron)); |
724 | $this->debug('patron_id: ' . $patron_id); |
725 | $this->debug('request_location: ' . $request_location); |
726 | $this->debug('item_id: ' . $item_id); |
727 | $this->debug('bib_id: ' . $bib_id); |
728 | $this->debug('pickup loc: ' . $pickup_location); |
729 | $this->debug('Needed before date: ' . $needed_before_date); |
730 | $this->debug('Level: ' . $level); |
731 | |
732 | // The following check is mainly required for certain old buggy Koha versions |
733 | // that allowed multiple holds from the same user to the same item |
734 | $sql = 'select count(*) as RCOUNT from reserves where borrowernumber = :rid ' |
735 | . 'and itemnumber = :iid'; |
736 | $reservesSqlStmt = $this->getDb()->prepare($sql); |
737 | $reservesSqlStmt->execute([':rid' => $patron_id, ':iid' => $item_id]); |
738 | $reservesCount = $reservesSqlStmt->fetch()['RCOUNT']; |
739 | |
740 | if ($reservesCount > 0) { |
741 | $this->debug('Fatal error: Patron has already reserved this item.'); |
742 | return [ |
743 | 'success' => false, |
744 | 'sysMessage' => 'It seems you have already reserved this item.', |
745 | ]; |
746 | } |
747 | |
748 | if ($level == 'title') { |
749 | $rqString = "HoldTitle&patron_id=$patron_id&bib_id=$bib_id" |
750 | . "&request_location=$request_location" |
751 | . "&pickup_location=$pickup_location"; |
752 | } else { |
753 | $rqString = "HoldItem&patron_id=$patron_id&bib_id=$bib_id" |
754 | . "&item_id=$item_id" |
755 | . "&pickup_location=$pickup_location"; |
756 | } |
757 | $dateString = empty($needed_before_date) |
758 | ? '' : "&expiry_date=$needed_before_date"; |
759 | |
760 | $rsp = $this->makeRequest($rqString . $dateString); |
761 | |
762 | if ($rsp->{'code'} == 'IllegalParameter' && $dateString != '') { |
763 | // In older versions of Koha, the date parameters were named differently |
764 | // and even never implemented, so if we got IllegalParameter, we know |
765 | // the Koha version is before 20.05 and could retry without expiry_date |
766 | // parameter. See: |
767 | // https://git.koha-community.org/Koha-community/Koha/commit/c8bf308e1b453023910336308d59566359efc535 |
768 | $rsp = $this->makeRequest($rqString); |
769 | } |
770 | //TODO - test this new functionality |
771 | /* |
772 | if ( $level == "title" ) { |
773 | $rsp2 = $this->makeIlsdiRequest("HoldTitle", |
774 | array("patron_id" => $patron_id, |
775 | "bib_id" => $bib_id, |
776 | "request_location" => $request_location, |
777 | "pickup_location" => $pickup_location, |
778 | "pickup_expiry_date" => $needed_before_date, |
779 | "needed_before_date" => $needed_before_date |
780 | )); |
781 | } else { |
782 | $rsp2 = $this->makeIlsdiRequest("HoldItem", |
783 | array("patron_id" => $patron_id, |
784 | "bib_id" => $bib_id, |
785 | "item_id" => $item_id, |
786 | "pickup_location" => $pickup_location, |
787 | "pickup_expiry_date" => $needed_before_date, |
788 | "needed_before_date" => $needed_before_date |
789 | )); |
790 | } |
791 | */ |
792 | $this->debug('Title: ' . $rsp->{'title'}); |
793 | $this->debug('Pickup Location: ' . $rsp->{'pickup_location'}); |
794 | $this->debug('Code: ' . $rsp->{'code'}); |
795 | |
796 | if ($rsp->{'code'} != '') { |
797 | $this->debug('Error Message: ' . $rsp->{'message'}); |
798 | return [ |
799 | 'success' => false, |
800 | 'sysMessage' => $this->getField($rsp->{'code'}) |
801 | . $holdDetails['level'], |
802 | ]; |
803 | } |
804 | return [ |
805 | 'success' => true, |
806 | //"sysMessage" => $message, |
807 | ]; |
808 | } |
809 | |
810 | /** |
811 | * Get Holding |
812 | * |
813 | * This is responsible for retrieving the holding information of a certain |
814 | * record. |
815 | * |
816 | * @param string $id The record id to retrieve the holdings for |
817 | * @param array $patron Patron data |
818 | * @param array $options Extra options (not currently used) |
819 | * |
820 | * @throws DateException |
821 | * @throws ILSException |
822 | * @return array On success, an associative array with the following |
823 | * keys: id, availability (boolean), status, location, reserve, callnumber, |
824 | * duedate, number, barcode. |
825 | * |
826 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
827 | */ |
828 | public function getHolding($id, array $patron = null, array $options = []) |
829 | { |
830 | $this->debug( |
831 | "Function getHolding($id, " |
832 | . implode(',', (array)$patron) |
833 | . ') called' |
834 | ); |
835 | |
836 | $started = microtime(true); |
837 | |
838 | $holding = []; |
839 | $available = true; |
840 | $duedate = $status = ''; |
841 | $loc = ''; |
842 | $locationField = $this->showPermanentLocation |
843 | ? 'permanent_location' : 'location'; |
844 | |
845 | $sql = "select i.itemnumber as ITEMNO, i.location, |
846 | COALESCE(av.lib_opac,av.lib,av.authorised_value,i.$locationField) |
847 | AS LOCATION, |
848 | i.holdingbranch as HLDBRNCH, i.homebranch as HOMEBRANCH, |
849 | i.reserves as RESERVES, i.itemcallnumber as CALLNO, i.barcode as BARCODE, |
850 | i.copynumber as COPYNO, i.notforloan as NOTFORLOAN, |
851 | i.enumchron AS ENUMCHRON, |
852 | i.itemnotes as PUBLICNOTES, b.frameworkcode as DOCTYPE, |
853 | t.frombranch as TRANSFERFROM, t.tobranch as TRANSFERTO, |
854 | i.itemlost as ITEMLOST, i.itemlost_on AS LOSTON, |
855 | i.stocknumber as STOCKNUMBER |
856 | from items i join biblio b on i.biblionumber = b.biblionumber |
857 | left outer join |
858 | (SELECT itemnumber, frombranch, tobranch from branchtransfers |
859 | where datearrived IS NULL) as t USING (itemnumber) |
860 | left join authorised_values as av |
861 | on i.$locationField = av.authorised_value |
862 | where i.biblionumber = :id |
863 | AND (av.category = :av_category OR av.category IS NULL) |
864 | order by i.itemnumber DESC"; |
865 | $sqlReserves = 'select count(*) as RESERVESCOUNT from reserves ' |
866 | . 'WHERE biblionumber = :id AND found IS NULL'; |
867 | $sqlWaitingReserve = 'select count(*) as WAITING from reserves ' |
868 | . "WHERE itemnumber = :item_id and found = 'W'"; |
869 | if ($this->tableExists('biblio_metadata')) { |
870 | $sqlHoldings = 'SELECT ' |
871 | . 'ExtractValue(( SELECT metadata FROM biblio_metadata ' |
872 | . "WHERE biblionumber = :id AND format='marcxml'), " |
873 | . "'//datafield[@tag=\"866\"]/subfield[@code=\"a\"]') AS MFHD;"; |
874 | } else { |
875 | $sqlHoldings = 'SELECT ExtractValue(( SELECT marcxml FROM biblioitems ' |
876 | . 'WHERE biblionumber = :id), ' |
877 | . "'//datafield[@tag=\"866\"]/subfield[@code=\"a\"]') AS MFHD;"; |
878 | } |
879 | try { |
880 | $itemSqlStmt = $this->getDb()->prepare($sql); |
881 | $itemSqlStmt->execute( |
882 | [ |
883 | ':id' => $id, |
884 | ':av_category' => $this->locationAuthorisedValuesCategory, |
885 | ] |
886 | ); |
887 | $sqlStmtReserves = $this->getDb()->prepare($sqlReserves); |
888 | $sqlStmtWaitingReserve = $this->getDb()->prepare($sqlWaitingReserve); |
889 | $sqlStmtReserves->execute([':id' => $id]); |
890 | $sqlStmtHoldings = $this->getDb()->prepare($sqlHoldings); |
891 | $sqlStmtHoldings->execute([':id' => $id]); |
892 | } catch (PDOException $e) { |
893 | $this->debug('Connection failed: ' . $e->getMessage()); |
894 | $this->throwAsIlsException($e); |
895 | } |
896 | |
897 | $this->debug('Rows count: ' . $itemSqlStmt->rowCount()); |
898 | |
899 | $notes = $sqlStmtHoldings->fetch(); |
900 | $reservesRow = $sqlStmtReserves->fetch(); |
901 | $reservesCount = $reservesRow['RESERVESCOUNT']; |
902 | |
903 | foreach ($itemSqlStmt->fetchAll() as $rowItem) { |
904 | $inum = $rowItem['ITEMNO']; |
905 | $sqlStmtWaitingReserve->execute([':item_id' => $inum]); |
906 | $waitingReserveRow = $sqlStmtWaitingReserve->fetch(); |
907 | $waitingReserve = $waitingReserveRow['WAITING']; |
908 | if ($rowItem['LOCATION'] == 'PROC') { |
909 | $available = false; |
910 | $status = 'In processing'; |
911 | $duedate = ''; |
912 | } else { |
913 | $sql = 'select date_due as DUEDATE from issues |
914 | where itemnumber = :inum'; |
915 | switch ($rowItem['NOTFORLOAN']) { |
916 | case 0: |
917 | // If the item is available for loan, then check its current |
918 | // status |
919 | $issueSqlStmt = $this->getDb()->prepare($sql); |
920 | $issueSqlStmt->execute([':inum' => $inum]); |
921 | $rowIssue = $issueSqlStmt->fetch(); |
922 | if ($rowIssue) { |
923 | $available = false; |
924 | $status = 'Checked out'; |
925 | $duedate = $rowIssue['DUEDATE']; |
926 | } else { |
927 | $available = true; |
928 | $status = 'Available'; |
929 | // No due date for an available item |
930 | $duedate = ''; |
931 | } |
932 | break; |
933 | case 1: // The item is not available for loan |
934 | default: |
935 | $available = false; |
936 | $status = 'Not for loan'; |
937 | $duedate = ''; |
938 | break; |
939 | } |
940 | } |
941 | /* |
942 | * If the Item is in any of locations defined by |
943 | * availableLocations[] in the KohaILSDI.ini file |
944 | * the item is considered available |
945 | */ |
946 | |
947 | if (in_array($rowItem['LOCATION'], $this->availableLocationsDefault)) { |
948 | $available = true; |
949 | $duedate = ''; |
950 | $status = 'Available'; |
951 | } |
952 | |
953 | // If Item is Lost or Missing, provide that status |
954 | if ($rowItem['ITEMLOST'] > 0) { |
955 | $available = false; |
956 | $duedate = $rowItem['LOSTON']; |
957 | $status = 'Lost/Missing'; |
958 | } |
959 | |
960 | $duedate_formatted = $this->displayDate($duedate); |
961 | |
962 | if ($rowItem['HLDBRNCH'] == null && $rowItem['HOMEBRANCH'] == null) { |
963 | $loc = 'Unknown'; |
964 | } else { |
965 | $loc = $rowItem['LOCATION']; |
966 | } |
967 | |
968 | if ($this->showHomebranch) { |
969 | $branch = $rowItem['HOMEBRANCH'] ?? $rowItem['HLDBRNCH'] ?? ''; |
970 | } else { |
971 | $branch = $rowItem['HLDBRNCH'] ?? $rowItem['HOMEBRANCH'] ?? ''; |
972 | } |
973 | |
974 | $sqlBranch = 'select branchname as BNAME |
975 | from branches |
976 | where branchcode = :branch'; |
977 | $branchSqlStmt = $this->getDb()->prepare($sqlBranch); |
978 | //Retrieving the full branch name |
979 | if ($loc != 'Unknown') { |
980 | $branchSqlStmt->execute([':branch' => $branch]); |
981 | $row = $branchSqlStmt->fetch(); |
982 | if ($row) { |
983 | $loc = $row['BNAME'] . ' - ' . $loc; |
984 | } |
985 | } |
986 | |
987 | $onTransfer = false; |
988 | if ( |
989 | ($rowItem['TRANSFERFROM'] != null) |
990 | && ($rowItem['TRANSFERTO'] != null) |
991 | ) { |
992 | $branchSqlStmt->execute([':branch' => $rowItem['TRANSFERFROM']]); |
993 | $rowFrom = $branchSqlStmt->fetch(); |
994 | $transferfrom = $rowFrom |
995 | ? $rowFrom['BNAME'] : $rowItem['TRANSFERFROM']; |
996 | $branchSqlStmt->execute([':branch' => $rowItem['TRANSFERTO']]); |
997 | $rowTo = $branchSqlStmt->fetch(); |
998 | $transferto = $rowTo ? $rowTo['BNAME'] : $rowItem['TRANSFERTO']; |
999 | $status = 'In transit between library locations'; |
1000 | $available = false; |
1001 | $onTransfer = true; |
1002 | } |
1003 | |
1004 | if ($rowItem['DOCTYPE'] == 'PE') { |
1005 | $rowItem['COPYNO'] = $rowItem['PERIONAME']; |
1006 | } |
1007 | if ($waitingReserve) { |
1008 | $available = false; |
1009 | $status = 'Waiting'; |
1010 | $waiting = true; |
1011 | } else { |
1012 | $waiting = false; |
1013 | } |
1014 | $holding[] = [ |
1015 | 'id' => $id, |
1016 | 'availability' => (string)$available, |
1017 | 'item_id' => $rowItem['ITEMNO'], |
1018 | 'status' => $status, |
1019 | 'location' => $loc, |
1020 | 'item_notes' => (null == $rowItem['PUBLICNOTES'] |
1021 | ? null : [ $rowItem['PUBLICNOTES'] ]), |
1022 | 'notes' => $notes['MFHD'], |
1023 | //'reserve' => (null == $rowItem['RESERVES']) |
1024 | // ? 'N' : $rowItem['RESERVES'], |
1025 | 'reserve' => 'N', |
1026 | 'callnumber' => |
1027 | ((null == $rowItem['CALLNO']) || ($rowItem['DOCTYPE'] == 'PE')) |
1028 | ? '' : $rowItem['CALLNO'], |
1029 | 'duedate' => ($onTransfer || $waiting) |
1030 | ? '' : (string)$duedate_formatted, |
1031 | 'barcode' => (null == $rowItem['BARCODE']) |
1032 | ? 'Unknown' : $rowItem['BARCODE'], |
1033 | 'number' => |
1034 | $rowItem['COPYNO'] ?? $rowItem['STOCKNUMBER'] ?? '', |
1035 | 'enumchron' => $rowItem['ENUMCHRON'] ?? null, |
1036 | 'requests_placed' => $reservesCount ? $reservesCount : 0, |
1037 | 'frameworkcode' => $rowItem['DOCTYPE'], |
1038 | ]; |
1039 | } |
1040 | |
1041 | $this->debug( |
1042 | 'Processing finished, rows processed: ' |
1043 | . count($holding) . ', took ' . (microtime(true) - $started) . |
1044 | ' sec' |
1045 | ); |
1046 | |
1047 | return $holding; |
1048 | } |
1049 | |
1050 | /** |
1051 | * This method queries the ILS for new items |
1052 | * |
1053 | * @param int $page Page number of results to retrieve (counting starts at 1) |
1054 | * @param int $limit The size of each page of results to retrieve |
1055 | * @param int $daysOld The maximum age of records to retrieve in days (max. 30) |
1056 | * @param int $fundId optional fund ID to use for limiting results (use a value |
1057 | * returned by getFunds, or exclude for no limit); note that "fund" may be a |
1058 | * misnomer - if funds are not an appropriate way to limit your new item |
1059 | * results, you can return a different set of values from getFunds. The |
1060 | * important thing is that this parameter supports an ID returned by getFunds, |
1061 | * whatever that may mean. |
1062 | * |
1063 | * @return array provides a count and the results of new items. |
1064 | */ |
1065 | public function getNewItems($page, $limit, $daysOld, $fundId = null) |
1066 | { |
1067 | $this->debug("getNewItems called $page|$limit|$daysOld|$fundId"); |
1068 | |
1069 | $items = []; |
1070 | $daysOld = min(abs(intval($daysOld)), 30); |
1071 | $sql = "SELECT distinct biblionumber as id |
1072 | FROM items |
1073 | WHERE itemlost = 0 |
1074 | and dateaccessioned > DATE_ADD(CURRENT_TIMESTAMP, |
1075 | INTERVAL -$daysOld day) |
1076 | ORDER BY dateaccessioned DESC"; |
1077 | |
1078 | $this->debug($sql); |
1079 | |
1080 | $itemSqlStmt = $this->getDb()->prepare($sql); |
1081 | $itemSqlStmt->execute(); |
1082 | |
1083 | $rescount = 0; |
1084 | foreach ($itemSqlStmt->fetchAll() as $rowItem) { |
1085 | $items[] = [ |
1086 | 'id' => $rowItem['id'], |
1087 | ]; |
1088 | $rescount++; |
1089 | } |
1090 | |
1091 | $this->debug($rescount . ' fetched'); |
1092 | |
1093 | $results = array_slice($items, ($page - 1) * $limit, ($page * $limit) - 1); |
1094 | return ['count' => $rescount, 'results' => $results]; |
1095 | } |
1096 | |
1097 | /** |
1098 | * Get Hold Link |
1099 | * |
1100 | * The goal for this method is to return a URL to a "place hold" web page on |
1101 | * the ILS OPAC. This is used for ILSs that do not support an API or method |
1102 | * to place Holds. |
1103 | * |
1104 | * @param string $id The id of the bib record |
1105 | * @param array $details Item details from getHoldings return array |
1106 | * |
1107 | * @return string URL to ILS's OPAC's place hold screen. |
1108 | * |
1109 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1110 | */ |
1111 | /*public function getHoldLink($id, $details) |
1112 | { |
1113 | // Web link of the ILS for placing hold on the item |
1114 | return $this->ilsBaseUrl . "/cgi-bin/koha/opac-reserve.pl?biblionumber=$id"; |
1115 | }*/ |
1116 | |
1117 | /** |
1118 | * Get Patron Fines |
1119 | * |
1120 | * This is responsible for retrieving all fines by a specific patron. |
1121 | * |
1122 | * @param array $patron The patron array from patronLogin |
1123 | * |
1124 | * @throws DateException |
1125 | * @throws ILSException |
1126 | * @return mixed Array of the patron's fines on success. |
1127 | */ |
1128 | public function getMyFines($patron) |
1129 | { |
1130 | $id = 0; |
1131 | $transactionLst = []; |
1132 | $row = $sql = $sqlStmt = ''; |
1133 | try { |
1134 | $id = $patron['id']; |
1135 | $sql = 'SELECT al.amount*100 as amount, ' |
1136 | . 'al.amountoutstanding*100 as balance, ' |
1137 | . 'COALESCE(al.credit_type_code, al.debit_type_code) as fine, ' |
1138 | . 'al.date as createdat, items.biblionumber as id, ' |
1139 | . 'al.description as title, issues.date_due as duedate, ' |
1140 | . 'issues.issuedate as issuedate ' |
1141 | . 'FROM `accountlines` al ' |
1142 | . 'LEFT JOIN items USING (itemnumber) ' |
1143 | . 'LEFT JOIN issues USING (issue_id) ' |
1144 | . 'WHERE al.borrowernumber = :id '; |
1145 | $sqlStmt = $this->getDb()->prepare($sql); |
1146 | $sqlStmt->execute([':id' => $id]); |
1147 | foreach ($sqlStmt->fetchAll() as $row) { |
1148 | switch ($row['fine']) { |
1149 | case 'ACCOUNT': |
1150 | $fineValue = 'Account creation fee'; |
1151 | break; |
1152 | case 'ACCOUNT_RENEW': |
1153 | $fineValue = 'Account renewal fee'; |
1154 | break; |
1155 | case 'LOST': |
1156 | $fineValue = 'Lost item'; |
1157 | break; |
1158 | case 'MANUAL': |
1159 | $fineValue = 'Manual fee'; |
1160 | break; |
1161 | case 'NEW_CARD': |
1162 | $fineValue = 'New card'; |
1163 | break; |
1164 | case 'OVERDUE': |
1165 | $fineValue = 'Fine'; |
1166 | break; |
1167 | case 'PROCESSING': |
1168 | $fineValue = 'Lost item processing fee'; |
1169 | break; |
1170 | case 'RENT': |
1171 | $fineValue = 'Rental fee'; |
1172 | break; |
1173 | case 'RENT_DAILY': |
1174 | $fineValue = 'Daily rental fee'; |
1175 | break; |
1176 | case 'RENT_RENEW': |
1177 | $fineValue = 'Renewal of rental item'; |
1178 | break; |
1179 | case 'RENT_DAILY_RENEW': |
1180 | $fineValue = 'Renewal of daily rental item'; |
1181 | break; |
1182 | case 'RESERVE': |
1183 | $fineValue = 'Hold fee'; |
1184 | break; |
1185 | case 'RESERVE_EXPIRED': |
1186 | $fineValue = 'Hold waiting too long'; |
1187 | break; |
1188 | case 'Payout': |
1189 | $fineValue = 'Payout'; |
1190 | break; |
1191 | case 'PAYMENT': |
1192 | $fineValue = 'Payment'; |
1193 | break; |
1194 | case 'WRITEOFF': |
1195 | $fineValue = 'Writeoff'; |
1196 | break; |
1197 | case 'FORGIVEN': |
1198 | $fineValue = 'Forgiven'; |
1199 | break; |
1200 | case 'CREDIT': |
1201 | $fineValue = 'Credit'; |
1202 | break; |
1203 | case 'LOST_FOUND': |
1204 | $fineValue = 'Lost item fee refund'; |
1205 | break; |
1206 | case 'OVERPAYMENT': |
1207 | $fineValue = 'Overpayment refund'; |
1208 | break; |
1209 | case 'REFUND': |
1210 | $fineValue = 'Refund'; |
1211 | break; |
1212 | case 'CANCELLATION': |
1213 | $fineValue = 'Cancelled charge'; |
1214 | break; |
1215 | default: |
1216 | $fineValue = 'Unknown Charge'; |
1217 | break; |
1218 | } |
1219 | |
1220 | $transactionLst[] = [ |
1221 | 'amount' => $row['amount'], |
1222 | 'checkout' => $this->displayDateTime($row['issuedate']), |
1223 | 'title' => $row['title'], |
1224 | 'fine' => $fineValue, |
1225 | 'balance' => $row['balance'], |
1226 | 'createdate' => $this->displayDate($row['createdat']), |
1227 | 'duedate' => $this->displayDate($row['duedate']), |
1228 | 'id' => $row['id'] ?? -1, |
1229 | ]; |
1230 | } |
1231 | return $transactionLst; |
1232 | } catch (PDOException $e) { |
1233 | $this->throwAsIlsException($e); |
1234 | } |
1235 | } |
1236 | |
1237 | /** |
1238 | * Get Patron Fines |
1239 | * |
1240 | * This is responsible for retrieving all fines by a specific patron. |
1241 | * |
1242 | * @param array $patron The patron array from patronLogin |
1243 | * |
1244 | * @throws DateException |
1245 | * @throws ILSException |
1246 | * @return mixed Array of the patron's fines on success. |
1247 | */ |
1248 | public function getMyFinesILS($patron) |
1249 | { |
1250 | $id = $patron['id']; |
1251 | $fineLst = []; |
1252 | |
1253 | $rsp = $this->makeRequest( |
1254 | "GetPatronInfo&patron_id=$id" . '&show_contact=0&show_fines=1' |
1255 | ); |
1256 | |
1257 | $this->debug('ID: ' . $rsp->{'borrowernumber'}); |
1258 | $this->debug('Chrgs: ' . $rsp->{'charges'}); |
1259 | |
1260 | foreach ($rsp->{'fines'}->{'fine'} ?? [] as $fine) { |
1261 | $fineLst[] = [ |
1262 | 'amount' => 100 * $this->getField($fine->{'amount'}), |
1263 | // FIXME: require accountlines.itemnumber -> issues.issuedate data |
1264 | 'checkout' => 'N/A', |
1265 | 'fine' => $this->getField($fine->{'description'}), |
1266 | 'balance' => 100 * $this->getField($fine->{'amountoutstanding'}), |
1267 | 'createdate' => $this->displayDate($this->getField($fine->{'date'})), |
1268 | // FIXME: require accountlines.itemnumber -> issues.date_due data. |
1269 | 'duedate' => 'N/A', |
1270 | // FIXME: require accountlines.itemnumber -> items.biblionumber data |
1271 | 'id' => 'N/A', |
1272 | ]; |
1273 | } |
1274 | return $fineLst; |
1275 | } |
1276 | |
1277 | /** |
1278 | * Get Patron Holds |
1279 | * |
1280 | * This is responsible for retrieving all holds by a specific patron. |
1281 | * |
1282 | * @param array $patron The patron array from patronLogin |
1283 | * |
1284 | * @throws DateException |
1285 | * @throws ILSException |
1286 | * @return array Array of the patron's holds on success. |
1287 | */ |
1288 | public function getMyHolds($patron) |
1289 | { |
1290 | $id = $patron['id']; |
1291 | $holdLst = []; |
1292 | |
1293 | $rsp = $this->makeRequest( |
1294 | "GetPatronInfo&patron_id=$id" . '&show_contact=0&show_holds=1' |
1295 | ); |
1296 | |
1297 | $this->debug('ID: ' . $rsp->{'borrowernumber'}); |
1298 | |
1299 | foreach ($rsp->{'holds'}->{'hold'} ?? [] as $hold) { |
1300 | $holdLst[] = [ |
1301 | 'id' => $this->getField($hold->{'biblionumber'}), |
1302 | 'location' => $this->getField($hold->{'branchname'}), |
1303 | 'expire' => isset($hold->{'expirationdate'}) |
1304 | ? $this->displayDate( |
1305 | $this->getField($hold->{'expirationdate'}) |
1306 | ) |
1307 | : 'N/A', |
1308 | 'create' => $this->displayDate( |
1309 | $this->getField($hold->{'reservedate'}) |
1310 | ), |
1311 | 'position' => $this->getField($hold->{'priority'}), |
1312 | 'title' => $this->getField($hold->{'title'}), |
1313 | 'available' => ($this->getField($hold->{'found'}) == 'W'), |
1314 | 'reserve_id' => $this->getField($hold->{'reserve_id'}), |
1315 | ]; |
1316 | } |
1317 | return $holdLst; |
1318 | } |
1319 | |
1320 | /** |
1321 | * Get Cancel Hold Details |
1322 | * |
1323 | * In order to cancel a hold, Koha requires the patron details and |
1324 | * an item ID. This function returns the item id as a string. This |
1325 | * value is then used by the CancelHolds function. |
1326 | * |
1327 | * @param array $holdDetails A single hold array from getMyHolds |
1328 | * @param array $patron Patron information from patronLogin |
1329 | * |
1330 | * @return string Data for use in a form field |
1331 | * |
1332 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1333 | */ |
1334 | public function getCancelHoldDetails($holdDetails, $patron = []) |
1335 | { |
1336 | return $holdDetails['reserve_id']; |
1337 | } |
1338 | |
1339 | /** |
1340 | * Cancel Holds |
1341 | * |
1342 | * Attempts to Cancel a hold or recall on a particular item. The |
1343 | * data in $cancelDetails['details'] is determined by getCancelHoldDetails(). |
1344 | * |
1345 | * @param array $cancelDetails An array of item and patron data |
1346 | * |
1347 | * @return array An array of data on each request including |
1348 | * whether or not it was successful and a system message (if available) |
1349 | */ |
1350 | public function cancelHolds($cancelDetails) |
1351 | { |
1352 | $retVal = ['count' => 0, 'items' => []]; |
1353 | $details = $cancelDetails['details']; |
1354 | $patron_id = $cancelDetails['patron']['id']; |
1355 | $request_prefix = 'CancelHold&patron_id=' . $patron_id . '&item_id='; |
1356 | |
1357 | foreach ($details as $cancelItem) { |
1358 | $rsp = $this->makeRequest($request_prefix . $cancelItem); |
1359 | if ($rsp->{'code'} != 'Canceled') { |
1360 | $retVal['items'][$cancelItem] = [ |
1361 | 'success' => false, |
1362 | 'status' => 'hold_cancel_fail', |
1363 | 'sysMessage' => $this->getField($rsp->{'code'}), |
1364 | ]; |
1365 | } else { |
1366 | $retVal['count']++; |
1367 | $retVal['items'][$cancelItem] = [ |
1368 | 'success' => true, |
1369 | 'status' => 'hold_cancel_success', |
1370 | ]; |
1371 | } |
1372 | } |
1373 | return $retVal; |
1374 | } |
1375 | |
1376 | /** |
1377 | * Get Patron Profile |
1378 | * |
1379 | * This is responsible for retrieving the profile for a specific patron. |
1380 | * |
1381 | * @param array $patron The patron array |
1382 | * |
1383 | * @throws ILSException |
1384 | * @return array Array of the patron's profile data on success. |
1385 | */ |
1386 | public function getMyProfile($patron) |
1387 | { |
1388 | $id = $patron['id']; |
1389 | $profile = []; |
1390 | |
1391 | $rsp = $this->makeRequest( |
1392 | "GetPatronInfo&patron_id=$id" . '&show_contact=1' |
1393 | ); |
1394 | |
1395 | $this->debug('Code: ' . $rsp->{'code'}); |
1396 | $this->debug('Cardnumber: ' . $rsp->{'cardnumber'}); |
1397 | |
1398 | if ($rsp->{'code'} != 'PatronNotFound') { |
1399 | $profile = [ |
1400 | 'firstname' => $this->getField($rsp->{'firstname'}), |
1401 | 'lastname' => $this->getField($rsp->{'surname'}), |
1402 | 'address1' => $this->getField($rsp->{'address'}), |
1403 | 'address2' => $this->getField($rsp->{'address2'}), |
1404 | 'zip' => $this->getField($rsp->{'zipcode'}), |
1405 | 'phone' => $this->getField($rsp->{'phone'}), |
1406 | 'group' => $this->getField($rsp->{'categorycode'}), |
1407 | ]; |
1408 | return $profile; |
1409 | } else { |
1410 | $this->debug('Error Message: ' . $rsp->{'message'}); |
1411 | return null; |
1412 | } |
1413 | } |
1414 | |
1415 | /** |
1416 | * Check whether the patron has any blocks on their account. |
1417 | * |
1418 | * @param array $patron Patron data from patronLogin |
1419 | * |
1420 | * @throws ILSException |
1421 | * |
1422 | * @return mixed A boolean false if no blocks are in place and an array |
1423 | * of block reasons if blocks are in place |
1424 | */ |
1425 | public function getAccountBlocks($patron) |
1426 | { |
1427 | $blocks = []; |
1428 | |
1429 | try { |
1430 | $id = $patron['id']; |
1431 | $sql = 'select type as TYPE, comment as COMMENT ' . |
1432 | 'from borrower_debarments ' . |
1433 | 'where (expiration is null or expiration >= NOW()) ' . |
1434 | 'and borrowernumber = :id'; |
1435 | $sqlStmt = $this->getDb()->prepare($sql); |
1436 | $sqlStmt->execute([':id' => $id]); |
1437 | |
1438 | foreach ($sqlStmt->fetchAll() as $row) { |
1439 | $block = empty($this->blockTerms[$row['TYPE']]) |
1440 | ? [$row['TYPE']] |
1441 | : [$this->blockTerms[$row['TYPE']]]; |
1442 | |
1443 | if ( |
1444 | !empty($this->showBlockComments[$row['TYPE']]) |
1445 | && !empty($row['COMMENT']) |
1446 | ) { |
1447 | $block[] = $row['COMMENT']; |
1448 | } |
1449 | |
1450 | $blocks[] = implode(' - ', $block); |
1451 | } |
1452 | } catch (PDOException $e) { |
1453 | $this->throwAsIlsException($e); |
1454 | } |
1455 | |
1456 | return count($blocks) ? $blocks : false; |
1457 | } |
1458 | |
1459 | /** |
1460 | * Get Patron Loan History |
1461 | * |
1462 | * This is responsible for retrieving all historic loans (i.e. items previously |
1463 | * checked out and then returned), for a specific patron. |
1464 | * |
1465 | * @param array $patron The patron array from patronLogin |
1466 | * @param array $params Parameters |
1467 | * |
1468 | * @throws DateException |
1469 | * @throws ILSException |
1470 | * @return array Array of the patron's transactions on success. |
1471 | */ |
1472 | public function getMyTransactionHistory($patron, $params) |
1473 | { |
1474 | $id = 0; |
1475 | $historicLoans = []; |
1476 | $row = $sql = $sqlStmt = ''; |
1477 | try { |
1478 | $id = $patron['id']; |
1479 | |
1480 | // Get total count first |
1481 | $sql = 'select count(*) as cnt from old_issues ' . |
1482 | 'where old_issues.borrowernumber = :id'; |
1483 | $sqlStmt = $this->getDb()->prepare($sql); |
1484 | $sqlStmt->execute([':id' => $id]); |
1485 | $totalCount = $sqlStmt->fetch()['cnt']; |
1486 | |
1487 | // Get rows |
1488 | $limit = isset($params['limit']) ? (int)$params['limit'] : 50; |
1489 | $start = isset($params['page']) |
1490 | ? ((int)$params['page'] - 1) * $limit : 0; |
1491 | if (isset($params['sort'])) { |
1492 | $parts = explode(' ', $params['sort'], 2); |
1493 | switch ($parts[0]) { |
1494 | case 'return': |
1495 | $sort = 'RETURNED'; |
1496 | break; |
1497 | case 'due': |
1498 | $sort = 'DUEDATE'; |
1499 | break; |
1500 | default: |
1501 | $sort = 'ISSUEDATE'; |
1502 | break; |
1503 | } |
1504 | $sort .= isset($parts[1]) && 'asc' === $parts[1] ? ' asc' : ' desc'; |
1505 | } else { |
1506 | $sort = 'ISSUEDATE desc'; |
1507 | } |
1508 | $sql = 'select old_issues.issuedate as ISSUEDATE, ' . |
1509 | 'old_issues.date_due as DUEDATE, items.biblionumber as ' . |
1510 | 'BIBNO, items.barcode BARCODE, old_issues.returndate as RETURNED, ' . |
1511 | 'biblio.title as TITLE ' . |
1512 | 'from old_issues join items ' . |
1513 | 'on old_issues.itemnumber = items.itemnumber ' . |
1514 | 'join biblio on items.biblionumber = biblio.biblionumber ' . |
1515 | 'where old_issues.borrowernumber = :id ' . |
1516 | "order by $sort limit $start,$limit"; |
1517 | $sqlStmt = $this->getDb()->prepare($sql); |
1518 | |
1519 | $sqlStmt->execute([':id' => $id]); |
1520 | foreach ($sqlStmt->fetchAll() as $row) { |
1521 | $historicLoans[] = [ |
1522 | 'title' => $row['TITLE'], |
1523 | 'checkoutDate' => $this->displayDateTime($row['ISSUEDATE']), |
1524 | 'dueDate' => $this->displayDateTime($row['DUEDATE']), |
1525 | 'id' => $row['BIBNO'], |
1526 | 'barcode' => $row['BARCODE'], |
1527 | 'returnDate' => $this->displayDateTime($row['RETURNED']), |
1528 | ]; |
1529 | } |
1530 | } catch (PDOException $e) { |
1531 | $this->throwAsIlsException($e); |
1532 | } |
1533 | return [ |
1534 | 'count' => $totalCount, |
1535 | 'transactions' => $historicLoans, |
1536 | ]; |
1537 | } |
1538 | |
1539 | /** |
1540 | * Get Patron Transactions |
1541 | * |
1542 | * This is responsible for retrieving all transactions (i.e. checked out items) |
1543 | * by a specific patron. |
1544 | * |
1545 | * @param array $patron The patron array from patronLogin |
1546 | * |
1547 | * @throws DateException |
1548 | * @throws ILSException |
1549 | * @return array Array of the patron's transactions on success. |
1550 | */ |
1551 | public function getMyTransactions($patron) |
1552 | { |
1553 | $id = $patron['id']; |
1554 | $transactionLst = []; |
1555 | $start = microtime(true); |
1556 | $rsp = $this->makeRequest( |
1557 | "GetPatronInfo&patron_id=$id" . '&show_contact=0&show_loans=1' |
1558 | ); |
1559 | $end = microtime(true); |
1560 | $requestTimes = [$end - $start]; |
1561 | |
1562 | $this->debug('ID: ' . $rsp->{'borrowernumber'}); |
1563 | |
1564 | foreach ($rsp->{'loans'}->{'loan'} ?? [] as $loan) { |
1565 | $start = microtime(true); |
1566 | $rsp2 = $this->makeIlsdiRequest( |
1567 | 'GetServices', |
1568 | [ |
1569 | 'patron_id' => $id, |
1570 | 'item_id' => $this->getField($loan->{'itemnumber'}), |
1571 | ] |
1572 | ); |
1573 | $end = microtime(true); |
1574 | $requestTimes[] = $end - $start; |
1575 | $renewable = false; |
1576 | foreach ($rsp2->{'AvailableFor'} ?? [] as $service) { |
1577 | if ($this->getField((string)$service) == 'loan renewal') { |
1578 | $renewable = true; |
1579 | } |
1580 | } |
1581 | |
1582 | $transactionLst[] = [ |
1583 | 'duedate' => $this->displayDate( |
1584 | $this->getField($loan->{'date_due'}) |
1585 | ), |
1586 | 'id' => $this->getField($loan->{'biblionumber'}), |
1587 | 'item_id' => $this->getField($loan->{'itemnumber'}), |
1588 | 'barcode' => $this->getField($loan->{'barcode'}), |
1589 | 'renew' => $this->getField($loan->{'renewals'}, '0'), |
1590 | 'renewable' => $renewable, |
1591 | ]; |
1592 | } |
1593 | foreach ($requestTimes as $time) { |
1594 | $this->debug("Request time: $time"); |
1595 | } |
1596 | return $transactionLst; |
1597 | } |
1598 | |
1599 | /** |
1600 | * Get Renew Details |
1601 | * |
1602 | * In order to renew an item, Koha requires the patron details and |
1603 | * an item id. This function returns the item id as a string which |
1604 | * is then used as submitted form data in checkedOut.php. This |
1605 | * value is then extracted by the RenewMyItems function. |
1606 | * |
1607 | * @param array $checkOutDetails An array of item data |
1608 | * |
1609 | * @return string Data for use in a form field |
1610 | */ |
1611 | public function getRenewDetails($checkOutDetails) |
1612 | { |
1613 | return $checkOutDetails['item_id']; |
1614 | } |
1615 | |
1616 | /** |
1617 | * Renew My Items |
1618 | * |
1619 | * Function for attempting to renew a patron's items. The data in |
1620 | * $renewDetails['details'] is determined by getRenewDetails(). |
1621 | * |
1622 | * @param array $renewDetails An array of data required for |
1623 | * renewing items including the Patron ID and an array of renewal |
1624 | * IDS |
1625 | * |
1626 | * @return array An array of renewal information keyed by item ID |
1627 | */ |
1628 | public function renewMyItems($renewDetails) |
1629 | { |
1630 | $retVal = ['blocks' => false, 'details' => []]; |
1631 | $details = $renewDetails['details']; |
1632 | $patron_id = $renewDetails['patron']['id']; |
1633 | $request_prefix = 'RenewLoan&patron_id=' . $patron_id . '&item_id='; |
1634 | |
1635 | foreach ($details as $renewItem) { |
1636 | $rsp = $this->makeRequest($request_prefix . $renewItem); |
1637 | if ($rsp->{'success'} != '0') { |
1638 | [$date, $time] |
1639 | = explode(' ', $this->getField($rsp->{'date_due'})); |
1640 | $retVal['details'][$renewItem] = [ |
1641 | 'success' => true, |
1642 | 'new_date' => $this->displayDate($date), |
1643 | 'new_time' => $time, |
1644 | 'item_id' => $renewItem, |
1645 | ]; |
1646 | } else { |
1647 | $retVal['details'][$renewItem] = [ |
1648 | 'success' => false, |
1649 | 'new_date' => false, |
1650 | 'item_id' => $renewItem, |
1651 | //"sysMessage" => $this->getField($rsp->{'error'}), |
1652 | ]; |
1653 | } |
1654 | } |
1655 | return $retVal; |
1656 | } |
1657 | |
1658 | /** |
1659 | * Get Purchase History |
1660 | * |
1661 | * This is responsible for retrieving the acquisitions history data for the |
1662 | * specific record (usually recently received issues of a serial). |
1663 | * |
1664 | * @param string $id The record id to retrieve the info for |
1665 | * |
1666 | * @throws ILSException |
1667 | * @return array An array with the acquisitions data on success. |
1668 | * |
1669 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1670 | */ |
1671 | public function getPurchaseHistory($id) |
1672 | { |
1673 | try { |
1674 | $sql = "SELECT b.title, b.biblionumber, |
1675 | CONCAT(s.publisheddate, ' / ',s.serialseq) |
1676 | AS 'date and enumeration' |
1677 | FROM serial s |
1678 | LEFT JOIN biblio b USING (biblionumber) |
1679 | WHERE s.STATUS=2 and b.biblionumber = :id |
1680 | ORDER BY s.publisheddate DESC"; |
1681 | |
1682 | $sqlStmt = $this->getDb()->prepare($sql); |
1683 | $sqlStmt->execute(['id' => $id]); |
1684 | |
1685 | $result = []; |
1686 | foreach ($sqlStmt->fetchAll() as $rowItem) { |
1687 | $result[] = ['issue' => $rowItem['date and enumeration']]; |
1688 | } |
1689 | } catch (PDOException $e) { |
1690 | $this->throwAsIlsException($e); |
1691 | } |
1692 | return $result; |
1693 | } |
1694 | |
1695 | /** |
1696 | * Get Status |
1697 | * |
1698 | * This is responsible for retrieving the status information of a certain |
1699 | * record. |
1700 | * |
1701 | * @param string $id The record id to retrieve the holdings for |
1702 | * |
1703 | * @throws ILSException |
1704 | * @return mixed On success, an associative array with the following keys: |
1705 | * id, availability (boolean), status, location, reserve, callnumber. |
1706 | */ |
1707 | public function getStatus($id) |
1708 | { |
1709 | return $this->getHolding($id); |
1710 | } |
1711 | |
1712 | /** |
1713 | * Get Statuses |
1714 | * |
1715 | * This is responsible for retrieving the status information for a |
1716 | * collection of records. |
1717 | * |
1718 | * @param array $idLst The array of record ids to retrieve the status for |
1719 | * |
1720 | * @throws ILSException |
1721 | * @return array An array of getStatus() return values on success. |
1722 | */ |
1723 | public function getStatuses($idLst) |
1724 | { |
1725 | $this->debug('IDs:' . implode(',', $idLst)); |
1726 | |
1727 | $statusLst = []; |
1728 | foreach ($idLst as $id) { |
1729 | $statusLst[] = $this->getStatus($id); |
1730 | } |
1731 | return $statusLst; |
1732 | } |
1733 | |
1734 | /** |
1735 | * Get suppressed records. |
1736 | * |
1737 | * @throws ILSException |
1738 | * @return array ID numbers of suppressed records in the system. |
1739 | */ |
1740 | public function getSuppressedRecords() |
1741 | { |
1742 | try { |
1743 | if ($this->tableExists('biblio_metadata')) { |
1744 | $sql = "SELECT biblio.biblionumber AS biblionumber |
1745 | FROM biblio |
1746 | JOIN biblio_metadata USING (biblionumber) |
1747 | WHERE ExtractValue( |
1748 | metadata, '//datafield[@tag=\"942\"]/subfield[@code=\"n\"]' ) |
1749 | IN ('Y', '1') |
1750 | AND biblio_metadata.format = 'marcxml'"; |
1751 | } else { |
1752 | $sql = "SELECT biblio.biblionumber AS biblionumber |
1753 | FROM biblioitems |
1754 | JOIN biblio USING (biblionumber) |
1755 | WHERE ExtractValue( |
1756 | marcxml, '//datafield[@tag=\"942\"]/subfield[@code=\"n\"]' ) |
1757 | IN ('Y', '1')"; |
1758 | } |
1759 | $sqlStmt = $this->getDb()->prepare($sql); |
1760 | $sqlStmt->execute(); |
1761 | $result = []; |
1762 | foreach ($sqlStmt->fetchAll() as $rowItem) { |
1763 | $result[] = $rowItem['biblionumber']; |
1764 | } |
1765 | } catch (PDOException $e) { |
1766 | $this->throwAsIlsException($e); |
1767 | } |
1768 | return $result; |
1769 | } |
1770 | |
1771 | /** |
1772 | * Get Departments |
1773 | * |
1774 | * @throws ILSException |
1775 | * @return array An associative array with key = ID, value = dept. name. |
1776 | */ |
1777 | public function getDepartments() |
1778 | { |
1779 | $deptList = []; |
1780 | |
1781 | $sql = 'SELECT DISTINCT department as abv, lib_opac AS DEPARTMENT |
1782 | FROM courses |
1783 | INNER JOIN `authorised_values` |
1784 | ON courses.department = `authorised_values`.`authorised_value`'; |
1785 | try { |
1786 | $sqlStmt = $this->getDb()->prepare($sql); |
1787 | $sqlStmt->execute(); |
1788 | foreach ($sqlStmt->fetchAll() as $rowItem) { |
1789 | $deptList[$rowItem['abv']] = $rowItem['DEPARTMENT']; |
1790 | } |
1791 | } catch (PDOException $e) { |
1792 | $this->throwAsIlsException($e); |
1793 | } |
1794 | return $deptList; |
1795 | } |
1796 | |
1797 | /** |
1798 | * Get Instructors |
1799 | * |
1800 | * @throws ILSException |
1801 | * @return array An associative array with key = ID, value = name. |
1802 | */ |
1803 | public function getInstructors() |
1804 | { |
1805 | $instList = []; |
1806 | |
1807 | $sql = "SELECT DISTINCT borrowernumber, |
1808 | CONCAT(firstname, ' ', surname) AS name |
1809 | FROM course_instructors |
1810 | LEFT JOIN borrowers USING(borrowernumber)"; |
1811 | |
1812 | try { |
1813 | $sqlStmt = $this->getDb()->prepare($sql); |
1814 | $sqlStmt->execute(); |
1815 | foreach ($sqlStmt->fetchAll() as $rowItem) { |
1816 | $instList[$rowItem['borrowernumber']] = $rowItem['name']; |
1817 | } |
1818 | } catch (PDOException $e) { |
1819 | $this->throwAsIlsException($e); |
1820 | } |
1821 | return $instList; |
1822 | } |
1823 | |
1824 | /** |
1825 | * Get Courses |
1826 | * |
1827 | * @throws ILSException |
1828 | * @return array An associative array with key = ID, value = name. |
1829 | */ |
1830 | public function getCourses() |
1831 | { |
1832 | $courseList = []; |
1833 | |
1834 | $sql = "SELECT course_id, |
1835 | CONCAT (course_number, ' - ', course_name) AS course |
1836 | FROM courses |
1837 | WHERE enabled = 1"; |
1838 | try { |
1839 | $sqlStmt = $this->getDb()->prepare($sql); |
1840 | $sqlStmt->execute(); |
1841 | foreach ($sqlStmt->fetchAll() as $rowItem) { |
1842 | $courseList[$rowItem['course_id']] = $rowItem['course']; |
1843 | } |
1844 | } catch (PDOException $e) { |
1845 | $this->throwAsIlsException($e); |
1846 | } |
1847 | return $courseList; |
1848 | } |
1849 | |
1850 | /** |
1851 | * Find Reserves |
1852 | * |
1853 | * Obtain information on course reserves. |
1854 | * |
1855 | * This version of findReserves was contributed by Matthew Hooper and includes |
1856 | * support for electronic reserves (though eReserve support is still a work in |
1857 | * progress). |
1858 | * |
1859 | * @param string $course ID from getCourses (empty string to match all) |
1860 | * @param string $inst ID from getInstructors (empty string to match all) |
1861 | * @param string $dept ID from getDepartments (empty string to match all) |
1862 | * |
1863 | * @throws ILSException |
1864 | * @return array An array of associative arrays representing reserve items. |
1865 | */ |
1866 | public function findReserves($course, $inst, $dept) |
1867 | { |
1868 | $reserveWhere = []; |
1869 | $bindParams = []; |
1870 | if ($course != '') { |
1871 | $reserveWhere[] = 'COURSE_ID = :course'; |
1872 | $bindParams[':course'] = $course; |
1873 | } |
1874 | if ($inst != '') { |
1875 | $reserveWhere[] = 'INSTRUCTOR_ID = :inst'; |
1876 | $bindParams[':inst'] = $inst; |
1877 | } |
1878 | if ($dept != '') { |
1879 | $reserveWhere[] = 'DEPARTMENT_ID = :dept'; |
1880 | $bindParams[':dept'] = $dept; |
1881 | } |
1882 | $reserveWhere = empty($reserveWhere) ? |
1883 | '' : 'HAVING (' . implode(' AND ', $reserveWhere) . ')'; |
1884 | |
1885 | $sql = "SELECT biblionumber AS `BIB_ID`, |
1886 | courses.course_id AS `COURSE_ID`, |
1887 | course_instructors.borrowernumber as `INSTRUCTOR_ID`, |
1888 | courses.department AS `DEPARTMENT_ID` |
1889 | FROM courses |
1890 | INNER JOIN `authorised_values` |
1891 | ON courses.department = `authorised_values`.`authorised_value` |
1892 | INNER JOIN `course_reserves` USING (course_id) |
1893 | INNER JOIN `course_items` USING (ci_id) |
1894 | INNER JOIN `items` USING (itemnumber) |
1895 | INNER JOIN `course_instructors` USING (course_id) |
1896 | INNER JOIN `borrowers` USING (borrowernumber) |
1897 | WHERE courses.enabled = 'yes' " . $reserveWhere; |
1898 | |
1899 | try { |
1900 | $sqlStmt = $this->getDb()->prepare($sql); |
1901 | $sqlStmt->execute($bindParams); |
1902 | $result = []; |
1903 | foreach ($sqlStmt->fetchAll() as $rowItem) { |
1904 | $result[] = $rowItem; |
1905 | } |
1906 | } catch (PDOException $e) { |
1907 | $this->throwAsIlsException($e); |
1908 | } |
1909 | return $result; |
1910 | } |
1911 | |
1912 | /** |
1913 | * Patron Login |
1914 | * |
1915 | * This is responsible for authenticating a patron against the catalog. |
1916 | * |
1917 | * @param string $username The patron username |
1918 | * @param string $password The patron's password |
1919 | * |
1920 | * @throws ILSException |
1921 | * @return mixed Associative array of patron info on successful login, |
1922 | * null on unsuccessful login. |
1923 | */ |
1924 | public function patronLogin($username, $password) |
1925 | { |
1926 | $request = 'LookupPatron' . '&id=' . urlencode($username) |
1927 | . '&id_type=userid'; |
1928 | |
1929 | if ($this->validatePasswords) { |
1930 | $request = 'AuthenticatePatron' . '&username=' |
1931 | . urlencode($username) . '&password=' . $password; |
1932 | } |
1933 | |
1934 | $idObj = $this->makeRequest($request); |
1935 | |
1936 | $this->debug('username: ' . $username); |
1937 | $this->debug('Code: ' . $idObj->{'code'}); |
1938 | $this->debug('ID: ' . $idObj->{'id'}); |
1939 | |
1940 | $id = $this->getField($idObj->{'id'}, 0); |
1941 | if ($id) { |
1942 | $rsp = $this->makeRequest( |
1943 | "GetPatronInfo&patron_id=$id&show_contact=1" |
1944 | ); |
1945 | $profile = [ |
1946 | 'id' => $this->getField($idObj->{'id'}), |
1947 | 'firstname' => $this->getField($rsp->{'firstname'}), |
1948 | 'lastname' => $this->getField($rsp->{'surname'}), |
1949 | 'cat_username' => $username, |
1950 | 'cat_password' => $password, |
1951 | 'email' => $this->getField($rsp->{'email'}), |
1952 | 'major' => null, |
1953 | 'college' => null, |
1954 | ]; |
1955 | return $profile; |
1956 | } else { |
1957 | return null; |
1958 | } |
1959 | } |
1960 | |
1961 | /** |
1962 | * Change Password |
1963 | * |
1964 | * This method changes patron's password |
1965 | * |
1966 | * @param array $detail An associative array with three keys |
1967 | * patron - The patron array from patronLogin |
1968 | * oldPassword - Old password |
1969 | * newPassword - New password |
1970 | * |
1971 | * @return array An associative array with keys: |
1972 | * success - boolean, true if change was made |
1973 | * status - string, A status message - subject to translation |
1974 | */ |
1975 | public function changePassword($detail) |
1976 | { |
1977 | $sql = 'UPDATE borrowers SET password = ? WHERE borrowernumber = ?'; |
1978 | $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; |
1979 | $max = mb_strlen($keyspace, '8bit') - 1; |
1980 | $salt = ''; |
1981 | for ($i = 0; $i < 16; ++$i) { // 16 is length of salt |
1982 | $salt .= $keyspace[random_int(0, $max)]; |
1983 | } |
1984 | $salt = base64_encode($salt); |
1985 | $newPassword_hashed = crypt($detail['newPassword'], '$2a$08$' . $salt); |
1986 | try { |
1987 | $stmt = $this->getDb()->prepare($sql); |
1988 | $result = $stmt->execute( |
1989 | [ $newPassword_hashed, $detail['patron']['id'] ] |
1990 | ); |
1991 | } catch (\Exception $e) { |
1992 | return [ 'success' => false, 'status' => $e->getMessage() ]; |
1993 | } |
1994 | return [ |
1995 | 'success' => $result, |
1996 | 'status' => $result ? 'new_password_success' |
1997 | : 'password_error_not_unique', |
1998 | ]; |
1999 | } |
2000 | |
2001 | /** |
2002 | * Convert a database date to a displayable date. |
2003 | * |
2004 | * @param string $date Date to convert |
2005 | * |
2006 | * @return string |
2007 | */ |
2008 | public function displayDate($date) |
2009 | { |
2010 | if (empty($date)) { |
2011 | return ''; |
2012 | } elseif (preg_match("/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/", $date) === 1) { |
2013 | // YYYY-MM-DD HH:MM:SS |
2014 | return $this->dateConverter->convertToDisplayDate('Y-m-d H:i:s', $date); |
2015 | } elseif (preg_match("/^\d{4}-\d\d-\d\d \d\d:\d\d$/", $date) === 1) { |
2016 | // YYYY-MM-DD HH:MM |
2017 | return $this->dateConverter->convertToDisplayDate('Y-m-d H:i', $date); |
2018 | } elseif (preg_match("/^\d{4}-\d{2}-\d{2}$/", $date) === 1) { // YYYY-MM-DD |
2019 | return $this->dateConverter->convertToDisplayDate('Y-m-d', $date); |
2020 | } else { |
2021 | error_log("Unexpected date format: $date"); |
2022 | return $date; |
2023 | } |
2024 | } |
2025 | |
2026 | /** |
2027 | * Convert a database datetime to a displayable date and time. |
2028 | * |
2029 | * @param string $date Datetime to convert |
2030 | * |
2031 | * @return string |
2032 | */ |
2033 | public function displayDateTime($date) |
2034 | { |
2035 | if (empty($date)) { |
2036 | return ''; |
2037 | } elseif (preg_match("/^\d{4}-\d\d-\d\d \d\d:\d\d:\d\d$/", $date) === 1) { |
2038 | // YYYY-MM-DD HH:MM:SS |
2039 | return |
2040 | $this->dateConverter->convertToDisplayDateAndTime( |
2041 | 'Y-m-d H:i:s', |
2042 | $date |
2043 | ); |
2044 | } elseif (preg_match("/^\d{4}-\d\d-\d\d \d\d:\d\d$/", $date) === 1) { |
2045 | // YYYY-MM-DD HH:MM |
2046 | return |
2047 | $this->dateConverter->convertToDisplayDateAndTime( |
2048 | 'Y-m-d H:i', |
2049 | $date |
2050 | ); |
2051 | } else { |
2052 | error_log("Unexpected date format: $date"); |
2053 | return $date; |
2054 | } |
2055 | } |
2056 | |
2057 | /** |
2058 | * Helper method to determine whether or not a certain method can be |
2059 | * called on this driver. Required method for any smart drivers. |
2060 | * |
2061 | * @param string $method The name of the called method. |
2062 | * @param array $params Array of passed parameters |
2063 | * |
2064 | * @return bool True if the method can be called with the given parameters, |
2065 | * false otherwise. |
2066 | * |
2067 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
2068 | */ |
2069 | public function supportsMethod($method, $params) |
2070 | { |
2071 | // Loan history is only available if properly configured |
2072 | if ($method == 'getMyTransactionHistory') { |
2073 | return !empty($this->config['TransactionHistory']['enabled']); |
2074 | } |
2075 | return is_callable([$this, $method]); |
2076 | } |
2077 | } |