Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
11.98% |
69 / 576 |
|
11.11% |
4 / 36 |
CRAP | |
0.00% |
0 / 1 |
Unicorn | |
11.98% |
69 / 576 |
|
11.11% |
4 / 36 |
13892.96 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
init | |
22.22% |
2 / 9 |
|
0.00% |
0 / 1 |
16.76 | |||
getConfig | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getPickUpLocations | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getDefaultPickUpLocation | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
getRenewDetails | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
renewMyItems | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
42 | |||
getStatus | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
56 | |||
getStatuses | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
20 | |||
getPurchaseHistory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getHolding | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
placeHold | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
20 | |||
patronLogin | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
20 | |||
getMyProfile | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
2 | |||
getMyFines | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
20 | |||
getMyHolds | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
20 | |||
getCancelHoldDetails | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
cancelHolds | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
72 | |||
getMyTransactions | |
0.00% |
0 / 49 |
|
0.00% |
0 / 1 |
72 | |||
getCourses | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
getInstructors | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getDepartments | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
findReserves | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
156 | |||
getNewItems | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
getSuppressedRecords | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
parseStatusLine | |
0.00% |
0 / 53 |
|
0.00% |
0 / 1 |
110 | |||
mapLocation | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
4.12 | |||
mapLibrary | |
50.00% |
2 / 4 |
|
0.00% |
0 / 1 |
4.12 | |||
querySirsi | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
42 | |||
calculateRecallDueDate | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
parseDateTime | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
formatDateTime | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
toUTF8 | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
processMarcHoldingLocation | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
1 | |||
decodeMarcHoldingRecord | |
76.60% |
36 / 47 |
|
0.00% |
0 / 1 |
17.88 | |||
getMarcHoldings | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | /** |
4 | * SirsiDynix Unicorn ILS Driver (VuFind side) |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * This program is free software; you can redistribute it and/or modify |
9 | * it under the terms of the GNU General Public License version 2, |
10 | * as published by the Free Software Foundation. |
11 | * |
12 | * This program is distributed in the hope that it will be useful, |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | * GNU General Public License for more details. |
16 | * |
17 | * You should have received a copy of the GNU General Public License |
18 | * along with this program; if not, write to the Free Software |
19 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
20 | * |
21 | * @category VuFind |
22 | * @package ILS_Drivers |
23 | * @author Tuan Nguyen <tuan@yorku.ca> |
24 | * @author Drew Farrugia <vufind-unicorn-l@lists.lehigh.edu> |
25 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
26 | * @link http://code.google.com/p/vufind-unicorn/ vufind-unicorn project |
27 | */ |
28 | |
29 | namespace VuFind\ILS\Driver; |
30 | |
31 | use VuFind\Date\DateException; |
32 | use VuFind\Exception\ILS as ILSException; |
33 | use VuFind\Marc\MarcCollection; |
34 | use VuFind\Marc\MarcReader; |
35 | |
36 | use function array_key_exists; |
37 | use function array_slice; |
38 | use function count; |
39 | use function floatval; |
40 | use function in_array; |
41 | use function strlen; |
42 | |
43 | /** |
44 | * SirsiDynix Unicorn ILS Driver (VuFind side) |
45 | * |
46 | * IMPORTANT: To use this driver you need to download the SirsiDynix API driver.pl |
47 | * from http://code.google.com/p/vufind-unicorn/ and install it on your Sirsi |
48 | * Unicorn/Symphony server. Please note: currently you will need to download |
49 | * the driver.pl in the yorku branch on google code to use this driver. |
50 | * |
51 | * @category VuFind |
52 | * @package ILS_Drivers |
53 | * @author Tuan Nguyen <tuan@yorku.ca> |
54 | * @author Drew Farrugia <vufind-unicorn-l@lists.lehigh.edu> |
55 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
56 | * @link http://code.google.com/p/vufind-unicorn/ vufind-unicorn project |
57 | **/ |
58 | class Unicorn extends AbstractBase implements |
59 | \VuFindHttp\HttpServiceAwareInterface, |
60 | \VuFind\I18n\HasSorterInterface |
61 | { |
62 | use \VuFindHttp\HttpServiceAwareTrait; |
63 | use \VuFind\I18n\HasSorterTrait; |
64 | |
65 | /** |
66 | * Host |
67 | * |
68 | * @var string |
69 | */ |
70 | protected $host; |
71 | |
72 | /** |
73 | * Port |
74 | * |
75 | * @var string |
76 | */ |
77 | protected $port; |
78 | |
79 | /** |
80 | * Name of API program |
81 | * |
82 | * @var string |
83 | */ |
84 | protected $search_prog; |
85 | |
86 | /** |
87 | * Full URL to API (alternative to host/port/search_prog) |
88 | * |
89 | * @var string |
90 | */ |
91 | protected $url; |
92 | |
93 | /** |
94 | * Date converter object |
95 | * |
96 | * @var \VuFind\Date\Converter |
97 | */ |
98 | protected $dateConverter; |
99 | |
100 | /** |
101 | * Constructor |
102 | * |
103 | * @param \VuFind\Date\Converter $dateConverter Date converter object |
104 | */ |
105 | public function __construct(\VuFind\Date\Converter $dateConverter) |
106 | { |
107 | $this->dateConverter = $dateConverter; |
108 | } |
109 | |
110 | /** |
111 | * Initialize the driver. |
112 | * |
113 | * Validate configuration and perform all resource-intensive tasks needed to |
114 | * make the driver active. |
115 | * |
116 | * @throws ILSException |
117 | * @return void |
118 | */ |
119 | public function init() |
120 | { |
121 | if (empty($this->config)) { |
122 | throw new ILSException('Configuration needs to be set.'); |
123 | } |
124 | |
125 | // allow user to specify the full url to the Sirsi side perl script |
126 | $this->url = $this->config['Catalog']['url']; |
127 | |
128 | // host/port/search_prog kept for backward compatibility |
129 | if ( |
130 | isset($this->config['Catalog']['host']) |
131 | && isset($this->config['Catalog']['port']) |
132 | && isset($this->config['Catalog']['search_prog']) |
133 | ) { |
134 | $this->host = $this->config['Catalog']['host']; |
135 | $this->port = $this->config['Catalog']['port']; |
136 | $this->search_prog = $this->config['Catalog']['search_prog']; |
137 | } |
138 | } |
139 | |
140 | /** |
141 | * Public Function which retrieves renew, hold and cancel settings from the |
142 | * driver ini file. |
143 | * |
144 | * @param string $function The name of the feature to be checked |
145 | * @param array $params Optional feature-specific parameters (array) |
146 | * |
147 | * @return array An array with key-value pairs. |
148 | * |
149 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
150 | */ |
151 | public function getConfig($function, $params = []) |
152 | { |
153 | if (isset($this->config[$function])) { |
154 | $functionConfig = $this->config[$function]; |
155 | } else { |
156 | $functionConfig = false; |
157 | } |
158 | return $functionConfig; |
159 | } |
160 | |
161 | /** |
162 | * Get Pick Up Locations |
163 | * |
164 | * This is responsible for gettting a list of valid library locations for |
165 | * holds / recall retrieval |
166 | * |
167 | * @param array $patron Patron information returned by the patronLogin |
168 | * method. |
169 | * @param array $holdDetails Optional array, only passed in when getting a list |
170 | * in the context of placing or editing a hold. When placing a hold, it contains |
171 | * most of the same values passed to placeHold, minus the patron data. When |
172 | * editing a hold it contains all the hold information returned by getMyHolds. |
173 | * May be used to limit the pickup options or may be ignored. The driver must |
174 | * not add new options to the return array based on this data or other areas of |
175 | * VuFind may behave incorrectly. |
176 | * |
177 | * @throws ILSException |
178 | * @return array An array of associative arrays with locationID and |
179 | * locationDisplay keys |
180 | * |
181 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
182 | */ |
183 | public function getPickUpLocations($patron = false, $holdDetails = null) |
184 | { |
185 | $params = ['query' => 'libraries']; |
186 | $response = $this->querySirsi($params); |
187 | $response = rtrim($response); |
188 | $lines = explode("\n", $response); |
189 | $libraries = []; |
190 | |
191 | foreach ($lines as $line) { |
192 | [$code, $name] = explode('|', $line); |
193 | $libraries[] = [ |
194 | 'locationID' => $code, |
195 | 'locationDisplay' => empty($name) ? $code : $name, |
196 | ]; |
197 | } |
198 | return $libraries; |
199 | } |
200 | |
201 | /** |
202 | * Get Default Pick Up Location |
203 | * |
204 | * Returns the default pick up location set in VoyagerRestful.ini |
205 | * |
206 | * @param array $patron Patron information returned by the patronLogin |
207 | * method. |
208 | * @param array $holdDetails Optional array, only passed in when getting a list |
209 | * in the context of placing a hold; contains most of the same values passed to |
210 | * placeHold, minus the patron data. May be used to limit the pickup options |
211 | * or may be ignored. |
212 | * |
213 | * @return string The default pickup location for the patron. |
214 | * |
215 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
216 | */ |
217 | public function getDefaultPickUpLocation($patron = false, $holdDetails = null) |
218 | { |
219 | if ($patron && isset($patron['library'])) { |
220 | return $patron['library']; |
221 | } |
222 | return $this->config['Holds']['defaultPickupLocation']; |
223 | } |
224 | |
225 | /** |
226 | * Get Renew Details |
227 | * |
228 | * In order to renew an item, Voyager requires the patron details and an item |
229 | * id. This function returns the item id as a string which is then used |
230 | * as submitted form data in checkedOut.php. This value is then extracted by |
231 | * the RenewMyItems function. |
232 | * |
233 | * @param array $checkOutDetails An array of item data |
234 | * |
235 | * @return string Data for use in a form field |
236 | */ |
237 | public function getRenewDetails($checkOutDetails) |
238 | { |
239 | return $checkOutDetails['item_id']; |
240 | } |
241 | |
242 | /** |
243 | * Renew My Items |
244 | * |
245 | * Function for attempting to renew a patron's items. The data in |
246 | * $renewDetails['details'] is determined by getRenewDetails(). |
247 | * |
248 | * @param array $renewDetails An array of data required for renewing items |
249 | * including the Patron ID and an array of renewal IDS |
250 | * |
251 | * @return array An array of renewal information keyed by item ID |
252 | */ |
253 | public function renewMyItems($renewDetails) |
254 | { |
255 | $patron = $renewDetails['patron']; |
256 | $details = $renewDetails['details']; |
257 | |
258 | $chargeKeys = implode(',', $details); |
259 | $params = [ |
260 | 'query' => 'renew_items', 'chargeKeys' => $chargeKeys, |
261 | 'patronId' => $patron['cat_username'], 'pin' => $patron['cat_password'], |
262 | 'library' => $patron['library'], |
263 | ]; |
264 | $response = $this->querySirsi($params); |
265 | |
266 | // process the API response |
267 | if ($response == 'invalid_login') { |
268 | return ['blocks' => ['authentication_error_admin']]; |
269 | } |
270 | |
271 | $results = []; |
272 | $lines = explode("\n", $response); |
273 | foreach ($lines as $line) { |
274 | [$chargeKey, $result] = explode('-----API_RESULT-----', $line); |
275 | $results[$chargeKey] = ['item_id' => $chargeKey]; |
276 | $matches = []; |
277 | preg_match('/\^MN([0-9][0-9][0-9])/', $result, $matches); |
278 | if (isset($matches[1])) { |
279 | $status = $matches[1]; |
280 | if ($status == '214') { |
281 | $results[$chargeKey]['success'] = true; |
282 | } else { |
283 | $results[$chargeKey]['success'] = false; |
284 | $results[$chargeKey]['sysMessage'] |
285 | = $this->config['ApiMessages'][$status]; |
286 | } |
287 | } |
288 | preg_match('/\^CI([^\^]+)\^/', $result, $matches); |
289 | if (isset($matches[1])) { |
290 | [$newDate, $newTime] = explode(',', $matches[1]); |
291 | $results[$chargeKey]['new_date'] = $newDate; |
292 | $results[$chargeKey]['new_time'] = $newTime; |
293 | } |
294 | } |
295 | return ['details' => $results]; |
296 | } |
297 | |
298 | /** |
299 | * Get Status |
300 | * |
301 | * This is responsible for retrieving the status information of a certain |
302 | * record. |
303 | * |
304 | * @param string $id The record id to retrieve the holdings for |
305 | * |
306 | * @throws ILSException |
307 | * @return mixed On success, an associative array with the following keys: |
308 | * id, availability (boolean), status, location, reserve, callnumber. |
309 | */ |
310 | public function getStatus($id) |
311 | { |
312 | $params = ['query' => 'single', 'id' => $id]; |
313 | $response = $this->querySirsi($params); |
314 | if (empty($response)) { |
315 | return []; |
316 | } |
317 | |
318 | // separate the item lines and the MARC holdings records |
319 | $marc_marker = '-----BEGIN MARC-----'; |
320 | $marc_marker_pos = strpos($response, $marc_marker); |
321 | $lines = ($marc_marker_pos !== false) |
322 | ? substr($response, 0, $marc_marker_pos) : ''; |
323 | $marc = ($marc_marker_pos !== false) |
324 | ? substr($response, $marc_marker_pos + strlen($marc_marker)) : ''; |
325 | |
326 | // Initialize item holdings the ones received in MARC holding |
327 | // records |
328 | $items = $this->getMarcHoldings($marc); |
329 | |
330 | // Then add the ones from bibliographic records |
331 | $lines = explode("\n", rtrim($lines)); |
332 | foreach ($lines as $line) { |
333 | $item = $this->parseStatusLine($line); |
334 | $items[] = $item; |
335 | } |
336 | |
337 | // sort the items by shelving key in descending order, then ascending by |
338 | // copy number |
339 | $cmp = function ($a, $b) { |
340 | if ($a['shelving_key'] == $b['shelving_key']) { |
341 | return $a['number'] - $b['number']; |
342 | } |
343 | return $a['shelving_key'] < $b['shelving_key'] ? 1 : -1; |
344 | }; |
345 | usort($items, $cmp); |
346 | |
347 | return $items; |
348 | } |
349 | |
350 | /** |
351 | * Get Statuses |
352 | * |
353 | * This is responsible for retrieving the status information for a |
354 | * collection of records. |
355 | * |
356 | * @param array $idList The array of record ids to retrieve the status for |
357 | * |
358 | * @throws ILSException |
359 | * @return array An array of getStatus() return values on success. |
360 | */ |
361 | public function getStatuses($idList) |
362 | { |
363 | $statuses = []; |
364 | $params = [ |
365 | 'query' => 'multiple', 'ids' => implode('|', array_unique($idList)), |
366 | ]; |
367 | $response = $this->querySirsi($params); |
368 | if (empty($response)) { |
369 | return []; |
370 | } |
371 | $lines = explode("\n", $response); |
372 | |
373 | $currentId = null; |
374 | $group = -1; |
375 | foreach ($lines as $line) { |
376 | $item = $this->parseStatusLine($line); |
377 | if ($item['id'] != $currentId) { |
378 | $currentId = $item['id']; |
379 | $statuses[] = []; |
380 | $group++; |
381 | } |
382 | $statuses[$group][] = $item; |
383 | } |
384 | return $statuses; |
385 | } |
386 | |
387 | /** |
388 | * Get Purchase History |
389 | * |
390 | * This is responsible for retrieving the acquisitions history data for the |
391 | * specific record (usually recently received issues of a serial). |
392 | * |
393 | * @param string $id The record id to retrieve the info for |
394 | * |
395 | * @throws ILSException |
396 | * @return array An array with the acquisitions data on success. |
397 | * |
398 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
399 | */ |
400 | public function getPurchaseHistory($id) |
401 | { |
402 | // TODO |
403 | return []; |
404 | } |
405 | |
406 | /** |
407 | * Get Holding |
408 | * |
409 | * This is responsible for retrieving the holding information of a certain |
410 | * record. |
411 | * |
412 | * @param string $id The record id to retrieve the holdings for |
413 | * @param array $patron Patron data |
414 | * @param array $options Extra options (not currently used) |
415 | * |
416 | * @throws DateException |
417 | * @throws ILSException |
418 | * @return array On success, an associative array with the following |
419 | * keys: id, availability (boolean), status, location, reserve, callnumber, |
420 | * duedate, number, barcode. |
421 | * |
422 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
423 | */ |
424 | public function getHolding($id, array $patron = null, array $options = []) |
425 | { |
426 | return $this->getStatus($id); |
427 | } |
428 | |
429 | /** |
430 | * Place Hold |
431 | * |
432 | * Attempts to place a hold or recall on a particular item and returns |
433 | * an array with result details or throws an exception on failure of support |
434 | * classes |
435 | * |
436 | * @param array $holdDetails An array of item and patron data |
437 | * |
438 | * @throws ILSException |
439 | * @return mixed An array of data on the request including |
440 | * whether or not it was successful and a system message (if available) |
441 | */ |
442 | public function placeHold($holdDetails) |
443 | { |
444 | $patron = $holdDetails['patron']; |
445 | |
446 | // convert expire date from display format |
447 | // to the format Symphony/Unicorn expects |
448 | $expire = $holdDetails['requiredBy']; |
449 | $expire = $this->dateConverter->convertFromDisplayDate( |
450 | $this->config['Catalog']['server_date_format'], |
451 | $expire |
452 | ); |
453 | |
454 | // query sirsi |
455 | $params = [ |
456 | 'query' => 'hold', |
457 | 'itemId' => $holdDetails['item_id'], |
458 | 'patronId' => $patron['cat_username'], |
459 | 'pin' => $patron['cat_password'], |
460 | 'pickup' => $holdDetails['pickUpLocation'], |
461 | 'expire' => $expire, |
462 | 'comments' => $holdDetails['comment'], |
463 | 'holdType' => $holdDetails['level'], |
464 | 'callnumber' => $holdDetails['callnumber'], |
465 | 'override' => $holdDetails['override'], |
466 | ]; |
467 | $response = $this->querySirsi($params); |
468 | |
469 | // process the API response |
470 | if ($response == 'invalid_login') { |
471 | return [ |
472 | 'success' => false, |
473 | 'sysMessage' => 'authentication_error_admin']; |
474 | } |
475 | |
476 | $matches = []; |
477 | preg_match('/\^MN([0-9][0-9][0-9])/', $response, $matches); |
478 | if (isset($matches[1])) { |
479 | $status = $matches[1]; |
480 | if ($status == '209') { |
481 | return ['success' => true]; |
482 | } else { |
483 | return [ |
484 | 'success' => false, |
485 | 'sysMessage' => $this->config['ApiMessages'][$status]]; |
486 | } |
487 | } |
488 | |
489 | return ['success' => false]; |
490 | } |
491 | |
492 | /** |
493 | * Patron Login |
494 | * |
495 | * This is responsible for authenticating a patron against the catalog. |
496 | * |
497 | * @param string $username The patron username |
498 | * @param string $password The patron's password |
499 | * |
500 | * @throws ILSException |
501 | * @return mixed Associative array of patron info on successful login, |
502 | * null on unsuccessful login. |
503 | */ |
504 | public function patronLogin($username, $password) |
505 | { |
506 | //query sirsi |
507 | $params = [ |
508 | 'query' => 'login', 'patronId' => $username, 'pin' => $password, |
509 | ]; |
510 | $response = $this->querySirsi($params); |
511 | |
512 | if (empty($response)) { |
513 | return null; |
514 | } |
515 | |
516 | [$user_key, $alt_id, $barcode, $name, $library, $profile, $cat1, $cat2, |
517 | $cat3, $cat4, $cat5, $expiry, $holds, $status] = explode('|', $response); |
518 | |
519 | [$last, $first] = explode(',', $name); |
520 | $first = rtrim($first, ' '); |
521 | |
522 | if ($expiry != '0') { |
523 | $expiry = $this->parseDateTime(trim($expiry)); |
524 | } |
525 | $expired = ($expiry == '0') ? false : $expiry < time(); |
526 | return [ |
527 | 'id' => $username, |
528 | 'firstname' => $first, |
529 | 'lastname' => $last, |
530 | 'cat_username' => $username, |
531 | 'cat_password' => $password, |
532 | 'email' => null, |
533 | 'major' => null, |
534 | 'college' => null, |
535 | 'library' => $library, |
536 | 'barcode' => $barcode, |
537 | 'alt_id' => $alt_id, |
538 | 'cat1' => $cat1, |
539 | 'cat2' => $cat2, |
540 | 'cat3' => $cat3, |
541 | 'cat4' => $cat4, |
542 | 'cat5' => $cat5, |
543 | 'profile' => $profile, |
544 | 'expiry_date' => $this->formatDateTime($expiry), |
545 | 'expired' => $expired, |
546 | 'number_of_holds' => $holds, |
547 | 'status' => $status, |
548 | 'user_key' => $user_key, |
549 | ]; |
550 | } |
551 | |
552 | /** |
553 | * Get Patron Profile |
554 | * |
555 | * This is responsible for retrieving the profile for a specific patron. |
556 | * |
557 | * @param array $patron The patron array |
558 | * |
559 | * @throws ILSException |
560 | * @return array Array of the patron's profile data on success. |
561 | */ |
562 | public function getMyProfile($patron) |
563 | { |
564 | $username = $patron['cat_username']; |
565 | $password = $patron['cat_password']; |
566 | |
567 | //query sirsi |
568 | $params = [ |
569 | 'query' => 'profile', 'patronId' => $username, 'pin' => $password, |
570 | ]; |
571 | $response = $this->querySirsi($params); |
572 | |
573 | [, , , , $library, $profile, , , , , , , , $email, $address1, $zip, $phone, |
574 | $address2] = explode('|', $response); |
575 | |
576 | return [ |
577 | 'firstname' => $patron['firstname'], |
578 | 'lastname' => $patron['lastname'], |
579 | 'address1' => $address1, |
580 | 'address2' => $address2, |
581 | 'zip' => $zip, |
582 | 'phone' => $phone, |
583 | 'email' => $email, |
584 | 'group' => $profile, |
585 | 'library' => $library, |
586 | ]; |
587 | } |
588 | |
589 | /** |
590 | * Get Patron Fines |
591 | * |
592 | * This is responsible for retrieving all fines by a specific patron. |
593 | * |
594 | * @param array $patron The patron array from patronLogin |
595 | * |
596 | * @throws DateException |
597 | * @throws ILSException |
598 | * @return mixed Array of the patron's fines on success. |
599 | */ |
600 | public function getMyFines($patron) |
601 | { |
602 | $username = $patron['cat_username']; |
603 | $password = $patron['cat_password']; |
604 | |
605 | $params = [ |
606 | 'query' => 'fines', 'patronId' => $username, 'pin' => $password, |
607 | ]; |
608 | $response = $this->querySirsi($params); |
609 | if (empty($response)) { |
610 | return []; |
611 | } |
612 | $lines = explode("\n", $response); |
613 | $items = []; |
614 | foreach ($lines as $item) { |
615 | [$catkey, $amount, $balance, $date_billed, $number_of_payments, |
616 | $with_items, $reason, $date_charged, $duedate, $date_recalled] |
617 | = explode('|', $item); |
618 | |
619 | // the amount and balance are in cents, so we need to turn them into |
620 | // dollars if configured |
621 | if (!$this->config['Catalog']['leaveFinesAmountsInCents']) { |
622 | $amount = (floatval($amount) / 100.00); |
623 | $balance = (floatval($balance) / 100.00); |
624 | } |
625 | |
626 | $date_billed = $this->parseDateTime($date_billed); |
627 | $date_charged = $this->parseDateTime($date_charged); |
628 | $duedate = $this->parseDateTime($duedate); |
629 | $date_recalled = $this->parseDateTime($date_recalled); |
630 | $items[] = [ |
631 | 'id' => $catkey, |
632 | 'amount' => $amount, |
633 | 'balance' => $balance, |
634 | 'date_billed' => $this->formatDateTime($date_billed), |
635 | 'number_of_payments' => $number_of_payments, |
636 | 'with_items' => $with_items, |
637 | 'fine' => $reason, |
638 | 'checkout' => $this->formatDateTime($date_charged), |
639 | 'duedate' => $this->formatDateTime($duedate), |
640 | 'date_recalled' => $this->formatDateTime($date_recalled), |
641 | ]; |
642 | } |
643 | |
644 | return $items; |
645 | } |
646 | |
647 | /** |
648 | * Get Patron Holds |
649 | * |
650 | * This is responsible for retrieving all holds by a specific patron. |
651 | * |
652 | * @param array $patron The patron array from patronLogin |
653 | * |
654 | * @throws DateException |
655 | * @throws ILSException |
656 | * @return array Array of the patron's holds on success. |
657 | */ |
658 | public function getMyHolds($patron) |
659 | { |
660 | $username = $patron['cat_username']; |
661 | $password = $patron['cat_password']; |
662 | |
663 | $params = [ |
664 | 'query' => 'getholds', 'patronId' => $username, 'pin' => $password, |
665 | ]; |
666 | $response = $this->querySirsi($params); |
667 | if (empty($response)) { |
668 | return []; |
669 | } |
670 | $lines = explode("\n", $response); |
671 | $items = []; |
672 | foreach ($lines as $item) { |
673 | [$catkey, $holdkey, $available, , $date_expires, , $date_created, , |
674 | $type, $pickup_library, , , , , , , $barcode] = explode('|', $item); |
675 | |
676 | $date_created = $this->parseDateTime($date_created); |
677 | $date_expires = $this->parseDateTime($date_expires); |
678 | $items[] = [ |
679 | 'id' => $catkey, |
680 | 'reqnum' => $holdkey, |
681 | 'available' => ($available == 'Y') ? true : false, |
682 | 'expire' => $this->formatDateTime($date_expires), |
683 | 'create' => $this->formatDateTime($date_created), |
684 | 'type' => $type, |
685 | 'location' => $pickup_library, |
686 | 'item_id' => $holdkey, |
687 | 'barcode' => trim($barcode), |
688 | ]; |
689 | } |
690 | |
691 | return $items; |
692 | } |
693 | |
694 | /** |
695 | * Get Cancel Hold Details |
696 | * |
697 | * In order to cancel a hold, Voyager requires the patron details an item ID |
698 | * and a recall ID. This function returns the item id and recall id as a string |
699 | * separated by a pipe, which is then submitted as form data in Hold.php. This |
700 | * value is then extracted by the CancelHolds function. |
701 | * |
702 | * @param array $holdDetails A single hold array from getMyHolds |
703 | * @param array $patron Patron information from patronLogin |
704 | * |
705 | * @return string Data for use in a form field |
706 | * |
707 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
708 | */ |
709 | public function getCancelHoldDetails($holdDetails, $patron = []) |
710 | { |
711 | return $holdDetails['item_id']; |
712 | } |
713 | |
714 | /** |
715 | * Cancel Holds |
716 | * |
717 | * Attempts to Cancel a hold or recall on a particular item. The |
718 | * data in $cancelDetails['details'] is determined by getCancelHoldDetails(). |
719 | * |
720 | * @param array $cancelDetails An array of item and patron data |
721 | * |
722 | * @return array An array of data on each request including |
723 | * whether or not it was successful and a system message (if available) |
724 | */ |
725 | public function cancelHolds($cancelDetails) |
726 | { |
727 | $patron = $cancelDetails['patron']; |
728 | $details = $cancelDetails['details']; |
729 | $params = [ |
730 | 'query' => 'cancelHolds', |
731 | 'patronId' => $patron['cat_username'], 'pin' => $patron['cat_password'], |
732 | 'holdId' => implode('|', $details), |
733 | ]; |
734 | $response = $this->querySirsi($params); |
735 | |
736 | // process response |
737 | if (empty($response) || $response == 'invalid_login') { |
738 | return false; |
739 | } |
740 | |
741 | // break the response into separate lines |
742 | $lines = explode("\n", $response); |
743 | |
744 | // if there are more than 1 lines, then there is at least 1 failure |
745 | $failures = []; |
746 | if (count($lines) > 1) { |
747 | // extract the failed IDs. |
748 | foreach ($lines as $line) { |
749 | // error lines start with '**' |
750 | if (str_starts_with(trim($line), '**')) { |
751 | [, $holdKey] = explode(':', $line); |
752 | $failures[] = trim($holdKey, '()'); |
753 | } |
754 | } |
755 | } |
756 | |
757 | $count = 0; |
758 | $items = []; |
759 | foreach ($details as $holdKey) { |
760 | if (in_array($holdKey, $failures)) { |
761 | $items[$holdKey] = [ |
762 | 'success' => false, 'status' => 'hold_cancel_fail', |
763 | ]; |
764 | } else { |
765 | $count++; |
766 | $items[$holdKey] = [ |
767 | 'success' => true, 'status' => 'hold_cancel_success', |
768 | ]; |
769 | } |
770 | } |
771 | $result = ['count' => $count, 'items' => $items]; |
772 | return $result; |
773 | } |
774 | |
775 | /** |
776 | * Get Patron Transactions |
777 | * |
778 | * This is responsible for retrieving all transactions (i.e. checked out items) |
779 | * by a specific patron. |
780 | * |
781 | * @param array $patron The patron array from patronLogin |
782 | * |
783 | * @throws DateException |
784 | * @throws ILSException |
785 | * @return array Array of the patron's transactions on success. |
786 | */ |
787 | public function getMyTransactions($patron) |
788 | { |
789 | $username = $patron['cat_username']; |
790 | $password = $patron['cat_password']; |
791 | |
792 | $params = [ |
793 | 'query' => 'transactions', 'patronId' => $username, 'pin' => $password, |
794 | ]; |
795 | $response = $this->querySirsi($params); |
796 | if (empty($response)) { |
797 | return []; |
798 | } |
799 | $item_lines = explode("\n", $response); |
800 | $items = []; |
801 | foreach ($item_lines as $item) { |
802 | [$catkey, $date_charged, $duedate, $date_renewed, $accrued_fine, |
803 | $overdue, $number_of_renewals, $date_recalled, $charge_key1, |
804 | $charge_key2, $charge_key3, $charge_key4, $recall_period, $callnum] |
805 | = explode('|', $item); |
806 | |
807 | $duedate = $original_duedate = $this->parseDateTime($duedate); |
808 | $recall_duedate = false; |
809 | $date_recalled = $this->parseDateTime($date_recalled); |
810 | if ($date_recalled) { |
811 | $duedate = $recall_duedate = $this->calculateRecallDueDate( |
812 | $date_recalled, |
813 | $recall_period, |
814 | $original_duedate |
815 | ); |
816 | } |
817 | $charge_key = "$charge_key1|$charge_key2|$charge_key3|$charge_key4"; |
818 | $items[] = [ |
819 | 'id' => $catkey, |
820 | 'date_charged' => |
821 | $this->formatDateTime($this->parseDateTime($date_charged)), |
822 | 'duedate' => $this->formatDateTime($duedate), |
823 | 'duedate_raw' => $duedate, // unformatted duedate used for sorting |
824 | 'date_renewed' => |
825 | $this->formatDateTime($this->parseDateTime($date_renewed)), |
826 | 'accrued_fine' => $accrued_fine, |
827 | 'overdue' => $overdue, |
828 | 'number_of_renewals' => $number_of_renewals, |
829 | 'date_recalled' => $this->formatDateTime($date_recalled), |
830 | 'recall_duedate' => $this->formatDateTime($recall_duedate), |
831 | 'original_duedate' => $this->formatDateTime($original_duedate), |
832 | 'renewable' => true, |
833 | 'charge_key' => $charge_key, |
834 | 'item_id' => $charge_key, |
835 | 'callnum' => $callnum, |
836 | 'dueStatus' => $overdue == 'Y' ? 'overdue' : '', |
837 | ]; |
838 | } |
839 | |
840 | // sort the items by due date |
841 | $cmp = function ($a, $b) { |
842 | if ($a['duedate_raw'] == $b['duedate_raw']) { |
843 | return $a['id'] < $b['id'] ? -1 : 1; |
844 | } |
845 | return $a['duedate_raw'] < $b['duedate_raw'] ? -1 : 1; |
846 | }; |
847 | usort($items, $cmp); |
848 | |
849 | return $items; |
850 | } |
851 | |
852 | /** |
853 | * Get Courses |
854 | * |
855 | * Obtain a list of courses for use in limiting the reserves list. |
856 | * |
857 | * @throws ILSException |
858 | * @return array An associative array with key = ID, value = name. |
859 | */ |
860 | public function getCourses() |
861 | { |
862 | //query sirsi |
863 | $params = ['query' => 'courses']; |
864 | $response = $this->querySirsi($params); |
865 | |
866 | $response = rtrim($response); |
867 | $course_lines = explode("\n", $response); |
868 | $courses = []; |
869 | |
870 | foreach ($course_lines as $course) { |
871 | [$id, $code, $name] = explode('|', $course); |
872 | $name = ($code == $name) ? $name : $code . ' - ' . $name; |
873 | $courses[$id] = $name; |
874 | } |
875 | $this->getSorter()->asort($courses); |
876 | return $courses; |
877 | } |
878 | |
879 | /** |
880 | * Get Instructors |
881 | * |
882 | * Obtain a list of instructors for use in limiting the reserves list. |
883 | * |
884 | * @throws ILSException |
885 | * @return array An associative array with key = ID, value = name. |
886 | */ |
887 | public function getInstructors() |
888 | { |
889 | //query sirsi |
890 | $params = ['query' => 'instructors']; |
891 | $response = $this->querySirsi($params); |
892 | |
893 | $response = rtrim($response); |
894 | $user_lines = explode("\n", $response); |
895 | $users = []; |
896 | |
897 | foreach ($user_lines as $user) { |
898 | [$id, $name] = explode('|', $user); |
899 | $users[$id] = $name; |
900 | } |
901 | $this->getSorter()->asort($users); |
902 | return $users; |
903 | } |
904 | |
905 | /** |
906 | * Get Departments |
907 | * |
908 | * Obtain a list of departments for use in limiting the reserves list. |
909 | * |
910 | * @throws ILSException |
911 | * @return array An associative array with key = dept. ID, value = dept. name. |
912 | */ |
913 | public function getDepartments() |
914 | { |
915 | //query sirsi |
916 | $params = ['query' => 'desks']; |
917 | $response = $this->querySirsi($params); |
918 | |
919 | $response = rtrim($response); |
920 | $dept_lines = explode("\n", $response); |
921 | $depts = []; |
922 | |
923 | foreach ($dept_lines as $dept) { |
924 | [$id, $name] = explode('|', $dept); |
925 | $depts[$id] = $name; |
926 | } |
927 | $this->getSorter()->asort($depts); |
928 | return $depts; |
929 | } |
930 | |
931 | /** |
932 | * Find Reserves |
933 | * |
934 | * Obtain information on course reserves. |
935 | * |
936 | * @param string $courseId ID from getCourses (empty string to match all) |
937 | * @param string $instructorId ID from getInstructors (empty string to match all) |
938 | * @param string $departmentId ID from getDepartments (empty string to match all) |
939 | * |
940 | * @throws ILSException |
941 | * @return array An array of associative arrays representing |
942 | * reserve items. |
943 | */ |
944 | public function findReserves($courseId, $instructorId, $departmentId) |
945 | { |
946 | //query sirsi |
947 | if ($courseId) { |
948 | $params = [ |
949 | 'query' => 'reserves', 'course' => $courseId, 'instructor' => '', |
950 | 'desk' => '', |
951 | ]; |
952 | } elseif ($instructorId) { |
953 | $params = [ |
954 | 'query' => 'reserves', 'course' => '', 'instructor' => $instructorId, |
955 | 'desk' => '', |
956 | ]; |
957 | } elseif ($departmentId) { |
958 | $params = [ |
959 | 'query' => 'reserves', 'course' => '', 'instructor' => '', |
960 | 'desk' => $departmentId, |
961 | ]; |
962 | } else { |
963 | $params = [ |
964 | 'query' => 'reserves', 'course' => '', 'instructor' => '', |
965 | 'desk' => '', |
966 | ]; |
967 | } |
968 | |
969 | $response = $this->querySirsi($params); |
970 | |
971 | $item_lines = explode("\n", $response); |
972 | $items = []; |
973 | foreach ($item_lines as $item) { |
974 | [$instructor_id, $course_id, $dept_id, $bib_id] |
975 | = explode('|', $item); |
976 | if ( |
977 | $bib_id && (empty($instructorId) || $instructorId == $instructor_id) |
978 | && (empty($courseId) || $courseId == $course_id) |
979 | && (empty($departmentId) || $departmentId == $dept_id) |
980 | ) { |
981 | $items[] = [ |
982 | 'BIB_ID' => $bib_id, |
983 | 'INSTRUCTOR_ID' => $instructor_id, |
984 | 'COURSE_ID' => $course_id, |
985 | 'DEPARTMENT_ID' => $dept_id, |
986 | ]; |
987 | } |
988 | } |
989 | return $items; |
990 | } |
991 | |
992 | /** |
993 | * Get New Items |
994 | * |
995 | * Retrieve the IDs of items recently added to the catalog. |
996 | * |
997 | * @param int $page Page number of results to retrieve (counting starts at 1) |
998 | * @param int $limit The size of each page of results to retrieve |
999 | * @param int $daysOld The maximum age of records to retrieve in days (max. 30) |
1000 | * @param int $fundId optional fund ID to use for limiting results (use a value |
1001 | * returned by getFunds, or exclude for no limit); note that "fund" may be a |
1002 | * misnomer - if funds are not an appropriate way to limit your new item |
1003 | * results, you can return a different set of values from getFunds. The |
1004 | * important thing is that this parameter supports an ID returned by getFunds, |
1005 | * whatever that may mean. |
1006 | * |
1007 | * @throws ILSException |
1008 | * @return array Associative array with 'count' and 'results' keys |
1009 | * |
1010 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1011 | */ |
1012 | public function getNewItems($page, $limit, $daysOld, $fundId = null) |
1013 | { |
1014 | //query sirsi |
1015 | // isset($lib) |
1016 | // ? $params = array('query' => 'newItems', |
1017 | // 'lib' => array_search($lib, $config['Libraries'])) |
1018 | // : $params = array('query' => 'newItems'); |
1019 | $params = ['query' => 'newitems', 'lib' => 'PPL']; |
1020 | $response = $this->querySirsi($params); |
1021 | |
1022 | $item_lines = explode("\n", rtrim($response)); |
1023 | |
1024 | $rescount = 0; |
1025 | foreach ($item_lines as $item) { |
1026 | $item = rtrim($item, '|'); |
1027 | $items[$item] = [ |
1028 | 'id' => $item, |
1029 | ]; |
1030 | $rescount++; |
1031 | } |
1032 | |
1033 | $results = array_slice($items, ($page - 1) * $limit, ($page * $limit) - 1); |
1034 | return ['count' => $rescount, 'results' => $results]; |
1035 | } |
1036 | |
1037 | /** |
1038 | * Get suppressed records. |
1039 | * |
1040 | * @throws ILSException |
1041 | * @return array ID numbers of suppressed records in the system. |
1042 | */ |
1043 | public function getSuppressedRecords() |
1044 | { |
1045 | $params = ['query' => 'shadowed']; |
1046 | $response = $this->querySirsi($params); |
1047 | |
1048 | $record_lines = explode("\n", rtrim($response)); |
1049 | $records = []; |
1050 | foreach ($record_lines as $record) { |
1051 | $record = rtrim($record, '|'); |
1052 | $records[] = $record; |
1053 | } |
1054 | |
1055 | return $records; |
1056 | } |
1057 | |
1058 | /** |
1059 | * Parse a pipe-delimited status line received from the script on the |
1060 | * Unicorn/Symphony server. |
1061 | * |
1062 | * @param string $line The pipe-delimited status line to parse. |
1063 | * |
1064 | * @return array Associative array of holding information |
1065 | */ |
1066 | protected function parseStatusLine($line) |
1067 | { |
1068 | [$catkey, $shelving_key, $callnum, $itemkey1, $itemkey2, $itemkey3, |
1069 | $barcode, $reserve, $number_of_charges, $item_type, $recirculate_flag, |
1070 | $holdcount, $library_code, $library, $location_code, $location, |
1071 | $currLocCode, $current_location, $holdable, $circulation_rule, $duedate, |
1072 | $date_recalled, $recall_period, $format, $title_holds] |
1073 | = explode('|', $line); |
1074 | |
1075 | // availability |
1076 | $availability = ($number_of_charges == 0) ? 1 : 0; |
1077 | |
1078 | // due date (if checked out) |
1079 | $duedate = $this->parseDateTime(trim($duedate)); |
1080 | |
1081 | // date recalled |
1082 | $date_recalled = $this->parseDateTime(trim($date_recalled)); |
1083 | |
1084 | // a recalled item has a new due date, we have to calculate that new due date |
1085 | if ($date_recalled !== false) { |
1086 | $duedate = $this->calculateRecallDueDate( |
1087 | $date_recalled, |
1088 | $recall_period, |
1089 | $duedate |
1090 | ); |
1091 | } |
1092 | |
1093 | // item status |
1094 | $status = ($availability) ? 'Available' : 'Checked Out'; |
1095 | |
1096 | // even though item is NOT checked out, it still may not be "Available" |
1097 | // the following are the special cases |
1098 | if ( |
1099 | isset($this->config['UnavailableItemTypes']) |
1100 | && isset($this->config['UnavailableItemTypes'][$item_type]) |
1101 | ) { |
1102 | $availability = 0; |
1103 | $status = $this->config['UnavailableItemTypes'][$item_type]; |
1104 | } elseif ( |
1105 | isset($this->config['UnavailableLocations']) |
1106 | && isset($this->config['UnavailableLocations'][$currLocCode]) |
1107 | ) { |
1108 | $availability = 0; |
1109 | $status = $this->config['UnavailableLocations'][$currLocCode]; |
1110 | } |
1111 | |
1112 | $item = [ |
1113 | 'status' => $status, |
1114 | 'availability' => $availability, |
1115 | 'id' => $catkey, |
1116 | 'number' => $itemkey3, // copy number |
1117 | 'duedate' => $this->formatDateTime($duedate), |
1118 | 'callnumber' => $callnum, |
1119 | 'reserve' => ($reserve == '0') ? 'N' : 'Y', |
1120 | 'location_code' => $location_code, |
1121 | 'location' => $location, |
1122 | 'home_location_code' => $location_code, |
1123 | 'home_location' => $location, |
1124 | 'library_code' => $library_code, |
1125 | 'library' => ($library) ? $library : $library_code, |
1126 | 'barcode' => trim($barcode), |
1127 | 'item_id' => trim($barcode), |
1128 | 'is_holdable' => $holdable, |
1129 | 'requests_placed' => $holdcount + $title_holds, |
1130 | 'current_location_code' => $currLocCode, |
1131 | 'current_location' => $current_location, |
1132 | 'item_type' => $item_type, |
1133 | 'recirculate_flag' => $recirculate_flag, |
1134 | 'shelving_key' => $shelving_key, |
1135 | 'circulation_rule' => $circulation_rule, |
1136 | 'date_recalled' => $this->formatDateTime($date_recalled), |
1137 | 'item_key' => $itemkey1 . '|' . $itemkey2 . '|' . $itemkey3 . '|', |
1138 | 'format' => $format, |
1139 | ]; |
1140 | |
1141 | return $item; |
1142 | } |
1143 | |
1144 | /** |
1145 | * Map the location code to friendly name. |
1146 | * |
1147 | * @param string $code The location code from Unicorn/Symphony |
1148 | * |
1149 | * @return string The friendly name if defined, otherwise the code is |
1150 | * returned. |
1151 | */ |
1152 | protected function mapLocation($code) |
1153 | { |
1154 | if ( |
1155 | isset($this->config['Locations']) |
1156 | && isset($this->config['Locations'][$code]) |
1157 | ) { |
1158 | return $this->config['Locations'][$code]; |
1159 | } |
1160 | return $code; |
1161 | } |
1162 | |
1163 | /** |
1164 | * Maps the library code to friendly library name. |
1165 | * |
1166 | * @param string $code The library code from Unicorn/Symphony |
1167 | * |
1168 | * @return string The library friendly name if defined, otherwise the code |
1169 | * is returned. |
1170 | */ |
1171 | protected function mapLibrary($code) |
1172 | { |
1173 | if ( |
1174 | isset($this->config['Libraries']) |
1175 | && isset($this->config['Libraries'][$code]) |
1176 | ) { |
1177 | return $this->config['Libraries'][$code]; |
1178 | } |
1179 | return $code; |
1180 | } |
1181 | |
1182 | /** |
1183 | * Send a request to the SIRSI side API script and returns the response. |
1184 | * |
1185 | * @param array $params Associative array of query parameters to send. |
1186 | * |
1187 | * @return string |
1188 | */ |
1189 | protected function querySirsi($params) |
1190 | { |
1191 | // make sure null parameters are sent as empty strings instead or else the |
1192 | // driver.pl may choke on null parameter values |
1193 | foreach ($params as $key => $value) { |
1194 | if ($value == null) { |
1195 | $params[$key] = ''; |
1196 | } |
1197 | } |
1198 | |
1199 | $url = $this->url; |
1200 | if (empty($url)) { |
1201 | $url = $this->host; |
1202 | if ($this->port) { |
1203 | $url = 'http://' . $url . ':' . $this->port . '/' . |
1204 | $this->search_prog; |
1205 | } else { |
1206 | $url = 'http://' . $url . '/' . $this->search_prog; |
1207 | } |
1208 | } |
1209 | |
1210 | $httpClient = $this->httpService->createClient($url, 'POST'); |
1211 | $httpClient->setRawBody(http_build_query($params)); |
1212 | $httpClient->setEncType('application/x-www-form-urlencoded'); |
1213 | // use HTTP POST so parameters like user id and PIN are NOT logged by web |
1214 | // servers |
1215 | $result = $httpClient->send(); |
1216 | |
1217 | // Even if we get a response, make sure it's a 'good' one. |
1218 | if (!$result->isSuccess()) { |
1219 | throw new ILSException("Error response code received from $url"); |
1220 | } |
1221 | |
1222 | // get the response data |
1223 | $response = $result->getBody(); |
1224 | |
1225 | return rtrim($response); |
1226 | } |
1227 | |
1228 | /** |
1229 | * Given the date recalled, calculate the new due date based on circulation |
1230 | * policy. |
1231 | * |
1232 | * @param int $dateRecalled Unix time stamp of when the recall was issued. |
1233 | * @param int $recallPeriod Number of days to due date (from date recalled). |
1234 | * @param int $duedate Original duedate. |
1235 | * |
1236 | * @return int New due date as unix time stamp. |
1237 | */ |
1238 | protected function calculateRecallDueDate($dateRecalled, $recallPeriod, $duedate) |
1239 | { |
1240 | // FIXME: There must be a better way of getting recall due date |
1241 | if ($dateRecalled) { |
1242 | $recallDue = $dateRecalled |
1243 | + (($recallPeriod + 1) * 24 * 60 * 60) - 60; |
1244 | return ($recallDue < $duedate) ? $recallDue : $duedate; |
1245 | } |
1246 | return false; |
1247 | } |
1248 | |
1249 | /** |
1250 | * Take a date/time string from SIRSI seltool and convert it into unix time |
1251 | * stamp. |
1252 | * |
1253 | * @param string $date The input date string. Expected format YYYYMMDDHHMM. |
1254 | * |
1255 | * @return int Unix time stamp if successful, false otherwise. |
1256 | */ |
1257 | protected function parseDateTime($date) |
1258 | { |
1259 | if (strlen($date) >= 8) { |
1260 | // format is MM/DD/YYYY HH:MI so it can be passed to strtotime |
1261 | $formatted_date = substr($date, 4, 2) . '/' . substr($date, 6, 2) . |
1262 | '/' . substr($date, 0, 4); |
1263 | if (strlen($date) > 8) { |
1264 | $formatted_date .= ' ' . substr($date, 8, 2) . ':' . |
1265 | substr($date, 10); |
1266 | } |
1267 | return strtotime($formatted_date); |
1268 | } |
1269 | return false; |
1270 | } |
1271 | |
1272 | /** |
1273 | * Format the given unix time stamp to a human readable format. The format is |
1274 | * configurable in Unicorn.ini |
1275 | * |
1276 | * @param int $time Unix time stamp. |
1277 | * |
1278 | * @return string Formatted date/time. |
1279 | */ |
1280 | protected function formatDateTime($time) |
1281 | { |
1282 | $dateTimeString = ''; |
1283 | if ($time) { |
1284 | $dateTimeString = $this->dateConverter->convertToDisplayDate('U', $time); |
1285 | } |
1286 | return $dateTimeString; |
1287 | } |
1288 | |
1289 | /** |
1290 | * Convert the given ISO-8859-1 string to UTF-8 if it is not already UTF-8. |
1291 | * |
1292 | * @param string $s The string to convert. |
1293 | * |
1294 | * @return string The input string converted to UTF-8 |
1295 | */ |
1296 | protected function toUTF8($s) |
1297 | { |
1298 | return (mb_detect_encoding($s, 'UTF-8') == 'UTF-8') ? $s : utf8_encode($s); |
1299 | } |
1300 | |
1301 | /** |
1302 | * Given a location field, return the values relevant to VuFind. |
1303 | * |
1304 | * This method is meant to be overridden in inheriting classes to |
1305 | * reflect local policies regarding interpretation of the a, b and |
1306 | * c subfields of 852. |
1307 | * |
1308 | * @param MarcReader $record MARC record. |
1309 | * @param array $field Location field to be processed. |
1310 | * |
1311 | * @return array Location information. |
1312 | */ |
1313 | protected function processMarcHoldingLocation(MarcReader $record, $field) |
1314 | { |
1315 | $library_code = $record->getSubfield($field, 'b'); |
1316 | $location_code = $record->getSubfield($field, 'c'); |
1317 | $location = [ |
1318 | 'library_code' => $library_code, |
1319 | 'library' => $this->mapLibrary($library_code), |
1320 | 'location_code' => $location_code, |
1321 | 'location' => $this->mapLocation($location_code), |
1322 | 'notes' => $record->getSubfields($field, 'z'), |
1323 | 'marc852' => $field, |
1324 | ]; |
1325 | return $location; |
1326 | } |
1327 | |
1328 | /** |
1329 | * Decode a MARC holding record. |
1330 | * |
1331 | * @param MarcReader $record Holding record to decode.. |
1332 | * |
1333 | * @return array Has two elements: the first is the list of |
1334 | * locations found in the record, the second are the |
1335 | * decoded holdings per se. |
1336 | * |
1337 | * @todo Check if is OK to print multiple times textual holdings |
1338 | * that had more than one $8. |
1339 | */ |
1340 | protected function decodeMarcHoldingRecord(MarcReader $record) |
1341 | { |
1342 | $locations = []; |
1343 | $holdings = []; |
1344 | // First pass: |
1345 | // - process locations |
1346 | // |
1347 | // - collect textual holdings indexed by linking number to be |
1348 | // able to easily check later what fields from enumeration |
1349 | // and chronology they override. |
1350 | $textuals = []; |
1351 | $fields = array_merge($record->getFields('852'), $record->getFields('866')); |
1352 | foreach ($fields as $field) { |
1353 | switch ($field['tag']) { |
1354 | case '852': |
1355 | $locations[] |
1356 | = $this->processMarcHoldingLocation($record, $field); |
1357 | break; |
1358 | case '866': |
1359 | $linking_fields = $record->getSubfields($field, '8'); |
1360 | if ($linking_fields === false) { |
1361 | // Skip textual holdings fields with no linking |
1362 | continue 2; |
1363 | } |
1364 | foreach ($linking_fields as $linking_field) { |
1365 | $linking = explode('.', $linking_field); |
1366 | // Only the linking part is used in textual |
1367 | // holdings... |
1368 | $linking = $linking[0]; |
1369 | // and it should be an int. |
1370 | $textuals[(int)($linking)] = &$field; |
1371 | } |
1372 | break; |
1373 | } |
1374 | } |
1375 | |
1376 | // Second pass: enumeration and chronology, biblio |
1377 | |
1378 | // Digits to use to build a combined index with linking number |
1379 | // and sequence number. |
1380 | // PS: Does this make this implementation year-3K safe? |
1381 | $link_digits = floor(strlen((string)PHP_INT_MAX) / 2); |
1382 | |
1383 | $data863 = array_key_exists(0, $textuals) ? [] : $record->getFields('863'); |
1384 | foreach ($data863 as $field) { |
1385 | $linking_field = $record->getSubfield($field, '8'); |
1386 | |
1387 | if ($linking_field === false) { |
1388 | // Skip record if there is no linking number |
1389 | continue; |
1390 | } |
1391 | |
1392 | $linking = explode('.', $linking_field); |
1393 | if (1 < count($linking)) { |
1394 | $sequence = explode('\\', $linking[1]); |
1395 | // Lets ignore the link type, as we only care for \x |
1396 | $sequence = $sequence[0]; |
1397 | } else { |
1398 | $sequence = 0; |
1399 | } |
1400 | $linking = $linking[0]; |
1401 | |
1402 | if (array_key_exists((int)$linking, $textuals)) { |
1403 | // Skip coded holdings overridden by textual |
1404 | // holdings |
1405 | continue; |
1406 | } |
1407 | |
1408 | $decoded_holding = ''; |
1409 | foreach ($field['subfields'] as $subfield) { |
1410 | if (str_contains('68x', $subfield['code'])) { |
1411 | continue; |
1412 | } |
1413 | $decoded_holding .= ' ' . $subfield['data']; |
1414 | } |
1415 | |
1416 | $ndx = (int)($linking |
1417 | . sprintf("%0{$link_digits}u", $sequence)); |
1418 | $holdings[$ndx] = trim($decoded_holding); |
1419 | } |
1420 | |
1421 | foreach ($textuals as $linking => $field) { |
1422 | $textual_holding = $record->getSubfield($field, 'a'); |
1423 | foreach ($record->getSubfields($field, 'z') as $note) { |
1424 | $textual_holding .= ' ' . $note; |
1425 | } |
1426 | |
1427 | $ndx = (int)($linking . sprintf("%0{$link_digits}u", 0)); |
1428 | $holdings[$ndx] = trim($textual_holding); |
1429 | } |
1430 | |
1431 | return [$locations, $holdings]; |
1432 | } |
1433 | |
1434 | /** |
1435 | * Get textual holdings summary. |
1436 | * |
1437 | * @param string $marc Raw marc holdings records. |
1438 | * |
1439 | * @return array Array of holdings data similar to the one returned by |
1440 | * getHolding. |
1441 | */ |
1442 | protected function getMarcHoldings($marc) |
1443 | { |
1444 | $holdings = []; |
1445 | $collection = new MarcCollection($marc); |
1446 | foreach ($collection as $record) { |
1447 | [$locations, $record_holdings] |
1448 | = $this->decodeMarcHoldingRecord($record); |
1449 | // Flatten locations with corresponding holdings as VuFind |
1450 | // expects it. |
1451 | foreach ($locations as $location) { |
1452 | $holdings[] = array_merge_recursive( |
1453 | $location, |
1454 | ['summary' => $record_holdings] |
1455 | ); |
1456 | } |
1457 | } |
1458 | return $holdings; |
1459 | } |
1460 | } |