Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
9.83% |
73 / 743 |
|
2.94% |
1 / 34 |
CRAP | |
0.00% |
0 / 1 |
Symphony | |
9.83% |
73 / 743 |
|
2.94% |
1 / 34 |
23410.61 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
init | |
92.86% |
39 / 42 |
|
0.00% |
0 / 1 |
3.00 | |||
getSoapClient | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
4.01 | |||
getSoapHeader | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
getSessionToken | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
42 | |||
makeRequest | |
16.67% |
6 / 36 |
|
0.00% |
0 / 1 |
55.88 | |||
checkSymwsVersion | |
20.00% |
1 / 5 |
|
0.00% |
0 / 1 |
12.19 | |||
getStatuses999Holdings | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
72 | |||
lookupTitleInfo | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
42 | |||
libraryIsFilteredOut | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
12 | |||
parseCallInfo | |
0.00% |
0 / 94 |
|
0.00% |
0 / 1 |
552 | |||
parseBoundwithLinkInfo | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
110 | |||
parseTitleOrderInfo | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
132 | |||
parseMarcHoldingsInfo | |
0.00% |
0 / 22 |
|
0.00% |
0 / 1 |
90 | |||
getLiveStatuses | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
72 | |||
translatePolicyID | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getStatus | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getStatuses | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getHolding | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPurchaseHistory | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
patronLogin | |
0.00% |
0 / 44 |
|
0.00% |
0 / 1 |
132 | |||
getMyProfile | |
0.00% |
0 / 55 |
|
0.00% |
0 / 1 |
42 | |||
getMyTransactions | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
72 | |||
getMyHolds | |
0.00% |
0 / 32 |
|
0.00% |
0 / 1 |
42 | |||
getMyFines | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
30 | |||
getCancelHoldDetails | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
cancelHolds | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
12 | |||
getConfig | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getRenewDetails | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
renewMyItems | |
0.00% |
0 / 30 |
|
0.00% |
0 / 1 |
12 | |||
placeHold | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
56 | |||
getPolicyList | |
54.55% |
12 / 22 |
|
0.00% |
0 / 1 |
11.60 | |||
getPickUpLocations | |
42.86% |
3 / 7 |
|
0.00% |
0 / 1 |
2.75 | |||
getDefaultPickUpLocation | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | /** |
4 | * Symphony Web Services (symws) ILS Driver |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2007. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package ILS_Drivers |
25 | * @author Steven Hild <sjhild@wm.edu> |
26 | * @author Michael Gillen <mlgillen@sfasu.edu> |
27 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
28 | * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki |
29 | */ |
30 | |
31 | namespace VuFind\ILS\Driver; |
32 | |
33 | use Laminas\Log\LoggerAwareInterface; |
34 | use SoapClient; |
35 | use SoapFault; |
36 | use SoapHeader; |
37 | use VuFind\Cache\Manager as CacheManager; |
38 | use VuFind\Exception\ILS as ILSException; |
39 | use VuFind\Record\Loader; |
40 | |
41 | use function count; |
42 | use function in_array; |
43 | use function is_array; |
44 | |
45 | /** |
46 | * Symphony Web Services (symws) ILS Driver |
47 | * |
48 | * @category VuFind |
49 | * @package ILS_Drivers |
50 | * @author Steven Hild <sjhild@wm.edu> |
51 | * @author Michael Gillen <mlgillen@sfasu.edu> |
52 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
53 | * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki |
54 | */ |
55 | class Symphony extends AbstractBase implements LoggerAwareInterface |
56 | { |
57 | use \VuFind\Log\LoggerAwareTrait; |
58 | |
59 | /** |
60 | * Cache for policy information |
61 | * |
62 | * @var object |
63 | */ |
64 | protected $policyCache = false; |
65 | |
66 | /** |
67 | * Policy information |
68 | * |
69 | * @var array |
70 | */ |
71 | protected $policies; |
72 | |
73 | /** |
74 | * Cache manager |
75 | * |
76 | * @var CacheManager |
77 | */ |
78 | protected $cacheManager; |
79 | |
80 | /** |
81 | * Record loader |
82 | * |
83 | * @var Loader |
84 | */ |
85 | protected $recordLoader; |
86 | |
87 | /** |
88 | * Constructor |
89 | * |
90 | * @param Loader $loader Record loader |
91 | * @param CacheManager $cacheManager Cache manager (optional) |
92 | */ |
93 | public function __construct(Loader $loader, CacheManager $cacheManager = null) |
94 | { |
95 | $this->recordLoader = $loader; |
96 | $this->cacheManager = $cacheManager; |
97 | } |
98 | |
99 | /** |
100 | * Initialize the driver. |
101 | * |
102 | * Validate configuration and perform all resource-intensive tasks needed to |
103 | * make the driver active. |
104 | * |
105 | * @throws ILSException |
106 | * @return void |
107 | */ |
108 | public function init() |
109 | { |
110 | // Merge in defaults. |
111 | $this->config += [ |
112 | 'WebServices' => [], |
113 | 'PolicyCache' => [], |
114 | 'LibraryFilter' => [], |
115 | 'MarcHoldings' => [], |
116 | '999Holdings' => [], |
117 | 'Behaviors' => [], |
118 | ]; |
119 | |
120 | $this->config['WebServices'] += [ |
121 | 'clientID' => 'VuFind', |
122 | 'baseURL' => 'http://localhost:8080/symws', |
123 | 'soapOptions' => [], |
124 | ]; |
125 | |
126 | $this->config['PolicyCache'] += [ |
127 | 'backend' => 'file', |
128 | 'backendOptions' => [], |
129 | 'frontendOptions' => [], |
130 | ]; |
131 | |
132 | $this->config['PolicyCache']['frontendOptions'] += [ |
133 | 'automatic_serialization' => true, |
134 | 'lifetime' => null, |
135 | ]; |
136 | |
137 | $this->config['LibraryFilter'] += [ |
138 | 'include_only' => [], |
139 | 'exclude' => [], |
140 | ]; |
141 | |
142 | $this->config['999Holdings'] += [ |
143 | 'entry_number' => 999, |
144 | 'mode' => 'off', // also off, failover |
145 | ]; |
146 | |
147 | $this->config['Behaviors'] += [ |
148 | 'showBaseCallNumber' => true, |
149 | 'showAccountLogin' => true, |
150 | 'showStaffNotes' => true, |
151 | 'showFeeType' => 'ALL_FEES', |
152 | 'usernameField' => 'userID', |
153 | 'userProfileGroupField' => 'USER_PROFILE_ID', |
154 | ]; |
155 | |
156 | // Initialize cache manager. |
157 | if ( |
158 | isset($this->config['PolicyCache']['type']) |
159 | && $this->cacheManager |
160 | ) { |
161 | $this->policyCache = $this->cacheManager |
162 | ->getCache($this->config['PolicyCache']['type']); |
163 | } |
164 | } |
165 | |
166 | /** |
167 | * Return a SoapClient for the specified SymWS service. |
168 | * |
169 | * SoapClient instantiation fetches and parses remote files, |
170 | * so this method instantiates SoapClients lazily and keeps them around |
171 | * so that they can be reused for multiple requests. |
172 | * |
173 | * @param string $service The name of the SymWS service |
174 | * |
175 | * @return object The SoapClient object for the specified service |
176 | */ |
177 | protected function getSoapClient($service) |
178 | { |
179 | static $soapClients = []; |
180 | |
181 | if (!isset($soapClients[$service])) { |
182 | try { |
183 | $soapClients[$service] = new SoapClient( |
184 | $this->config['WebServices']['baseURL'] . "/soap/$service?wsdl", |
185 | $this->config['WebServices']['soapOptions'] |
186 | ); |
187 | } catch (SoapFault $e) { |
188 | // This SoapFault may have happened because, e.g., PHP's |
189 | // SoapClient won't load SymWS 3.1's Patron service WSDL. |
190 | // However, we can't check the SymWS version if this fault |
191 | // happened with the Standard service (which contains the |
192 | // 'version' operation). |
193 | if ($service != 'standard') { |
194 | $this->checkSymwsVersion(); |
195 | } |
196 | |
197 | throw $e; |
198 | } |
199 | } |
200 | |
201 | return $soapClients[$service]; |
202 | } |
203 | |
204 | /** |
205 | * Return a SoapHeader for the specified login and password. |
206 | * |
207 | * @param mixed $login The login account name if logging in, otherwise null |
208 | * @param mixed $password The login password if logging in, otherwise null |
209 | * @param bool $reset Whether or not the session token should be reset |
210 | * |
211 | * @return object The SoapHeader object |
212 | */ |
213 | protected function getSoapHeader( |
214 | $login = null, |
215 | $password = null, |
216 | $reset = false |
217 | ) { |
218 | $data = ['clientID' => $this->config['WebServices']['clientID']]; |
219 | if (null !== $login) { |
220 | $data['sessionToken'] |
221 | = $this->getSessionToken($login, $password, $reset); |
222 | } |
223 | return new SoapHeader( |
224 | 'http://www.sirsidynix.com/xmlns/common/header', |
225 | 'SdHeader', |
226 | $data |
227 | ); |
228 | } |
229 | |
230 | /** |
231 | * Return a SymWS session token for given credentials. |
232 | * |
233 | * To avoid needing to repeatedly log in the same user, |
234 | * cache acquired session tokens by the credentials provided. |
235 | * If the cached session token is expired or otherwise defective, |
236 | * the caller can use the $reset parameter. |
237 | * |
238 | * @param string $login The login account name |
239 | * @param ?string $password The login password, or null for no password |
240 | * @param bool $reset If true, replace any currently cached token |
241 | * |
242 | * @return string The session token |
243 | */ |
244 | protected function getSessionToken( |
245 | string $login, |
246 | ?string $password = null, |
247 | bool $reset = false |
248 | ) { |
249 | static $sessionTokens = []; |
250 | |
251 | // If we keyed only by $login, we might mistakenly retrieve a valid |
252 | // session token when provided with an invalid password. |
253 | // We hash the credentials to reduce the potential for |
254 | // incompatibilities with key limitations of whatever cache backend |
255 | // an administrator might elect to use for session tokens, |
256 | // and though more expensive, we use a secure hash because |
257 | // what we're hashing contains a password. |
258 | $key = hash('sha256', "$login:$password"); |
259 | |
260 | if (!isset($sessionTokens[$key]) || $reset) { |
261 | if (!$reset && $token = $_SESSION['symws']['session'][$key]) { |
262 | $sessionTokens[$key] = $token; |
263 | } else { |
264 | $params = ['login' => $login]; |
265 | if (isset($password)) { |
266 | $params['password'] = $password; |
267 | } |
268 | |
269 | $response = $this->makeRequest('security', 'loginUser', $params); |
270 | $sessionTokens[$key] = $response->sessionToken; |
271 | $_SESSION['symws']['session'] = $sessionTokens; |
272 | } |
273 | } |
274 | |
275 | return $sessionTokens[$key]; |
276 | } |
277 | |
278 | /** |
279 | * Make a request to Symphony Web Services using the SOAP protocol. |
280 | * |
281 | * @param string $service the SymWS service name |
282 | * @param string $operation the SymWS operation name |
283 | * @param array $parameters the request parameters for the operation |
284 | * @param array $options An associative array of additional options: |
285 | * - 'login': login to use for the operation; omit for configured default |
286 | * credentials or anonymous |
287 | * - 'password': password associated with login; omit for no password |
288 | * - 'header': SoapHeader to use for the request; omit to handle automatically |
289 | * |
290 | * @return mixed the result of the SOAP call |
291 | */ |
292 | protected function makeRequest( |
293 | $service, |
294 | $operation, |
295 | $parameters = [], |
296 | $options = [] |
297 | ) { |
298 | // If provided, use the SoapHeader and skip the rest of makeRequest(). |
299 | if (isset($options['header'])) { |
300 | return $this->getSoapClient($service)->soapCall( |
301 | $operation, |
302 | $parameters, |
303 | null, |
304 | [$options['header']] |
305 | ); |
306 | } |
307 | |
308 | /* Determine what credentials, if any, to use for the SymWS request. |
309 | * |
310 | * If a login and password are specified in $options, use them. |
311 | * If not, for any operation not exempted from SymWS' |
312 | * "Always Require Authentication" option, use the login and password |
313 | * specified in the configuration. Otherwise, proceed anonymously. |
314 | */ |
315 | if (isset($options['login'])) { |
316 | $login = $options['login']; |
317 | $password = $options['password'] ?? null; |
318 | } elseif ( |
319 | isset($options['WebServices']['login']) |
320 | && !in_array( |
321 | $operation, |
322 | ['isRestrictedAccess', 'license', 'loginUser', 'version'] |
323 | ) |
324 | ) { |
325 | $login = $this->config['WebServices']['login']; |
326 | $password = $this->config['WebServices']['password'] ?? null; |
327 | } else { |
328 | $login = null; |
329 | $password = null; |
330 | } |
331 | |
332 | // Attempt the request. |
333 | $soapClient = $this->getSoapClient($service); |
334 | try { |
335 | $header = $this->getSoapHeader($login, $password); |
336 | $soapClient->__setSoapHeaders($header); |
337 | return $soapClient->$operation($parameters); |
338 | } catch (SoapFault $e) { |
339 | $timeoutException = 'ns0:com.sirsidynix.symws.service.' |
340 | . 'exceptions.SecurityServiceException.sessionTimedOut'; |
341 | if ($e->faultcode == $timeoutException) { |
342 | // The SoapHeader's session has expired. Tell |
343 | // getSoapHeader() to have a new one established. |
344 | $header = $this->getSoapHeader($login, $password, true); |
345 | // Try the request again with the new SoapHeader. |
346 | $soapClient->__setSoapHeaders($header); |
347 | return $soapClient->$operation($parameters); |
348 | } elseif ($operation == 'logoutUser') { |
349 | return null; |
350 | } elseif ($operation == 'lookupSessionInfo') { |
351 | // lookupSessionInfo did not exist in SymWS 3.0. |
352 | $this->checkSymwsVersion(); |
353 | throw $e; |
354 | } else { |
355 | throw $e; |
356 | } |
357 | } |
358 | } |
359 | |
360 | /** |
361 | * Check the SymWS version, and throw an Exception if it's too old. |
362 | * |
363 | * Always checking at initialization would result in many unnecessary |
364 | * roundtrips with the SymWS server, so this method is intended to be |
365 | * called when an error happens that might be correctable by upgrading |
366 | * SymWS. In such a case it will produce a potentially more helpful error |
367 | * message than the original error would have. |
368 | * |
369 | * @throws \Exception if the SymWS version is too old |
370 | * @return void |
371 | */ |
372 | protected function checkSymwsVersion() |
373 | { |
374 | $resp = $this->makeRequest('standard', 'version', []); |
375 | foreach ($resp->version as $v) { |
376 | if ($v->product == 'SYM-WS') { |
377 | if (version_compare($v->version, 'v3.2', '<')) { |
378 | // ILSException didn't seem to produce an error message |
379 | // when checkSymwsVersion() was called from the catch |
380 | // block in makeRequest(). |
381 | throw new \Exception('SymWS version too old'); |
382 | } |
383 | break; |
384 | } |
385 | } |
386 | } |
387 | |
388 | /** |
389 | * Get Statuses from 999 Holdings Marc Tag |
390 | * |
391 | * Protected support method for parsing status info from the marc record |
392 | * |
393 | * @param array $ids The array of record ids to retrieve the item info for |
394 | * |
395 | * @return array An associative array of items |
396 | */ |
397 | protected function getStatuses999Holdings($ids) |
398 | { |
399 | $items = []; |
400 | $marcMap = [ |
401 | 'call number' => 'marc|a', |
402 | 'copy number' => 'marc|c', |
403 | 'barcode number' => 'marc|i', |
404 | 'library' => 'marc|m', |
405 | 'current location' => 'marc|k', |
406 | 'home location' => 'marc|l', |
407 | 'item type' => 'marc|t', |
408 | 'circulate flag' => 'marc|r', |
409 | ]; |
410 | |
411 | $entryNumber = $this->config['999Holdings']['entry_number']; |
412 | |
413 | $records = $this->recordLoader->loadBatch($ids); |
414 | foreach ($records as $record) { |
415 | $results = $record->getFormattedMarcDetails($entryNumber, $marcMap); |
416 | foreach ($results as $result) { |
417 | $library = $this->translatePolicyID('LIBR', $result['library']); |
418 | $home_loc |
419 | = $this->translatePolicyID('LOCN', $result['home location']); |
420 | |
421 | $curr_loc = isset($result['current location']) ? |
422 | $this->translatePolicyID('LOCN', $result['current location']) : |
423 | $home_loc; |
424 | |
425 | $available = (empty($curr_loc) || $curr_loc == $home_loc) |
426 | || $result['circulate flag'] == 'Y'; |
427 | $callnumber = $result['call number']; |
428 | $location = $library . ' - ' . ($available && !empty($curr_loc) |
429 | ? $curr_loc : $home_loc); |
430 | |
431 | $material = $this->translatePolicyID('ITYP', $result['item type']); |
432 | |
433 | $items[$result['id']][] = [ |
434 | 'id' => $result['id'], |
435 | 'availability' => $available, |
436 | 'status' => $curr_loc, |
437 | 'location' => $location, |
438 | 'reserve' => null, |
439 | 'callnumber' => $callnumber, |
440 | 'duedate' => null, |
441 | 'returnDate' => false, |
442 | 'number' => $result['copy number'], |
443 | 'barcode' => $result['barcode number'], |
444 | 'item_id' => $result['barcode number'], |
445 | 'library' => $library, |
446 | 'material' => $material, |
447 | ]; |
448 | } |
449 | } |
450 | return $items; |
451 | } |
452 | |
453 | /** |
454 | * Look up title info |
455 | * |
456 | * Protected support method for parsing the call info into items. |
457 | * |
458 | * @param array $ids The array of record ids to retrieve the item info for |
459 | * |
460 | * @return object Result of the "lookupTitleInfo" call to the standard service |
461 | */ |
462 | protected function lookupTitleInfo($ids) |
463 | { |
464 | $ids = is_array($ids) ? $ids : [$ids]; |
465 | |
466 | // SymWS ignores invalid titleIDs instead of rejecting them, so |
467 | // checking ahead of time for obviously invalid titleIDs is a useful |
468 | // sanity check (which has a good chance of catching, for example, |
469 | // the use of something other than catkeys as record IDs). |
470 | $invalid = preg_grep('/^[1-9][0-9]*$/', $ids, PREG_GREP_INVERT); |
471 | if (count($invalid) > 0) { |
472 | $titleIDs = count($invalid) == 1 ? 'titleID' : 'titleIDs'; |
473 | $msg = "Invalid $titleIDs: " . implode(', ', $invalid); |
474 | throw new ILSException($msg); |
475 | } |
476 | |
477 | // Prepare $params array for makeRequest(). |
478 | $params = [ |
479 | 'titleID' => $ids, |
480 | 'includeAvailabilityInfo' => 'true', |
481 | 'includeItemInfo' => 'true', |
482 | 'includeBoundTogether' => 'true', |
483 | 'includeOrderInfo' => 'true', |
484 | ]; |
485 | |
486 | // If the driver is configured to populate holdings_text_fields |
487 | // with MFHD, also request MARC holdings information from SymWS. |
488 | if (count(array_filter($this->config['MarcHoldings'])) > 0) { |
489 | $params['includeMarcHoldings'] = 'true'; |
490 | // With neither marcEntryFilter nor marcEntryID, or with |
491 | // marcEntryFilter NONE, SymWS won't return MarcHoldingsInfo, |
492 | // and there doesn't seem to be another option for marcEntryFilter |
493 | // that returns just MarcHoldingsInfo without BibliographicInfo. |
494 | // So we filter BibliographicInfo for an unlikely entry. |
495 | $params['marcEntryID'] = '999'; |
496 | } |
497 | |
498 | // If only one library is being exclusively included, |
499 | // filtering can be done within Web Services. |
500 | if (count($this->config['LibraryFilter']['include_only']) == 1) { |
501 | $params['libraryFilter'] |
502 | = $this->config['LibraryFilter']['include_only'][0]; |
503 | } |
504 | |
505 | return $this->makeRequest('standard', 'lookupTitleInfo', $params); |
506 | } |
507 | |
508 | /** |
509 | * Determine if a library is excluded by LibraryFilter configuration. |
510 | * |
511 | * @param string $libraryID the ID of the library in question |
512 | * |
513 | * @return bool true if excluded, false if not |
514 | */ |
515 | protected function libraryIsFilteredOut($libraryID) |
516 | { |
517 | $notIncluded = !empty($this->config['LibraryFilter']['include_only']) |
518 | && !in_array( |
519 | $libraryID, |
520 | $this->config['LibraryFilter']['include_only'] |
521 | ); |
522 | $excluded = in_array( |
523 | $libraryID, |
524 | $this->config['LibraryFilter']['exclude'] |
525 | ); |
526 | return $notIncluded || $excluded; |
527 | } |
528 | |
529 | /** |
530 | * Parse Call Info |
531 | * |
532 | * Protected support method for parsing the call info into items. |
533 | * |
534 | * @param object $callInfos The call info of the title |
535 | * @param int $titleID The catalog key of the title in the catalog |
536 | * @param bool $is_holdable Whether or not the title is holdable |
537 | * @param int $bound_in The ID of the parent title |
538 | * |
539 | * @return array An array of items, an empty array otherwise |
540 | */ |
541 | protected function parseCallInfo( |
542 | $callInfos, |
543 | $titleID, |
544 | $is_holdable = false, |
545 | $bound_in = null |
546 | ) { |
547 | $items = []; |
548 | |
549 | $callInfos = is_array($callInfos) ? $callInfos : [$callInfos]; |
550 | |
551 | foreach ($callInfos as $callInfo) { |
552 | $libraryID = $callInfo->libraryID; |
553 | |
554 | if ($this->libraryIsFilteredOut($libraryID)) { |
555 | continue; |
556 | } |
557 | |
558 | if (!isset($callInfo->ItemInfo)) { |
559 | continue; // no items! |
560 | } |
561 | |
562 | $library = $this->translatePolicyID('LIBR', $libraryID); |
563 | // ItemInfo does not include copy numbers, so we generate them under |
564 | // the assumption that items are being listed in order. |
565 | $copyNumber = 0; |
566 | |
567 | $itemInfos = is_array($callInfo->ItemInfo) |
568 | ? $callInfo->ItemInfo |
569 | : [$callInfo->ItemInfo]; |
570 | foreach ($itemInfos as $itemInfo) { |
571 | $in_transit = isset($itemInfo->transitReason); |
572 | $currentLocation = $this->translatePolicyID( |
573 | 'LOCN', |
574 | $itemInfo->currentLocationID |
575 | ); |
576 | $homeLocation = $this->translatePolicyID( |
577 | 'LOCN', |
578 | $itemInfo->homeLocationID |
579 | ); |
580 | |
581 | /* I would like to be able to write |
582 | * $available = $itemInfo->numberOfCharges == 0; |
583 | * but SymWS does not appear to provide that information. |
584 | * |
585 | * SymWS *will* tell me if an item is "chargeable", |
586 | * but this is inadequate because reference and internet |
587 | * materials may be available, but not chargeable. |
588 | * |
589 | * I can't rely on the presence of dueDate, because |
590 | * although "dueDate is only returned if the item is currently |
591 | * checked out", the converse is not true: due dates of NEVER |
592 | * are simply omitted. |
593 | * |
594 | * TitleAvailabilityInfo would be more helpful per item; |
595 | * as it is, it tells me only number available and library. |
596 | * |
597 | * Hence the following criterion: an available item must not |
598 | * be in-transit, and if it, like exhibits and reserves, |
599 | * is not in its home location, it must be chargeable. |
600 | */ |
601 | $available = !$in_transit && |
602 | ($itemInfo->currentLocationID == $itemInfo->homeLocationID |
603 | || $itemInfo->chargeable); |
604 | |
605 | /* Statuses like "Checked out" and "Missing" are represented |
606 | * by an item's current location. */ |
607 | $status = $in_transit ? 'In transit' : $currentLocation; |
608 | |
609 | /* "$library - $location" may be misleading for items that are |
610 | * on reserve at a reserve desk in another library, so for |
611 | * items on reserve, report location as just the reserve desk. |
612 | */ |
613 | if (isset($itemInfo->reserveCollectionID)) { |
614 | $reserveDeskID = $itemInfo->reserveCollectionID; |
615 | $location = $this->translatePolicyID('RSRV', $reserveDeskID); |
616 | } else { |
617 | /* If an item is available, its current location should be |
618 | * reported as its location. */ |
619 | $location = $available ? $currentLocation : $homeLocation; |
620 | |
621 | /* Locations may be shared among libraries, so unless |
622 | * holdings are being filtered to just one library, |
623 | * it is insufficient to provide just the location |
624 | * description as the "location." |
625 | */ |
626 | if (count($this->config['LibraryFilter']['include_only']) != 1) { |
627 | $location = "$library - $location"; |
628 | } |
629 | } |
630 | |
631 | $material = $this->translatePolicyID('ITYP', $itemInfo->itemTypeID); |
632 | |
633 | $duedate = isset($itemInfo->dueDate) ? |
634 | date('F j, Y', strtotime($itemInfo->dueDate)) : null; |
635 | $duedate = isset($itemInfo->recallDueDate) ? |
636 | date('F j, Y', strtotime($itemInfo->recallDueDate)) : |
637 | $duedate; |
638 | |
639 | $requests_placed = $itemInfo->numberOfHolds ?? 0; |
640 | |
641 | // Handle item notes |
642 | $notes = []; |
643 | |
644 | if (isset($itemInfo->publicNote)) { |
645 | $notes[] = $itemInfo->publicNote; |
646 | } |
647 | |
648 | if ( |
649 | isset($itemInfo->staffNote) |
650 | && $this->config['Behaviors']['showStaffNotes'] |
651 | ) { |
652 | $notes[] = $itemInfo->staffNote; |
653 | } |
654 | |
655 | $transitSourceLibrary |
656 | = isset($itemInfo->transitSourceLibraryID) |
657 | ? $this->translatePolicyID( |
658 | 'LIBR', |
659 | $itemInfo->transitSourceLibraryID |
660 | ) |
661 | : null; |
662 | |
663 | $transitDestinationLibrary |
664 | = isset($itemInfo->transitDestinationLibraryID) |
665 | ? $this->translatePolicyID( |
666 | 'LIBR', |
667 | $itemInfo->transitDestinationLibraryID |
668 | ) |
669 | : null; |
670 | |
671 | $transitReason = $itemInfo->transitReason ?? null; |
672 | |
673 | $transitDate = isset($itemInfo->transitDate) ? |
674 | date('F j, Y', strtotime($itemInfo->transitDate)) : null; |
675 | |
676 | $holdtype = $available ? 'hold' : 'recall'; |
677 | |
678 | $items[] = [ |
679 | 'id' => $titleID, |
680 | 'availability' => $available, |
681 | 'status' => $status, |
682 | 'location' => $location, |
683 | 'reserve' => isset($itemInfo->reserveCollectionID) |
684 | ? 'Y' : 'N', |
685 | 'callnumber' => $callInfo->callNumber, |
686 | 'duedate' => $duedate, |
687 | 'returnDate' => false, // Not returned by symws |
688 | 'number' => ++$copyNumber, |
689 | 'requests_placed' => $requests_placed, |
690 | 'barcode' => $itemInfo->itemID, |
691 | 'notes' => $notes, |
692 | 'summary' => [], |
693 | 'is_holdable' => $is_holdable, |
694 | 'holdtype' => $holdtype, |
695 | 'addLink' => $is_holdable, |
696 | 'item_id' => $itemInfo->itemID, |
697 | |
698 | // The fields below are non-standard and |
699 | // should be added to your holdings.tpl |
700 | // RecordDriver template to be utilized. |
701 | 'library' => $library, |
702 | 'material' => $material, |
703 | 'bound_in' => $bound_in, |
704 | //'bound_in_title' => , |
705 | 'transit_source_library' => |
706 | $transitSourceLibrary, |
707 | 'transit_destination_library' => |
708 | $transitDestinationLibrary, |
709 | 'transit_reason' => $transitReason, |
710 | 'transit_date' => $transitDate, |
711 | ]; |
712 | } |
713 | } |
714 | return $items; |
715 | } |
716 | |
717 | /** |
718 | * Parse Bound With Link Info |
719 | * |
720 | * Protected support method for parsing bound with link information. |
721 | * |
722 | * @param object $boundwithLinkInfos The boundwithLinkInfos object of the title |
723 | * @param int $ckey The catalog key of the title in the catalog |
724 | * |
725 | * @return array An array of parseCallInfo() return values on success, |
726 | * an empty array otherwise. |
727 | */ |
728 | protected function parseBoundwithLinkInfo($boundwithLinkInfos, $ckey) |
729 | { |
730 | $items = []; |
731 | |
732 | $boundwithLinkInfos = is_array($boundwithLinkInfos) |
733 | ? $boundwithLinkInfos |
734 | : [$boundwithLinkInfos]; |
735 | |
736 | foreach ($boundwithLinkInfos as $boundwithLinkInfo) { |
737 | // Ignore BoundwithLinkInfos which do not refer to parents |
738 | // or which refer to the record we're already looking at. |
739 | if ( |
740 | !$boundwithLinkInfo->linkedAsParent |
741 | || $boundwithLinkInfo->linkedTitle->titleID == $ckey |
742 | ) { |
743 | continue; |
744 | } |
745 | |
746 | // Fetch the record that contains the parent CallInfo, |
747 | // identify the CallInfo by matching itemIDs, |
748 | // and parse that CallInfo in the items array. |
749 | $parent_ckey = $boundwithLinkInfo->linkedTitle->titleID; |
750 | $linked_itemID = $boundwithLinkInfo->itemID; |
751 | $resp = $this->lookupTitleInfo($parent_ckey); |
752 | $is_holdable = $resp->TitleInfo->TitleAvailabilityInfo->holdable; |
753 | |
754 | $callInfos = is_array($resp->TitleInfo->CallInfo) |
755 | ? $resp->TitleInfo->CallInfo |
756 | : [$resp->TitleInfo->CallInfo]; |
757 | |
758 | foreach ($callInfos as $callInfo) { |
759 | $itemInfos = is_array($callInfo->ItemInfo) |
760 | ? $callInfo->ItemInfo |
761 | : [$callInfo->ItemInfo]; |
762 | foreach ($itemInfos as $itemInfo) { |
763 | if ($itemInfo->itemID == $linked_itemID) { |
764 | $items += $this->parseCallInfo( |
765 | $callInfo, |
766 | $ckey, |
767 | $is_holdable, |
768 | $parent_ckey |
769 | ); |
770 | } |
771 | } |
772 | } |
773 | } |
774 | |
775 | return $items; |
776 | } |
777 | |
778 | /** |
779 | * Parse Title Order Info |
780 | * |
781 | * Protected support method for parsing order info. |
782 | * |
783 | * @param object $titleOrderInfos The titleOrderInfo object of the title |
784 | * @param int $titleID The ID of the title in the catalog |
785 | * |
786 | * @return array An array of items that are on order, an empty array otherwise. |
787 | */ |
788 | protected function parseTitleOrderInfo($titleOrderInfos, $titleID) |
789 | { |
790 | $items = []; |
791 | |
792 | $titleOrderInfos = is_array($titleOrderInfos) |
793 | ? $titleOrderInfos : [$titleOrderInfos]; |
794 | |
795 | foreach ($titleOrderInfos as $titleOrderInfo) { |
796 | $library_id = $titleOrderInfo->orderLibraryID; |
797 | |
798 | /* Allow returned holdings information to be |
799 | * limited to a specified list of library names. */ |
800 | if ( |
801 | isset($this->config['holdings']['include_libraries']) |
802 | && !in_array( |
803 | $library_id, |
804 | $this->config['holdings']['include_libraries'] |
805 | ) |
806 | ) { |
807 | continue; |
808 | } |
809 | |
810 | /* Allow libraries to be excluded by name |
811 | * from returned holdings information. */ |
812 | if ( |
813 | isset($this->config['holdings']['exclude_libraries']) |
814 | && in_array( |
815 | $library_id, |
816 | $this->config['holdings']['exclude_libraries'] |
817 | ) |
818 | ) { |
819 | continue; |
820 | } |
821 | |
822 | $nr_copies = $titleOrderInfo->copiesOrdered; |
823 | $library = $this->translatePolicyID('LIBR', $library_id); |
824 | |
825 | $statuses = []; |
826 | if (!empty($titleOrderInfo->orderDateReceived)) { |
827 | $statuses[] = "Received $titleOrderInfo->orderDateReceived"; |
828 | } |
829 | |
830 | if (!empty($titleOrderInfo->orderNote)) { |
831 | $statuses[] = $titleOrderInfo->orderNote; |
832 | } |
833 | |
834 | if (!empty($titleOrderInfo->volumesOrdered)) { |
835 | $statuses[] = $titleOrderInfo->volumesOrdered; |
836 | } |
837 | |
838 | for ($i = 1; $i <= $nr_copies; ++$i) { |
839 | $items[] = [ |
840 | 'id' => $titleID, |
841 | 'availability' => false, |
842 | 'status' => implode('; ', $statuses), |
843 | 'location' => "On order for $library", |
844 | 'callnumber' => null, |
845 | 'duedate' => null, |
846 | 'reserve' => 'N', |
847 | 'number' => $i, |
848 | 'barcode' => true, |
849 | 'offsite' => $library_id == 'OFFSITE', |
850 | ]; |
851 | } |
852 | } |
853 | return $items; |
854 | } |
855 | |
856 | /** |
857 | * Parse MarcHoldingInfo into VuFind items. |
858 | * |
859 | * @param object $marcHoldingsInfos MarcHoldingInfo, from TitleInfo |
860 | * @param int $titleID The catalog key of the title record |
861 | * |
862 | * @return array an array (possibly empty) of VuFind items |
863 | */ |
864 | protected function parseMarcHoldingsInfo($marcHoldingsInfos, $titleID) |
865 | { |
866 | $items = []; |
867 | $marcHoldingsInfos = is_array($marcHoldingsInfos) |
868 | ? $marcHoldingsInfos |
869 | : [$marcHoldingsInfos]; |
870 | |
871 | foreach ($marcHoldingsInfos as $marcHoldingsInfo) { |
872 | $libraryID = $marcHoldingsInfo->holdingLibraryID; |
873 | if ($this->libraryIsFilteredOut($libraryID)) { |
874 | continue; |
875 | } |
876 | |
877 | $marcEntryInfos = is_array($marcHoldingsInfo->MarcEntryInfo) |
878 | ? $marcHoldingsInfo->MarcEntryInfo |
879 | : [$marcHoldingsInfo->MarcEntryInfo]; |
880 | $item = []; |
881 | |
882 | foreach ($marcEntryInfos as $marcEntryInfo) { |
883 | foreach ($this->config['MarcHoldings'] as $textfield => $spec) { |
884 | if (in_array($marcEntryInfo->entryID, $spec)) { |
885 | $item[$textfield][] = $marcEntryInfo->text; |
886 | } |
887 | } |
888 | } |
889 | |
890 | if (!empty($item)) { |
891 | $items[] = $item + [ |
892 | 'id' => $titleID, |
893 | 'location' => $this->translatePolicyID('LIBR', $libraryID), |
894 | ]; |
895 | } |
896 | } |
897 | |
898 | return $items; |
899 | } |
900 | |
901 | /** |
902 | * Get Live Statuses |
903 | * |
904 | * Protected support method for retrieving a list of item statuses from symws. |
905 | * |
906 | * @param array $ids The array of record ids to retrieve the status for |
907 | * |
908 | * @return array An array of parseCallInfo() return values on success, |
909 | * an empty array otherwise. |
910 | */ |
911 | protected function getLiveStatuses($ids) |
912 | { |
913 | $items = []; |
914 | foreach ($ids as $id) { |
915 | $items[$id] = []; |
916 | } |
917 | |
918 | /* In Symphony, a title record has at least one "callnum" record, |
919 | * to which are attached zero or more item records. This structure |
920 | * is reflected in the LookupTitleInfoResponse, which contains |
921 | * one or more TitleInfo elements, which contain one or more |
922 | * CallInfo elements, which contain zero or more ItemInfo elements. |
923 | */ |
924 | $response = $this->lookupTitleInfo($ids); |
925 | $titleInfos = is_array($response->TitleInfo) |
926 | ? $response->TitleInfo |
927 | : [$response->TitleInfo]; |
928 | |
929 | foreach ($titleInfos as $titleInfo) { |
930 | $ckey = $titleInfo->titleID; |
931 | $is_holdable = $titleInfo->TitleAvailabilityInfo->holdable; |
932 | |
933 | /* In order to have only one item record per item regardless of |
934 | * how many titles are bound within, Symphony handles titles bound |
935 | * with others by linking callnum records in parent-children |
936 | * relationships, where only the parent callnum has item records |
937 | * attached to it. The CallInfo element of a child callnum |
938 | * does not contain any ItemInfo elements, so we must locate the |
939 | * parent CallInfo using BoundwithLinkInfo, in order to parse |
940 | * the ItemInfo. |
941 | */ |
942 | if (isset($titleInfo->BoundwithLinkInfo)) { |
943 | $items[$ckey] = $this->parseBoundwithLinkInfo( |
944 | $titleInfo->BoundwithLinkInfo, |
945 | $ckey |
946 | ); |
947 | } |
948 | |
949 | /* Callnums that are not bound-with, or are bound-with parents, |
950 | * have item records and can be parsed directly. Since bound-with |
951 | * children do not have item records, parsing them should have no |
952 | * effect. */ |
953 | if (isset($titleInfo->CallInfo)) { |
954 | $items[$ckey] = array_merge( |
955 | $items[$ckey], |
956 | $this->parseCallInfo($titleInfo->CallInfo, $ckey, $is_holdable) |
957 | ); |
958 | } |
959 | |
960 | /* Copies on order do not have item records, |
961 | * so we make some pseudo-items for VuFind. */ |
962 | if (isset($titleInfo->TitleOrderInfo)) { |
963 | $items[$ckey] = array_merge( |
964 | $items[$ckey], |
965 | $this->parseTitleOrderInfo($titleInfo->TitleOrderInfo, $ckey) |
966 | ); |
967 | } |
968 | |
969 | /* MARC holdings records are associated with title records rather |
970 | * than item records, so we make pseudo-items for VuFind. */ |
971 | if (isset($titleInfo->MarcHoldingsInfo)) { |
972 | $items[$ckey] = array_merge( |
973 | $items[$ckey], |
974 | $this->parseMarcHoldingsInfo($titleInfo->MarcHoldingsInfo, $ckey) |
975 | ); |
976 | } |
977 | } |
978 | return $items; |
979 | } |
980 | |
981 | /** |
982 | * Translate a Symphony policy ID into a policy description |
983 | * (e.g. VIDEO-COLL => Videorecording Collection). |
984 | * |
985 | * In order to minimize roundtrips with the SymWS server, |
986 | * we fetch more than was requested and cache the results. |
987 | * At time of writing, SymWS did not appear to |
988 | * support retrieving policies of multiple types simultaneously, |
989 | * so we currently fetch only all policies of one type at a time. |
990 | * |
991 | * @param string $policyType The policy type, e.g. LOCN or LIBR. |
992 | * @param string $policyID The policy ID, e.g. VIDEO-COLL or SWEM. |
993 | * |
994 | * @return string The policy description, if found, or the policy ID, if not. |
995 | * |
996 | * @todo policy description override |
997 | */ |
998 | protected function translatePolicyID($policyType, $policyID) |
999 | { |
1000 | $policyType = strtoupper($policyType); |
1001 | $policyID = strtoupper($policyID); |
1002 | $policyList = $this->getPolicyList($policyType); |
1003 | |
1004 | return $policyList[$policyID] ?? $policyID; |
1005 | } |
1006 | |
1007 | /** |
1008 | * Get Status |
1009 | * |
1010 | * This is responsible for retrieving the status information of a certain |
1011 | * record. |
1012 | * |
1013 | * @param string $id The record id to retrieve the holdings for |
1014 | * |
1015 | * @throws ILSException |
1016 | * @return mixed On success, an associative array with the following keys: |
1017 | * id, availability (boolean), status, location, reserve, callnumber. |
1018 | */ |
1019 | public function getStatus($id) |
1020 | { |
1021 | $statuses = $this->getStatuses([$id]); |
1022 | return $statuses[$id] ?? []; |
1023 | } |
1024 | |
1025 | /** |
1026 | * Get Statuses |
1027 | * |
1028 | * This is responsible for retrieving the status information for a |
1029 | * collection of records. |
1030 | * |
1031 | * @param array $ids The array of record ids to retrieve the status for |
1032 | * |
1033 | * @throws ILSException |
1034 | * @return array An array of getStatus() return values on success. |
1035 | */ |
1036 | public function getStatuses($ids) |
1037 | { |
1038 | if ($this->config['999Holdings']['mode']) { |
1039 | return $this->getStatuses999Holdings($ids); |
1040 | } else { |
1041 | return $this->getLiveStatuses($ids); |
1042 | } |
1043 | } |
1044 | |
1045 | /** |
1046 | * Get Holding |
1047 | * |
1048 | * This is responsible for retrieving the holding information of a certain |
1049 | * record. |
1050 | * |
1051 | * @param string $id The record id to retrieve the holdings for |
1052 | * @param array $patron Patron data |
1053 | * @param array $options Extra options (not currently used) |
1054 | * |
1055 | * @throws ILSException |
1056 | * @return array On success, an associative array with the following |
1057 | * keys: id, availability (boolean), status, location, reserve, callnumber, |
1058 | * duedate, number, barcode. |
1059 | * |
1060 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1061 | */ |
1062 | public function getHolding($id, array $patron = null, array $options = []) |
1063 | { |
1064 | return $this->getStatus($id); |
1065 | } |
1066 | |
1067 | /** |
1068 | * Get Purchase History |
1069 | * |
1070 | * This is responsible for retrieving the acquisitions history data for the |
1071 | * specific record (usually recently received issues of a serial). |
1072 | * |
1073 | * @param string $id The record id to retrieve the info for |
1074 | * |
1075 | * @throws ILSException |
1076 | * @return array An array with the acquisitions data on success. |
1077 | */ |
1078 | public function getPurchaseHistory($id) |
1079 | { |
1080 | return []; |
1081 | } |
1082 | |
1083 | /** |
1084 | * Patron Login |
1085 | * |
1086 | * This is responsible for authenticating a patron against the catalog. |
1087 | * |
1088 | * @param string $username The patron username |
1089 | * @param string $password The patron password |
1090 | * |
1091 | * @throws ILSException |
1092 | * @return mixed Associative array of patron info on successful login, |
1093 | * null on unsuccessful login. |
1094 | */ |
1095 | public function patronLogin($username, $password) |
1096 | { |
1097 | $usernameField = $this->config['Behaviors']['usernameField']; |
1098 | |
1099 | $patron = [ |
1100 | 'cat_username' => $username, |
1101 | 'cat_password' => $password, |
1102 | ]; |
1103 | |
1104 | try { |
1105 | $resp = $this->makeRequest( |
1106 | 'patron', |
1107 | 'lookupMyAccountInfo', |
1108 | [ |
1109 | 'includePatronInfo' => 'true', |
1110 | 'includePatronAddressInfo' => 'true', |
1111 | ], |
1112 | [ |
1113 | 'login' => $username, |
1114 | 'password' => $password, |
1115 | ] |
1116 | ); |
1117 | } catch (SoapFault $e) { |
1118 | $unableToLogin = 'ns0:com.sirsidynix.symws.service.' |
1119 | . 'exceptions.SecurityServiceException.unableToLogin'; |
1120 | if ($e->faultcode == $unableToLogin) { |
1121 | return null; |
1122 | } else { |
1123 | throw $e; |
1124 | } |
1125 | } |
1126 | |
1127 | $patron['id'] = $resp->patronInfo->$usernameField; |
1128 | $patron['library'] = $resp->patronInfo->patronLibraryID; |
1129 | |
1130 | $regEx = '/([^,]*),\s([^\s]*)/'; |
1131 | if (preg_match($regEx, $resp->patronInfo->displayName, $matches)) { |
1132 | $patron['firstname'] = $matches[2]; |
1133 | $patron['lastname'] = $matches[1]; |
1134 | } |
1135 | |
1136 | // There may be an email address in any of three numbered addresses, |
1137 | // so we search each one until we find an email address, |
1138 | // starting with the one marked primary. |
1139 | $addrinfo_check_order = ['1','2','3']; |
1140 | if (isset($resp->patronAddressInfo->primaryAddress)) { |
1141 | $primary_addr_n = $resp->patronAddressInfo->primaryAddress; |
1142 | array_unshift($addrinfo_check_order, $primary_addr_n); |
1143 | } |
1144 | foreach ($addrinfo_check_order as $n) { |
1145 | $AddressNInfo = "Address{$n}Info"; |
1146 | if (isset($resp->patronAddressInfo->$AddressNInfo)) { |
1147 | $addrinfos = is_array($resp->patronAddressInfo->$AddressNInfo) |
1148 | ? $resp->patronAddressInfo->$AddressNInfo |
1149 | : [$resp->patronAddressInfo->$AddressNInfo]; |
1150 | foreach ($addrinfos as $addrinfo) { |
1151 | if ( |
1152 | $addrinfo->addressPolicyID == 'EMAIL' |
1153 | && !empty($addrinfo->addressValue) |
1154 | ) { |
1155 | $patron['email'] = $addrinfo->addressValue; |
1156 | break; |
1157 | } |
1158 | } |
1159 | } |
1160 | } |
1161 | |
1162 | // @TODO: major, college |
1163 | |
1164 | return $patron; |
1165 | } |
1166 | |
1167 | /** |
1168 | * Get Patron Profile |
1169 | * |
1170 | * This is responsible for retrieving the profile for a specific patron. |
1171 | * |
1172 | * @param array $patron The patron array |
1173 | * |
1174 | * @throws ILSException |
1175 | * @return array Array of the patron's profile data on success. |
1176 | */ |
1177 | public function getMyProfile($patron) |
1178 | { |
1179 | try { |
1180 | $userProfileGroupField |
1181 | = $this->config['Behaviors']['userProfileGroupField']; |
1182 | |
1183 | $options = [ |
1184 | 'includePatronInfo' => 'true', |
1185 | 'includePatronAddressInfo' => 'true', |
1186 | 'includePatronStatusInfo' => 'true', |
1187 | 'includeUserGroupInfo' => 'true', |
1188 | ]; |
1189 | |
1190 | $result = $this->makeRequest( |
1191 | 'patron', |
1192 | 'lookupMyAccountInfo', |
1193 | $options, |
1194 | [ |
1195 | 'login' => $patron['cat_username'], |
1196 | 'password' => $patron['cat_password'], |
1197 | ] |
1198 | ); |
1199 | |
1200 | $primaryAddress = $result->patronAddressInfo->primaryAddress; |
1201 | |
1202 | $primaryAddressInfo = 'Address' . $primaryAddress . 'Info'; |
1203 | |
1204 | $addressInfo = $result->patronAddressInfo->$primaryAddressInfo; |
1205 | $address1 = $addressInfo[0]->addressValue; |
1206 | $address2 = $addressInfo[1]->addressValue; |
1207 | $zip = $addressInfo[2]->addressValue; |
1208 | $phone = $addressInfo[3]->addressValue; |
1209 | |
1210 | if (strcmp($userProfileGroupField, 'GROUP_ID') == 0) { |
1211 | $group = $result->patronInfo->groupID; |
1212 | } elseif (strcmp($userProfileGroupField, 'USER_PROFILE_ID') == 0) { |
1213 | $group = $this->makeRequest( |
1214 | 'security', |
1215 | 'lookupSessionInfo', |
1216 | $options, |
1217 | [ |
1218 | 'login' => $patron['cat_username'], |
1219 | 'password' => $patron['cat_password'], |
1220 | ] |
1221 | )->userProfileID; |
1222 | } elseif (strcmp($userProfileGroupField, 'PATRON_LIBRARY_ID') == 0) { |
1223 | $group = $result->patronInfo->patronLibraryID; |
1224 | } elseif (strcmp($userProfileGroupField, 'DEPARTMENT') == 0) { |
1225 | $group = $result->patronInfo->department; |
1226 | } else { |
1227 | $group = null; |
1228 | } |
1229 | |
1230 | [$lastname, $firstname] |
1231 | = explode(', ', $result->patronInfo->displayName); |
1232 | |
1233 | $profile = [ |
1234 | 'lastname' => $lastname, |
1235 | 'firstname' => $firstname, |
1236 | 'address1' => $address1, |
1237 | 'address2' => $address2, |
1238 | 'zip' => $zip, |
1239 | 'phone' => $phone, |
1240 | 'group' => $group, |
1241 | ]; |
1242 | } catch (\Exception $e) { |
1243 | $this->throwAsIlsException($e); |
1244 | } |
1245 | return $profile; |
1246 | } |
1247 | |
1248 | /** |
1249 | * Get Patron Transactions |
1250 | * |
1251 | * This is responsible for retrieving all transactions (i.e. checked out items) |
1252 | * by a specific patron. |
1253 | * |
1254 | * @param array $patron The patron array from patronLogin |
1255 | * |
1256 | * @throws ILSException |
1257 | * @return array Array of the patron's transactions on success. |
1258 | */ |
1259 | public function getMyTransactions($patron) |
1260 | { |
1261 | try { |
1262 | $transList = []; |
1263 | $options = ['includePatronCheckoutInfo' => 'ALL']; |
1264 | |
1265 | $result = $this->makeRequest( |
1266 | 'patron', |
1267 | 'lookupMyAccountInfo', |
1268 | $options, |
1269 | [ |
1270 | 'login' => $patron['cat_username'], |
1271 | 'password' => $patron['cat_password'], |
1272 | ] |
1273 | ); |
1274 | |
1275 | if (isset($result->patronCheckoutInfo)) { |
1276 | $transactions = $result->patronCheckoutInfo; |
1277 | $transactions = !is_array($transactions) ? [$transactions] : |
1278 | $transactions; |
1279 | |
1280 | foreach ($transactions as $transaction) { |
1281 | $urr = !empty($transaction->unseenRenewalsRemaining) |
1282 | || !empty($transaction->unseenRenewalsRemainingUnlimited); |
1283 | $rr = !empty($transaction->renewalsRemaining) |
1284 | || !empty($transaction->renewalsRemainingUnlimited); |
1285 | $renewable = ($urr && $rr); |
1286 | |
1287 | $transList[] = [ |
1288 | 'duedate' => |
1289 | date('F j, Y', strtotime($transaction->dueDate)), |
1290 | 'id' => $transaction->titleKey, |
1291 | 'barcode' => $transaction->itemID, |
1292 | 'renew' => $transaction->renewals, |
1293 | 'request' => $transaction->recallNoticesSent, |
1294 | //'volume' => null, |
1295 | //'publication_year' => null, |
1296 | 'renewable' => $renewable, |
1297 | //'message' => null, |
1298 | 'title' => $transaction->title, |
1299 | 'item_id' => $transaction->itemID, |
1300 | ]; |
1301 | } |
1302 | } |
1303 | } catch (\Exception $e) { |
1304 | $this->throwAsIlsException($e); |
1305 | } |
1306 | return $transList; |
1307 | } |
1308 | |
1309 | /** |
1310 | * Get Patron Holds |
1311 | * |
1312 | * This is responsible for retrieving all holds by a specific patron. |
1313 | * |
1314 | * @param array $patron The patron array from patronLogin |
1315 | * |
1316 | * @throws ILSException |
1317 | * @return array Array of the patron's holds on success. |
1318 | */ |
1319 | public function getMyHolds($patron) |
1320 | { |
1321 | try { |
1322 | $holdList = []; |
1323 | $options = ['includePatronHoldInfo' => 'ACTIVE']; |
1324 | |
1325 | $result = $this->makeRequest( |
1326 | 'patron', |
1327 | 'lookupMyAccountInfo', |
1328 | $options, |
1329 | [ |
1330 | 'login' => $patron['cat_username'], |
1331 | 'password' => $patron['cat_password'], |
1332 | ] |
1333 | ); |
1334 | |
1335 | if (!property_exists($result, 'patronHoldInfo')) { |
1336 | return null; |
1337 | } |
1338 | |
1339 | $holds = $result->patronHoldInfo; |
1340 | $holds = !is_array($holds) ? [$holds] : $holds; |
1341 | |
1342 | foreach ($holds as $hold) { |
1343 | $holdList[] = [ |
1344 | 'id' => $hold->titleKey, |
1345 | //'type' => , |
1346 | 'location' => $hold->pickupLibraryID, |
1347 | 'reqnum' => $hold->holdKey, |
1348 | 'expire' => date('F j, Y', strtotime($hold->expiresDate)), |
1349 | 'create' => date('F j, Y', strtotime($hold->placedDate)), |
1350 | 'position' => $hold->queuePosition, |
1351 | 'available' => $hold->available, |
1352 | 'item_id' => $hold->itemID, |
1353 | //'volume' => null, |
1354 | //'publication_year' => null, |
1355 | 'title' => $hold->title, |
1356 | ]; |
1357 | } |
1358 | } catch (SoapFault $e) { |
1359 | return null; |
1360 | } catch (\Exception $e) { |
1361 | $this->throwAsIlsException($e); |
1362 | } |
1363 | return $holdList; |
1364 | } |
1365 | |
1366 | /** |
1367 | * Get Patron Fines |
1368 | * |
1369 | * This is responsible for retrieving all fines by a specific patron. |
1370 | * |
1371 | * @param array $patron The patron array from patronLogin |
1372 | * |
1373 | * @throws ILSException |
1374 | * @return mixed Array of the patron's fines on success. |
1375 | */ |
1376 | public function getMyFines($patron) |
1377 | { |
1378 | try { |
1379 | $fineList = []; |
1380 | $feeType = $this->config['Behaviors']['showFeeType']; |
1381 | $options = ['includeFeeInfo' => $feeType]; |
1382 | |
1383 | $result = $this->makeRequest( |
1384 | 'patron', |
1385 | 'lookupMyAccountInfo', |
1386 | $options, |
1387 | [ |
1388 | 'login' => $patron['cat_username'], |
1389 | 'password' => $patron['cat_password'], |
1390 | ] |
1391 | ); |
1392 | |
1393 | if (isset($result->feeInfo)) { |
1394 | $fees = $result->feeInfo; |
1395 | $fees = !is_array($fees) ? [$fees] : $fees; |
1396 | |
1397 | foreach ($fees as $fee) { |
1398 | $fineList[] = [ |
1399 | 'amount' => $fee->amount->_ * 100, |
1400 | 'checkout' => $fee->feeItemInfo->checkoutDate ?? null, |
1401 | 'fine' => $fee->billReasonDescription, |
1402 | 'balance' => $fee->amountOutstanding->_ * 100, |
1403 | 'createdate' => $fee->dateBilled ?? null, |
1404 | 'duedate' => $fee->feeItemInfo->dueDate ?? null, |
1405 | 'id' => $fee->feeItemInfo->titleKey ?? null, |
1406 | ]; |
1407 | } |
1408 | } |
1409 | |
1410 | return $fineList; |
1411 | } catch (SoapFault | \Exception $e) { |
1412 | $this->throwAsIlsException($e); |
1413 | } |
1414 | } |
1415 | |
1416 | /** |
1417 | * Get Cancel Hold Form |
1418 | * |
1419 | * Supplies the form details required to cancel a hold |
1420 | * |
1421 | * @param array $holdDetails A single hold array from getMyHolds |
1422 | * @param array $patron Patron information from patronLogin |
1423 | * |
1424 | * @return string Data for use in a form field |
1425 | * |
1426 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1427 | */ |
1428 | public function getCancelHoldDetails($holdDetails, $patron = []) |
1429 | { |
1430 | return $holdDetails['reqnum']; |
1431 | } |
1432 | |
1433 | /** |
1434 | * Cancel Holds |
1435 | * |
1436 | * Attempts to Cancel a hold on a particular item |
1437 | * |
1438 | * @param array $cancelDetails An array of item and patron data |
1439 | * |
1440 | * @return mixed An array of data on each request including |
1441 | * whether or not it was successful and a system message (if available) |
1442 | * or boolean false on failure |
1443 | */ |
1444 | public function cancelHolds($cancelDetails) |
1445 | { |
1446 | $count = 0; |
1447 | $items = []; |
1448 | $patron = $cancelDetails['patron']; |
1449 | |
1450 | foreach ($cancelDetails['details'] as $holdKey) { |
1451 | try { |
1452 | $options = ['holdKey' => $holdKey]; |
1453 | |
1454 | $this->makeRequest( |
1455 | 'patron', |
1456 | 'cancelMyHold', |
1457 | $options, |
1458 | [ |
1459 | 'login' => $patron['cat_username'], |
1460 | 'password' => $patron['cat_password'], |
1461 | ] |
1462 | ); |
1463 | |
1464 | $count++; |
1465 | $items[$holdKey] = [ |
1466 | 'success' => true, |
1467 | 'status' => 'hold_cancel_success', |
1468 | ]; |
1469 | } catch (\Exception $e) { |
1470 | $items[$holdKey] = [ |
1471 | 'success' => false, |
1472 | 'status' => 'hold_cancel_fail', |
1473 | 'sysMessage' => $e->getMessage(), |
1474 | ]; |
1475 | } |
1476 | } |
1477 | $result = ['count' => $count, 'items' => $items]; |
1478 | return $result; |
1479 | } |
1480 | |
1481 | /** |
1482 | * Public Function which retrieves renew, hold and cancel settings from the |
1483 | * driver ini file. |
1484 | * |
1485 | * @param string $function The name of the feature to be checked |
1486 | * @param array $params Optional feature-specific parameters (array) |
1487 | * |
1488 | * @return array An array with key-value pairs. |
1489 | * |
1490 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1491 | */ |
1492 | public function getConfig($function, $params = []) |
1493 | { |
1494 | if (isset($this->config[$function])) { |
1495 | $functionConfig = $this->config[$function]; |
1496 | } else { |
1497 | $functionConfig = false; |
1498 | } |
1499 | return $functionConfig; |
1500 | } |
1501 | |
1502 | /** |
1503 | * Get Renew Details |
1504 | * |
1505 | * In order to renew an item, Symphony requires the patron details and an item |
1506 | * id. This function returns the item id as a string which is then used |
1507 | * as submitted form data in checkedOut.php. This value is then extracted by |
1508 | * the RenewMyItems function. |
1509 | * |
1510 | * @param array $checkOutDetails An array of item data |
1511 | * |
1512 | * @return string Data for use in a form field |
1513 | */ |
1514 | public function getRenewDetails($checkOutDetails) |
1515 | { |
1516 | $renewDetails = $checkOutDetails['barcode']; |
1517 | return $renewDetails; |
1518 | } |
1519 | |
1520 | /** |
1521 | * Renew My Items |
1522 | * |
1523 | * Function for attempting to renew a patron's items. The data in |
1524 | * $renewDetails['details'] is determined by getRenewDetails(). |
1525 | * |
1526 | * @param array $renewDetails An array of data required for renewing items |
1527 | * including the Patron ID and an array of renewal IDS |
1528 | * |
1529 | * @return array An array of renewal information keyed by item ID |
1530 | */ |
1531 | public function renewMyItems($renewDetails) |
1532 | { |
1533 | $details = []; |
1534 | $patron = $renewDetails['patron']; |
1535 | |
1536 | foreach ($renewDetails['details'] as $barcode) { |
1537 | try { |
1538 | $options = ['itemID' => $barcode]; |
1539 | |
1540 | $renewal = $this->makeRequest( |
1541 | 'patron', |
1542 | 'renewMyCheckout', |
1543 | $options, |
1544 | [ |
1545 | 'login' => $patron['cat_username'], |
1546 | 'password' => $patron['cat_password'], |
1547 | ] |
1548 | ); |
1549 | |
1550 | $details[$barcode] = [ |
1551 | 'success' => true, |
1552 | 'new_date' => date('j-M-y', strtotime($renewal->dueDate)), |
1553 | 'new_time' => date('g:i a', strtotime($renewal->dueDate)), |
1554 | 'item_id' => $renewal->itemID, |
1555 | 'sysMessage' => $renewal->message, |
1556 | ]; |
1557 | } catch (\Exception $e) { |
1558 | $details[$barcode] = [ |
1559 | 'success' => false, |
1560 | 'new_date' => false, |
1561 | 'new_time' => false, |
1562 | 'sysMessage' => |
1563 | 'We could not renew this item: ' . $e->getMessage(), |
1564 | ]; |
1565 | } |
1566 | } |
1567 | |
1568 | $result = ['details' => $details]; |
1569 | return $result; |
1570 | } |
1571 | |
1572 | /** |
1573 | * Place Hold |
1574 | * |
1575 | * Attempts to place a hold or recall on a particular item |
1576 | * |
1577 | * @param array $holdDetails An array of item and patron data |
1578 | * |
1579 | * @return array An array of data on the request including |
1580 | * whether or not it was successful and a system message (if available) |
1581 | */ |
1582 | public function placeHold($holdDetails) |
1583 | { |
1584 | try { |
1585 | $options = []; |
1586 | $patron = $holdDetails['patron']; |
1587 | |
1588 | if ($holdDetails['item_id'] != null) { |
1589 | $options['itemID'] = $holdDetails['item_id']; |
1590 | } |
1591 | |
1592 | if ($holdDetails['id'] != null) { |
1593 | $options['titleKey'] = $holdDetails['id']; |
1594 | } |
1595 | |
1596 | if ($holdDetails['pickUpLocation'] != null) { |
1597 | $options['pickupLibraryID'] = $holdDetails['pickUpLocation']; |
1598 | } |
1599 | |
1600 | if ($holdDetails['requiredBy'] != null) { |
1601 | $options['expiresDate'] = $holdDetails['requiredBy']; |
1602 | } |
1603 | |
1604 | if ($holdDetails['comment'] != null) { |
1605 | $options['comment'] = $holdDetails['comment']; |
1606 | } |
1607 | |
1608 | $this->makeRequest( |
1609 | 'patron', |
1610 | 'createMyHold', |
1611 | $options, |
1612 | [ |
1613 | 'login' => $patron['cat_username'], |
1614 | 'password' => $patron['cat_password'], |
1615 | ] |
1616 | ); |
1617 | |
1618 | $result = [ |
1619 | 'success' => true, |
1620 | 'sysMessage' => 'Your hold has been placed.', |
1621 | ]; |
1622 | return $result; |
1623 | } catch (SoapFault $e) { |
1624 | $result = [ |
1625 | 'success' => false, |
1626 | 'sysMessage' => |
1627 | 'We could not place the hold: ' . $e->getMessage(), |
1628 | ]; |
1629 | return $result; |
1630 | } |
1631 | } |
1632 | |
1633 | /** |
1634 | * Get Policy List |
1635 | * |
1636 | * Protected support method for getting a list of policies. |
1637 | * |
1638 | * @param string $policyType Symphony policy code for type of policy |
1639 | * |
1640 | * @return array An associative array of policy codes and descriptions. |
1641 | */ |
1642 | protected function getPolicyList($policyType) |
1643 | { |
1644 | try { |
1645 | $cacheKey = 'symphony' . hash('sha256', "{$policyType}"); |
1646 | |
1647 | if (isset($this->policies[$policyType])) { |
1648 | return $this->policies[$policyType]; |
1649 | } elseif ( |
1650 | $this->policyCache |
1651 | && ($policyList = $this->policyCache->getItem($cacheKey)) |
1652 | ) { |
1653 | $this->policies[$policyType] = $policyList; |
1654 | return $policyList; |
1655 | } else { |
1656 | $policyList = []; |
1657 | $options = ['policyType' => $policyType]; |
1658 | $policies = $this->makeRequest( |
1659 | 'admin', |
1660 | 'lookupPolicyList', |
1661 | $options |
1662 | ); |
1663 | |
1664 | foreach ($policies->policyInfo as $policyInfo) { |
1665 | $policyList[$policyInfo->policyID] |
1666 | = $policyInfo->policyDescription; |
1667 | } |
1668 | |
1669 | if ($this->policyCache) { |
1670 | $this->policyCache->setItem($cacheKey, $policyList); |
1671 | } |
1672 | |
1673 | return $policyList; |
1674 | } |
1675 | } catch (\Exception $e) { |
1676 | return []; |
1677 | } |
1678 | } |
1679 | |
1680 | /** |
1681 | * Get Pick Up Locations |
1682 | * |
1683 | * This is responsible get a list of valid library locations for holds / recall |
1684 | * retrieval |
1685 | * |
1686 | * @param array $patron Patron information returned by the patronLogin |
1687 | * method. |
1688 | * @param array $holdDetails Optional array, only passed in when getting a list |
1689 | * in the context of placing or editing a hold. When placing a hold, it contains |
1690 | * most of the same values passed to placeHold, minus the patron data. When |
1691 | * editing a hold it contains all the hold information returned by getMyHolds. |
1692 | * May be used to limit the pickup options or may be ignored. The driver must |
1693 | * not add new options to the return array based on this data or other areas of |
1694 | * VuFind may behave incorrectly. |
1695 | * |
1696 | * @return array An array of associative arrays with locationID and |
1697 | * locationDisplay keys |
1698 | * |
1699 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1700 | */ |
1701 | public function getPickUpLocations($patron = false, $holdDetails = null) |
1702 | { |
1703 | $libraries = []; |
1704 | |
1705 | foreach ($this->getPolicyList('LIBR') as $key => $library) { |
1706 | $libraries[] = [ |
1707 | 'locationID' => $key, |
1708 | 'locationDisplay' => $library, |
1709 | ]; |
1710 | } |
1711 | |
1712 | return $libraries; |
1713 | } |
1714 | |
1715 | /** |
1716 | * Get Default Pick Up Location |
1717 | * |
1718 | * Returns the default pick up location set in Symphony.ini |
1719 | * |
1720 | * @param array $patron Patron information returned by the patronLogin |
1721 | * method. |
1722 | * @param array $holdDetails Optional array, only passed in when getting a list |
1723 | * in the context of placing a hold; contains most of the same values passed to |
1724 | * placeHold, minus the patron data. May be used to limit the pickup options |
1725 | * or may be ignored. |
1726 | * |
1727 | * @return string The default pickup location for the patron. |
1728 | * |
1729 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
1730 | */ |
1731 | public function getDefaultPickUpLocation($patron = false, $holdDetails = null) |
1732 | { |
1733 | if (isset($patron['library'])) { |
1734 | // Check for library in patron info |
1735 | return $patron['library']; |
1736 | } elseif (isset($this->config['Holds']['defaultPickUpLocation'])) { |
1737 | // If no library returned in patron info, check config file |
1738 | return $this->config['Holds']['defaultPickUpLocation']; |
1739 | } else { |
1740 | // Default to first library in the list if none specified |
1741 | // in patron info or config file |
1742 | $libraries = $this->getPickUpLocations(); |
1743 | return $libraries[0]['locationID']; |
1744 | } |
1745 | } |
1746 | } |