Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
1.47% |
2 / 136 |
|
10.00% |
1 / 10 |
CRAP | |
0.00% |
0 / 1 |
Innovative | |
1.47% |
2 / 136 |
|
10.00% |
1 / 10 |
1011.48 | |
0.00% |
0 / 1 |
init | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
sendRequest | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
prepID | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getStatus | |
0.00% |
0 / 60 |
|
0.00% |
0 / 1 |
156 | |||
getStatuses | |
0.00% |
0 / 6 |
|
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 | |||
getHoldLink | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getMyProfile | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
patronLogin | |
0.00% |
0 / 50 |
|
0.00% |
0 / 1 |
42 |
1 | <?php |
2 | |
3 | /** |
4 | * III 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 Adam Brin <abrin@brynmawr.com> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki |
28 | */ |
29 | |
30 | namespace VuFind\ILS\Driver; |
31 | |
32 | use VuFind\Date\DateException; |
33 | use VuFind\Exception\ILS as ILSException; |
34 | |
35 | use function count; |
36 | use function strlen; |
37 | |
38 | /** |
39 | * VuFind Connector for Innovative |
40 | * |
41 | * This class uses screen scraping techniques to gather record holdings written |
42 | * by Adam Bryn of the Tri-College consortium. |
43 | * |
44 | * @category VuFind |
45 | * @package ILS_Drivers |
46 | * @author Adam Brin <abrin@brynmawr.com> |
47 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
48 | * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki |
49 | */ |
50 | class Innovative extends AbstractBase implements |
51 | \VuFindHttp\HttpServiceAwareInterface |
52 | { |
53 | use \VuFindHttp\HttpServiceAwareTrait; |
54 | |
55 | /** |
56 | * Initialize the driver. |
57 | * |
58 | * Validate configuration and perform all resource-intensive tasks needed to |
59 | * make the driver active. |
60 | * |
61 | * @throws ILSException |
62 | * @return void |
63 | */ |
64 | public function init() |
65 | { |
66 | if (empty($this->config)) { |
67 | throw new ILSException('Configuration needs to be set.'); |
68 | } |
69 | } |
70 | |
71 | /** |
72 | * Make an HTTP request |
73 | * |
74 | * @param string $url URL to request |
75 | * |
76 | * @return string |
77 | */ |
78 | protected function sendRequest($url) |
79 | { |
80 | // Make the NCIP request: |
81 | try { |
82 | $result = $this->httpService->get($url); |
83 | } catch (\Exception $e) { |
84 | $this->throwAsIlsException($e); |
85 | } |
86 | |
87 | if (!$result->isSuccess()) { |
88 | throw new ILSException('HTTP error'); |
89 | } |
90 | |
91 | return $result->getBody(); |
92 | } |
93 | |
94 | /** |
95 | * Prepare ID |
96 | * |
97 | * This function returns the correct record id format as defined |
98 | * in the Innovative.ini file. |
99 | * |
100 | * @param string $id ID to format |
101 | * |
102 | * @return string |
103 | */ |
104 | protected function prepID($id) |
105 | { |
106 | // Get the ID format from config (default to use_full_id if unset): |
107 | if ( |
108 | !isset($this->config['RecordID']['use_full_id']) |
109 | || $this->config['RecordID']['use_full_id'] |
110 | ) { |
111 | // Strip ID leading period and trailing check digit. |
112 | $id_ = substr(str_replace('.b', '', $id), 0, -1); |
113 | } else { |
114 | // Return digits only. |
115 | $id_ = substr($id, 1); |
116 | } |
117 | return $id_; |
118 | } |
119 | |
120 | /** |
121 | * Get Status |
122 | * |
123 | * This is responsible for retrieving the status information of a certain |
124 | * record. |
125 | * |
126 | * @param string $id The record id to retrieve the holdings for |
127 | * |
128 | * @throws ILSException |
129 | * @return mixed On success, an associative array with the following keys: |
130 | * id, availability (boolean), status, location, reserve, callnumber. |
131 | */ |
132 | public function getStatus($id) |
133 | { |
134 | // Strip ID |
135 | $id_ = $this->prepID($id); |
136 | |
137 | // Load Record Page |
138 | $host = rtrim($this->config['Catalog']['url'], '/'); |
139 | |
140 | // Grab the full item list view |
141 | //$result = $this->sendRequest($host . '/record=b' . $id_); |
142 | $result = $this->sendRequest( |
143 | $host . '/search/.b' . $id_ . '/.b' . $id_ . |
144 | '/1%2C1%2C1%2CB/holdings~' . $id_ . '&FF=&1%2C0%2C' |
145 | ); |
146 | |
147 | // strip out html before the first occurrence of 'bibItems', should be |
148 | // '<table class="bibItems" ' |
149 | $r = substr($result, stripos($result, 'bibItems')); |
150 | // strip out the rest of the first table tag. |
151 | $r = substr($r, strpos($r, '>') + 1); |
152 | // strip out the next table closing tag and everything after it. |
153 | $r = substr($r, 0, stripos($r, '</table')); |
154 | |
155 | // $r should only include the holdings table at this point |
156 | |
157 | // split up into strings that contain each table row, excluding the |
158 | // beginning tr tag. |
159 | $rows = preg_split('/<tr([^>]*)>/', $r); |
160 | $count = 0; |
161 | $keys = array_pad([], 10, ''); |
162 | |
163 | $loc_col_name = $this->config['OPAC']['location_column']; |
164 | $call_col_name = $this->config['OPAC']['call_no_column']; |
165 | $status_col_name = $this->config['OPAC']['status_column']; |
166 | $reserves_col_name = $this->config['OPAC']['location_column']; |
167 | $reserves_key_name = $this->config['OPAC']['reserves_key_name']; |
168 | $stat_avail = $this->config['OPAC']['status_avail']; |
169 | $stat_due = $this->config['OPAC']['status_due']; |
170 | |
171 | $ret = []; |
172 | foreach ($rows as $row) { |
173 | // Split up the contents of the row based on the th or td tag, excluding |
174 | // the tags themselves. |
175 | $cols = preg_split('/<t(h|d)([^>]*)>/', $row); |
176 | |
177 | // for each th or td section, do the following. |
178 | for ($i = 0; $i < count($cols); $i++) { |
179 | // replace non blocking space encodings with a space. |
180 | $cols[$i] = str_replace(' ', ' ', $cols[$i]); |
181 | // remove html comment tags |
182 | $cols[$i] = preg_replace('/<!--([^(-->)]*)-->/', '', $cols[$i]); |
183 | // Remove closing th or td tag, trim whitespace and decode html |
184 | // entities |
185 | $cols[$i] = html_entity_decode( |
186 | trim(substr($cols[$i], 0, stripos($cols[$i], '</t'))) |
187 | ); |
188 | |
189 | // If this is the first row, it is the header row and has the column |
190 | // names |
191 | if ($count == 1) { |
192 | $keys[$i] = $cols[$i]; |
193 | } elseif ($count > 1) { // not the first row, has holding info |
194 | //look for location column |
195 | if (stripos($keys[$i], (string)$loc_col_name) > -1) { |
196 | $ret[$count - 2]['location'] = strip_tags($cols[$i]); |
197 | } |
198 | // Does column hold reserves information? |
199 | if (stripos($keys[$i], (string)$reserves_col_name) > -1) { |
200 | if (stripos($cols[$i], (string)$reserves_key_name) > -1) { |
201 | $ret[$count - 2]['reserve'] = 'Y'; |
202 | } else { |
203 | $ret[$count - 2]['reserve'] = 'N'; |
204 | } |
205 | } |
206 | // Does column hold call numbers? |
207 | if (stripos($keys[$i], (string)$call_col_name) > -1) { |
208 | $ret[$count - 2]['callnumber'] = strip_tags($cols[$i]); |
209 | } |
210 | // Look for status information. |
211 | if (stripos($keys[$i], (string)$status_col_name) > -1) { |
212 | if (stripos($cols[$i], (string)$stat_avail) > -1) { |
213 | $ret[$count - 2]['status'] = 'Available On Shelf'; |
214 | $ret[$count - 2]['availability'] = 1; |
215 | } else { |
216 | $ret[$count - 2]['status'] = 'Available to request'; |
217 | $ret[$count - 2]['availability'] = 0; |
218 | } |
219 | if (stripos($cols[$i], (string)$stat_due) > -1) { |
220 | $t = trim( |
221 | substr( |
222 | $cols[$i], |
223 | stripos($cols[$i], (string)$stat_due) |
224 | + strlen($stat_due) |
225 | ) |
226 | ); |
227 | $t = substr($t, 0, stripos($t, ' ')); |
228 | $ret[$count - 2]['duedate'] = $t; |
229 | } |
230 | } |
231 | //$ret[$count-2][$keys[$i]] = $cols[$i]; |
232 | //$ret[$count-2]['id'] = $bibid; |
233 | $ret[$count - 2]['id'] = $id; |
234 | $ret[$count - 2]['number'] = ($count - 1); |
235 | // Return a fake barcode so hold link is enabled |
236 | // TODO: Should be dependent on settings variable, if bib level |
237 | // holds. |
238 | $ret[$count - 2]['barcode'] = '1234567890123'; |
239 | } |
240 | } |
241 | $count++; |
242 | } |
243 | return $ret; |
244 | } |
245 | |
246 | /** |
247 | * Get Statuses |
248 | * |
249 | * This is responsible for retrieving the status information for a |
250 | * collection of records. |
251 | * |
252 | * @param array $ids The array of record ids to retrieve the status for |
253 | * |
254 | * @throws ILSException |
255 | * @return array An array of getStatus() return values on success. |
256 | */ |
257 | public function getStatuses($ids) |
258 | { |
259 | $items = []; |
260 | $count = 0; |
261 | foreach ($ids as $id) { |
262 | $items[$count] = $this->getStatus($id); |
263 | $count++; |
264 | } |
265 | return $items; |
266 | } |
267 | |
268 | /** |
269 | * Get Holding |
270 | * |
271 | * This is responsible for retrieving the holding information of a certain |
272 | * record. |
273 | * |
274 | * @param string $id The record id to retrieve the holdings for |
275 | * @param array $patron Patron data |
276 | * @param array $options Extra options (not currently used) |
277 | * |
278 | * @throws DateException |
279 | * @throws ILSException |
280 | * @return array On success, an associative array with the following |
281 | * keys: id, availability (boolean), status, location, reserve, callnumber, |
282 | * duedate, number, barcode. |
283 | * |
284 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
285 | */ |
286 | public function getHolding($id, array $patron = null, array $options = []) |
287 | { |
288 | return $this->getStatus($id); |
289 | } |
290 | |
291 | /** |
292 | * Get Purchase History |
293 | * |
294 | * This is responsible for retrieving the acquisitions history data for the |
295 | * specific record (usually recently received issues of a serial). |
296 | * |
297 | * @param string $id The record id to retrieve the info for |
298 | * |
299 | * @throws ILSException |
300 | * @return array An array with the acquisitions data on success. |
301 | * |
302 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
303 | */ |
304 | public function getPurchaseHistory($id) |
305 | { |
306 | // TODO |
307 | return []; |
308 | } |
309 | |
310 | /** |
311 | * Get Hold Link |
312 | * |
313 | * The goal for this method is to return a URL to a "place hold" web page on |
314 | * the ILS OPAC. This is used for ILSs that do not support an API or method |
315 | * to place Holds. |
316 | * |
317 | * @param string $id The id of the bib record |
318 | * @param array $details Item details from getHoldings return array |
319 | * |
320 | * @return string URL to ILS's OPAC's place hold screen. |
321 | * |
322 | * @SuppressWarnings(PHPMD.UnusedFormalParameter) |
323 | */ |
324 | public function getHoldLink($id, $details) |
325 | { |
326 | // Strip ID |
327 | $id_ = $this->prepID($id); |
328 | |
329 | //Build request link |
330 | $link = $this->config['Catalog']['url'] . '/search?/.b' . $id_ . '/.b' . |
331 | $id_ . '/1%2C1%2C1%2CB/request~b' . $id_; |
332 | //$link = $this->config['Catalog']['url'] . '/record=b' . $id_; |
333 | |
334 | return $link; |
335 | } |
336 | |
337 | /** |
338 | * Get Patron Profile |
339 | * |
340 | * This is responsible for retrieving the profile for a specific patron. |
341 | * |
342 | * @param array $userinfo The patron array |
343 | * |
344 | * @throws ILSException |
345 | * @return array Array of the patron's profile data on success. |
346 | */ |
347 | public function getMyProfile($userinfo) |
348 | { |
349 | return $userinfo; |
350 | } |
351 | |
352 | /** |
353 | * Patron Login |
354 | * |
355 | * This is responsible for authenticating a patron against the catalog. |
356 | * |
357 | * @param string $username The patron username |
358 | * @param string $password The patron's password |
359 | * |
360 | * @throws ILSException |
361 | * @return mixed Associative array of patron info on successful login, |
362 | * null on unsuccessful login. |
363 | */ |
364 | public function patronLogin($username, $password) |
365 | { |
366 | // TODO: if username is a barcode, test to make sure it fits proper format |
367 | $enabled = $this->config['PATRONAPI']['enabled'] ?? false; |
368 | if ($enabled && strtolower($enabled) !== 'false') { |
369 | // use patronAPI to authenticate customer |
370 | $url = $this->config['PATRONAPI']['url']; |
371 | |
372 | // build patronapi pin test request |
373 | $result = $this->sendRequest( |
374 | $url . urlencode($username) . '/' . urlencode($password) . |
375 | '/pintest' |
376 | ); |
377 | |
378 | // search for successful response of "RETCOD=0" |
379 | if (stripos($result, 'RETCOD=0') == -1) { |
380 | // pin did not match, can look up specific error to return |
381 | // more useful info. |
382 | return null; |
383 | } |
384 | |
385 | // Pin did match, get patron information |
386 | $result = $this->sendRequest($url . urlencode($username) . '/dump'); |
387 | |
388 | // The following is taken and modified from patronapi.php by John Blyberg |
389 | // released under the GPL |
390 | $api_contents = trim(strip_tags($result)); |
391 | $api_array_lines = explode("\n", $api_contents); |
392 | $api_data = ['PBARCODE' => false]; |
393 | |
394 | foreach ($api_array_lines as $api_line) { |
395 | $api_line = str_replace('p=', 'peq', $api_line); |
396 | $api_line_arr = explode('=', $api_line); |
397 | $regex_match = ["/\[(.*?)\]/","/\s/",'/#/']; |
398 | $regex_replace = ['','','NUM']; |
399 | $key = trim( |
400 | preg_replace($regex_match, $regex_replace, $api_line_arr[0]) |
401 | ); |
402 | $api_data[$key] = trim($api_line_arr[1]); |
403 | } |
404 | |
405 | if (!$api_data['PBARCODE']) { |
406 | // No barcode found, can look up specific error to return more |
407 | // useful info. This check needs to be modified to handle using |
408 | // III patron ids also. |
409 | return null; |
410 | } |
411 | |
412 | // return patron info |
413 | $ret = []; |
414 | $ret['id'] = $api_data['PBARCODE']; // or should I return patron id num? |
415 | $names = explode(',', $api_data['PATRNNAME']); |
416 | $ret['firstname'] = $names[1]; |
417 | $ret['lastname'] = $names[0]; |
418 | $ret['cat_username'] = urlencode($username); |
419 | $ret['cat_password'] = urlencode($password); |
420 | $ret['email'] = $api_data['EMAILADDR']; |
421 | $ret['major'] = null; |
422 | $ret['college'] = $api_data['HOMELIBR']; |
423 | $ret['homelib'] = $api_data['HOMELIBR']; |
424 | // replace $ separator in III addresses with newline |
425 | $ret['address1'] = str_replace('$', ', ', $api_data['ADDRESS']); |
426 | $ret['address2'] = str_replace('$', ', ', $api_data['ADDRESS2']); |
427 | preg_match( |
428 | '/([0-9]{5}|[0-9]{5}-[0-9]{4})[ ]*$/', |
429 | $api_data['ADDRESS'], |
430 | $zipmatch |
431 | ); |
432 | $ret['zip'] = $zipmatch[1]; //retrieve from address |
433 | $ret['phone'] = $api_data['TELEPHONE']; |
434 | $ret['phone2'] = $api_data['TELEPHONE2']; |
435 | // Should probably have a translation table for patron type |
436 | $ret['group'] = $api_data['PTYPE']; |
437 | $ret['expiration'] = $api_data['EXPDATE']; |
438 | // Only if agency module is enabled. |
439 | $ret['region'] = $api_data['AGENCY']; |
440 | return $ret; |
441 | } else { |
442 | // TODO: use screen scrape |
443 | return null; |
444 | } |
445 | } |
446 | } |