diff --git a/config/vufind/XCNCIP2.ini b/config/vufind/XCNCIP2.ini index 20e177adab7ae078f76a4d499c4a53a8ede48a31..76be7e34c7e0a19adf82f32dbb85d604325efe35 100644 --- a/config/vufind/XCNCIP2.ini +++ b/config/vufind/XCNCIP2.ini @@ -1,5 +1,34 @@ [Catalog] ; Base URL for the XC NCIP Toolkit's NCIP responder: url = http://myuniversity.edu:8080/ncipv2/NCIPResponder + ; Your library's Agency ID (ILSDefaultAgency setting in driver_config.properties): -agency = "My University" \ No newline at end of file +agency = "My University" + +; Pickup location definitions: CSV file +; +; Format: [agency],[locationID],[locationDisplay] +; +; e.g., +; (for consortium=false) +; My University,1,Main Circulation Desk +; My University,2,Stacks +; +; e.g., +; (for consortium=true) +; Agency1,1,Agency1 - Main Circulation Desk +; Agency1,2,Agency1 - Stacks +; Agency2,11,Agency2 - Main Circulation Desk +; Agency2,12,Agency2 - Stacks +pickupLocationsFile = "XCNCIP2_locations.txt" + +;----------------------------------------------------------------- +; Consortium settings below: +;----------------------------------------------------------------- + +; Is this a consortium? +consortium = false + +; If consortium is true, list all valid agencies +;agency[] = "Agency1" +;agency[] = "Agency2" diff --git a/config/vufind/XCNCIP2_locations.txt b/config/vufind/XCNCIP2_locations.txt new file mode 100644 index 0000000000000000000000000000000000000000..7c1b6805ea5d993f8c05cc4407afb30a7a2656fd --- /dev/null +++ b/config/vufind/XCNCIP2_locations.txt @@ -0,0 +1,2 @@ +My University,1,Main Circulation Desk +My University,2,Stacks diff --git a/module/VuFind/src/VuFind/ILS/Driver/XCNCIP2.php b/module/VuFind/src/VuFind/ILS/Driver/XCNCIP2.php index 01c9864dee6c0e88983683531900ad2908a0d279..a3e4278ecd16f8d0400a7248eeaca470fc6c1340 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/XCNCIP2.php +++ b/module/VuFind/src/VuFind/ILS/Driver/XCNCIP2.php @@ -26,7 +26,8 @@ * @link http://vufind.org/wiki/vufind2:building_an_ils_driver Wiki */ namespace VuFind\ILS\Driver; -use VuFind\Exception\ILS as ILSException; +use VuFind\Exception\ILS as ILSException, + VuFind\Config\Locator as ConfigLocator; /** * XC NCIP Toolkit (v2) ILS Driver @@ -46,6 +47,34 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf */ protected $httpService = null; + /** + * Is this a consortium? Default: false + * + * @var boolean + */ + protected $consortium = false; + + /** + * Agency definitions (consortial) - Array list of consortium members + * + * @var array + */ + protected $agency = array(); + + /** + * NCIP server URL + * + * @var string + */ + protected $url; + + /** + * Pickup locations + * + * @var array + */ + protected $pickupLocations = array(); + /** * Set the HTTP service to be used for HTTP requests. * @@ -72,6 +101,50 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf if (empty($this->config)) { throw new ILSException('Configuration needs to be set.'); } + + $this->url = $this->config['Catalog']['url']; + if ($this->config['Catalog']['consortium']) { + $this->consortium = true; + foreach ($this->config['Catalog']['agency'] as $agency) { + $this->agency[$agency] = 1; + } + } else { + $this->consortium = false; + if (is_array($this->config['Catalog']['agency'])) { + $this->agency[$this->config['Catalog']['agency'][0]] = 1; + } else { + $this->agency[$this->config['Catalog']['agency']] = 1; + } + } + + $this->loadPickupLocations($this->config['Catalog']['pickupLocationsFile']); + } + + /** + * Load pickup locations + * + * Loads pickup location information from configuration file. + * + * @throws ILSException + * @return void + */ + public function loadPickupLocations($filename) + { + // Load pickup locations file: + $pickupLocationsFile = ConfigLocator::getConfigPath($filename, 'config/vufind'); + if (!file_exists($pickupLocationsFile)) { + throw new ILSException("Cannot load pickup locations file: {$pickupLocationsFile}."); + } + if (($handle = fopen($pickupLocationsFile, "r")) !== FALSE) { + while (($data = fgetcsv($handle)) !== FALSE) { + $this->pickupLocations[$data[0]][] = + array( + 'locationID' => $data[1], + 'locationDisplay' => $data[2] + ); + } + fclose($handle); + } } /** @@ -86,7 +159,11 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf // Make the NCIP request: try { $client = $this->httpService - ->createClient($this->config['Catalog']['url']); + ->createClient($this->url); + // Set timeout value + $timeout = isset($this->config['Catalog']['http_timeout']) + ? $this->config['Catalog']['http_timeout'] : 30; + $client->setOptions(array('timeout' => $timeout)); $client->setRawBody($xml); $client->setEncType('application/xml; "charset=utf-8"'); $result = $client->setMethod('POST')->send(); @@ -109,68 +186,136 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf } } + + /** + * Given a chunk of the availability response, extract the values needed + * by VuFind. + * + * @param $current Current LUIS holding chunk. + * + * @return array of status information for this holding + */ + protected function getStatusForChunk($current) + { + $status = $current->xpath( + 'ns1:ItemOptionalFields/ns1:CirculationStatus' + ); + $status = empty($status) ? '' : (string)$status[0]; + + $itemId = $current->xpath( + + 'ns1:ItemId/ns1:ItemIdentifierValue' + ); + $item_id = (string)$itemId[0]; + + $itemCallNo = $current->xpath( + + 'ns1:ItemOptionalFields/ns1:ItemDescription/ns1:CallNumber' + ); + $itemCallNo = (string)$itemCallNo[0]; + + $location = $current->xpath( + 'ns1:ItemOptionalFields/ns1:Location/ns1:LocationName/' . + 'ns1:LocationNameInstance/ns1:LocationNameValue' + ); + $location = (string)$location[0]; + + return array( + //'id' => ... + 'status' => $status, + 'location' => $location, + 'callnumber' => $itemCallNo, + 'availability' => ($status == "Not Charged"), + 'reserve' => 'N', // not supported + ); + } + /** * Given a chunk of the availability response, extract the values needed * by VuFind. * * @param array $current Current XCItemAvailability chunk. + * @param string $aggregate_id (Aggregate) ID of the consortial record + * @param string $bib_id Bib ID of one of the consortial record's source record(s) * * @return array */ - protected function getHoldingsForChunk($current) + protected function getHoldingsForChunk($current, $aggregate_id = null, $bib_id = null) { // Maintain an internal static count of line numbers: static $number = 1; + $current->registerXPathNamespace('ns1', 'http://www.niso.org/2008/ncip'); + // Extract details from the XML: $status = $current->xpath( - 'ns1:HoldingsSet/ns1:ItemInformation/' . 'ns1:ItemOptionalFields/ns1:CirculationStatus' ); $status = empty($status) ? '' : (string)$status[0]; - $id = $current->xpath( - 'ns1:BibliographicId/ns1:BibliographicItemId/' . - 'ns1:BibliographicItemIdentifier' - ); + $itemId = $current->xpath('ns1:ItemId/ns1:ItemIdentifierValue'); + + $itemAgencyId = $current->xpath('ns1:ItemId/ns1:AgencyId'); // Pick out the permanent location (TODO: better smarts for dealing with // temporary locations and multi-level location names): - $locationNodes = $current->xpath('ns1:HoldingsSet/ns1:Location'); - $location = ''; - foreach ($locationNodes as $curLoc) { - $type = $curLoc->xpath('ns1:LocationType'); - if ((string)$type[0] == 'Permanent') { - $tmp = $curLoc->xpath( - 'ns1:LocationName/ns1:LocationNameInstance/ns1:LocationNameValue' - ); - $location = (string)$tmp[0]; - } - } +// $locationNodes = $current->xpath('ns1:HoldingsSet/ns1:Location'); +// $location = ''; +// foreach ($locationNodes as $curLoc) { +// $type = $curLoc->xpath('ns1:LocationType'); +// if ((string)$type[0] == 'Permanent') { +// $tmp = $curLoc->xpath( +// 'ns1:LocationName/ns1:LocationNameInstance/ns1:LocationNameValue' +// ); +// $location = (string)$tmp[0]; +// } +// } + + $tmp = $current->xpath('ns1:ItemOptionalFields/ns1:Location/' . + 'ns1:LocationName/ns1:LocationNameInstance/ns1:LocationNameValue' + ); + $location = (string)$tmp[0]; - // Get both holdings and item level call numbers; we'll pick the most - // specific available value below. - $holdCallNo = $current->xpath('ns1:HoldingsSet/ns1:CallNumber'); - $holdCallNo = (string)$holdCallNo[0]; $itemCallNo = $current->xpath( - 'ns1:HoldingsSet/ns1:ItemInformation/' . 'ns1:ItemOptionalFields/ns1:ItemDescription/ns1:CallNumber' ); $itemCallNo = (string)$itemCallNo[0]; + + $volume = $current->xpath( + 'ns1:ItemOptionalFields/ns1:ItemDescription/' . + 'ns1:HoldingsInformation/ns1:UnstructuredHoldingsData' + ); + $volume = (string)$volume[0]; + + if ($status === "Not Charged") { + $holdType = "hold"; + } else { + $holdType = "recall"; + } // Build return array: return array( - 'id' => empty($id) ? '' : (string)$id[0], + 'id' => empty($aggregate_id) ? (empty($bib_id) ? '' : $bib_id) : $aggregate_id, 'availability' => ($status == 'Not Charged'), 'status' => $status, + 'item_id' => (string)$itemId[0], + 'bib_id' => $bib_id, + 'item_agency_id' => (string)$itemAgencyId[0], + 'aggregate_id' => $aggregate_id, 'location' => $location, 'reserve' => 'N', // not supported - 'callnumber' => empty($itemCallNo) ? $holdCallNo : $itemCallNo, + 'callnumber' => $itemCallNo, 'duedate' => '', // not supported - 'number' => $number++, + //'number' => $number++, + 'number' => $volume, // XC NCIP does not support barcode, but we need a placeholder here // to display anything on the record screen: - 'barcode' => 'placeholder' . $number + 'barcode' => 'placeholder' . $number, + 'is_holdable' => true, + 'addLink' => true, + 'holdtype' => $holdType, + 'storageRetrievalRequest' => 'auto', + 'addStorageRetrievalRequestLink' => 'true', ); } @@ -201,8 +346,17 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf * * @return string XML request */ - protected function getStatusRequest($idList, $resumption = null) + protected function getStatusRequest($idList, $resumption = null, $agency = null) { + // cedelis: + //if (is_null($agency)) $agency = $this->agency[0]; + + // pzurek: + //if (is_null($agency)) $agency = array_keys($this->agency)[0]; + // The above does not work on older versions of php + $keys = array_keys($this->agency); + if (is_null($agency)) $agency = $keys[0]; + // Build a list of the types of information we want to retrieve: $desiredParts = array( 'Bibliographic Description', @@ -223,12 +377,14 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf // Add the ID list: foreach ($idList as $id) { $xml .= '<ns1:BibliographicId>' . - '<ns1:BibliographicItemId>' . - '<ns1:BibliographicItemIdentifier>' . + '<ns1:BibliographicRecordId>' . + '<ns1:BibliographicRecordIdentifier>' . htmlspecialchars($id) . - '</ns1:BibliographicItemIdentifier>' . - '<ns1:AgencyId>LOCAL</ns1:AgencyId>' . - '</ns1:BibliographicItemId>' . + '</ns1:BibliographicRecordIdentifier>' . + '<ns1:AgencyId>' . + htmlspecialchars($agency) . + '</ns1:AgencyId>' . + '</ns1:BibliographicRecordId>' . '</ns1:BibliographicId>'; } @@ -265,6 +421,11 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf public function getStatuses($idList) { $status = array(); + + if ($this->consortium) { + return $status; // (empty) TODO: add support for consortial statuses. + } + $resumption = null; do { $request = $this->getStatusRequest($idList, $resumption); @@ -275,17 +436,38 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf // Build the array of statuses: foreach ($avail as $current) { - // Get data on the current chunk of data: - $chunk = $this->getHoldingsForChunk($current); - - // Each bibliographic ID has its own key in the $status array; make - // sure we initialize new arrays when necessary and then add the - // current chunk to the right place: - $id = $chunk['id']; - if (!isset($status[$id])) { - $status[$id] = array(); + $bib_id = $current->xpath( + 'ns1:BibliographicId/ns1:BibliographicRecordId/' . + 'ns1:BibliographicRecordIdentifier' + ); + $bib_id = (string)$bib_id[0]; + + $holdings = $current->xpath('ns1:HoldingsSet'); + + foreach ($holdings as $current) { + + $holdCallNo = $current->xpath('ns1:CallNumber'); + $holdCallNo = (string)$holdCallNo[0]; + + $items = $current->xpath('ns1:ItemInformation'); + + foreach ($items as $item) { + // Get data on the current chunk of data: + $chunk = $this->getStatusForChunk($item); + + $chunk['callnumber'] = empty($chunk['callnumber']) ? + $holdCallNo : $chunk['callnumber']; + + // Each bibliographic ID has its own key in the $status array; make + // sure we initialize new arrays when necessary and then add the + // current chunk to the right place: + $chunk['id'] = $bib_id; + if (!isset($status[$id])) { + $status[$id] = array(); + } + $status[$bib_id][] = $chunk; + } } - $status[$id][] = $chunk; } // Check for resumption token: @@ -296,6 +478,72 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf } while (!empty($resumption)); return $status; } + + /** + * Get Consortial Holding + * + * This is responsible for retrieving the holding information of a certain + * consortial record. + * + * @param string $id The record id to retrieve the holdings for + * @param array $ids The (consortial) source records for the record id + * @param array $patron Patron data + * + * @throws \VuFind\Exception\Date + * @throws ILSException + * @return array On success, an associative array with the following + * keys: id, availability (boolean), status, location, reserve, callnumber, + * duedate, number, barcode. + */ + public function getConsortialHoldings($id, array $patron = null, array $ids = null) + { + $aggregate_id = $id; + + $item_agency_id = array(); + if (! is_null($ids)) { + foreach ($ids as $_id) { + // Need to parse out the 035$a format, e.g., "(Agency) 123" + if (preg_match('/\(([^\)]+)\)\s*([0-9]+)/', $_id, $matches)) { + $matched_agency = $matches[1]; + $matched_id = $matches[2]; + if ($this->agency[$matched_agency]) { + $item_agency_id[$matched_agency] = $matched_id; + } + } + } + } + + $holdings = array(); + foreach ($item_agency_id as $_agency => $_id) { + $request = $this->getStatusRequest(array($_id), null, $_agency); + $response = $this->sendRequest($request); + + $bib_id = $response->xpath( + 'ns1:Ext/ns1:LookupItemSetResponse/ns1:BibInformation/' . + 'ns1:BibliographicId/ns1:BibliographicRecordId/' . + 'ns1:BibliographicRecordIdentifier' + ); + + $holdingSets = $response->xpath('//ns1:HoldingsSet'); + + foreach ($holdingSets as $holding) { + $holdCallNo = $holding->xpath('ns1:CallNumber'); + $holdCallNo = (string)$holdCallNo[0]; + $avail = $holding->xpath('ns1:ItemInformation'); + + // Build the array of holdings: + foreach ($avail as $current) { + $chunk = $this->getHoldingsForChunk($current, $aggregate_id, (string)$bib_id[0]); + $chunk['callnumber'] = empty($chunk['callnumber']) ? + $holdCallNo : $chunk['callnumber']; + $holdings[] = $chunk; + } + } + + } + + return $holdings; + } /** * Get Holding @@ -314,20 +562,21 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf */ public function getHolding($id, array $patron = null) { - $request = $this->getStatusRequest(array($id)); - $response = $this->sendRequest($request); - $avail = $response->xpath( - 'ns1:Ext/ns1:LookupItemSetResponse/ns1:BibInformation' - ); - - // Build the array of holdings: - $holdings = array(); - foreach ($avail as $current) { - $holdings[] = $this->getHoldingsForChunk($current); + $ids = null; + if (! $this->consortium) { + // Translate $id into consortial (035$a) format, e.g., "123" -> "(Agency) 123" + $sourceRecord = ''; + foreach ($this->agency as $_agency => $_dummy) { + $sourceRecord = '(' . $_agency . ') '; + } + $sourceRecord .= $id; + $ids = array($sourceRecord); } - return $holdings; + + return $this->getConsortialHolding($id, $patron, $ids); } + /** * Get Purchase History * @@ -346,49 +595,6 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf return array(); } - /** - * Build the request XML to log in a user: - * - * @param string $username Username for login - * @param string $password Password for login - * @param string $extras Extra elements to include in the request - * - * @return string NCIP request XML - */ - protected function getLookupUserRequest($username, $password, $extras = array()) - { - return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . - '<ns1:NCIPMessage xmlns:ns1="http://www.niso.org/2008/ncip" ' . - 'ns1:version="http://www.niso.org/schemas/ncip/v2_0/imp1/' . - 'xsd/ncip_v2_0.xsd">' . - '<ns1:LookupUser>' . - '<ns1:AuthenticationInput>' . - '<ns1:AuthenticationInputData>' . - htmlspecialchars($username) . - '</ns1:AuthenticationInputData>' . - '<ns1:AuthenticationDataFormatType>' . - 'text' . - '</ns1:AuthenticationDataFormatType>' . - '<ns1:AuthenticationInputType>' . - 'Username' . - '</ns1:AuthenticationInputType>' . - '</ns1:AuthenticationInput>' . - '<ns1:AuthenticationInput>' . - '<ns1:AuthenticationInputData>' . - htmlspecialchars($password) . - '</ns1:AuthenticationInputData>' . - '<ns1:AuthenticationDataFormatType>' . - 'text' . - '</ns1:AuthenticationDataFormatType>' . - '<ns1:AuthenticationInputType>' . - 'Password' . - '</ns1:AuthenticationInputType>' . - '</ns1:AuthenticationInput>' . - implode('', $extras) . - '</ns1:LookupUser>' . - '</ns1:NCIPMessage>'; - } - /** * Patron Login * @@ -408,10 +614,14 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf $id = $response->xpath( 'ns1:LookupUserResponse/ns1:UserId/ns1:UserIdentifierValue' ); + $patron_agency_id = $response->xpath( + 'ns1:LookupUserResponse/ns1:UserId/ns1:AgencyId' + ); if (!empty($id)) { // Fill in basic patron details: $patron = array( 'id' => (string)$id[0], + 'patron_agency_id' => (string)$patron_agency_id[0], 'cat_username' => $username, 'cat_password' => $password, 'email' => null, @@ -444,7 +654,7 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf * @return array Array of the patron's transactions on success. */ public function getMyTransactions($patron) - { + { $extras = array('<ns1:LoanedItemsDesired/>'); $request = $this->getLookupUserRequest( $patron['cat_username'], $patron['cat_password'], $extras @@ -453,16 +663,32 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf $retVal = array(); $list = $response->xpath('ns1:LookupUserResponse/ns1:LoanedItem'); + foreach ($list as $current) { - $due = $current->xpath('ns1:DateDue'); + $current->registerXPathNamespace('ns1', 'http://www.niso.org/2008/ncip'); + $tmp = $current->xpath('ns1:DateDue'); + $due = strtotime((string)$tmp[0]); + $due = date("l, d-M-y h:i a", $due); $title = $current->xpath('ns1:Title'); + $item_id = $current->xpath('ns1:ItemId/ns1:ItemIdentifierValue'); + $bib_id = $current->xpath('ns1:Ext/ns1:BibliographicDescription/' . + 'ns1:BibliographicRecordId/ns1:BibliographicRecordIdentifier'); + // Hack to account for bibs from other non-local institutions + // temporarily until consortial functionality is enabled. + if ((string)$bib_id[0]) { + $tmp = (string)$bib_id[0]; + } else { + $tmp = "1"; + } $retVal[] = array( - 'id' => false, - 'duedate' => (string)$due[0], - 'title' => (string)$title[0] + 'id' => $tmp, + 'duedate' => $due, + 'title' => (string)$title[0], + 'item_id' => (string)$item_id[0], + 'renewable' => true, ); - } - + } + return $retVal; } @@ -490,7 +716,11 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf ); $fines = array(); + $balance = 0; foreach ($list as $current) { + + $current->registerXPathNamespace('ns1', 'http://www.niso.org/2008/ncip'); + $tmp = $current->xpath( 'ns1:FiscalTransactionInformation/ns1:Amount/ns1:MonetaryValue' ); @@ -509,9 +739,10 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf $id = (string)$tmp[0]; */ $id = ''; + $balance += $amount; $fines[] = array( 'amount' => $amount, - 'balance' => $amount, + 'balance' => $balance, 'checkout' => '', 'fine' => $desc, 'duedate' => '', @@ -544,16 +775,33 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf $retVal = array(); $list = $response->xpath('ns1:LookupUserResponse/ns1:RequestedItem'); foreach ($list as $current) { + $id = $current->xpath('ns1:Ext/ns1:BibliographicDescription/' . + 'ns1:BibliographicRecordId/ns1:BibliographicRecordIdentifier'); $created = $current->xpath('ns1:DatePlaced'); $title = $current->xpath('ns1:Title'); $pos = $current->xpath('ns1:HoldQueuePosition'); - $retVal[] = array( - 'id' => false, - 'create' => (string)$created[0], - 'expire' => '', - 'title' => (string)$title[0], - 'position' => (string)$pos[0] - ); + $requestType = $current->xpath('ns1:RequestType'); + $requestId = $current->xpath('ns1:RequestId/ns1:RequestIdentifierValue'); + $itemId = $current->xpath('ns1:ItemId/ns1:ItemIdentifierValue'); + $pickupLocation = $current->xpath('ns1:PickupLocation'); + $expireDate = $current->xpath('ns1:PickupExpiryDate'); + $expireDate = strtotime((string)$expireDate[0]); + $expireDate = date("l, d-M-y", $expireDate); + $requestType = (string)$requestType[0]; + // Only return requests of type Hold or Recall. Callslips/Stack + // Retrieval requests are fetched using getMyStorageRetrievalRequests + if ($requestType === "Hold" or $requestType === "Recall") { + $retVal[] = array( + 'id' => (string)$id[0], + 'create' => '', + 'expire' => $expireDate, + 'title' => (string)$title[0], + 'position' => (string)$pos[0], + 'requestId' => (string)$requestId[0], + 'item_id' => (string)$itemId[0], + 'location' => (string)$pickupLocation[0], + ); + } } return $retVal; @@ -728,4 +976,636 @@ class XCNCIP2 extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterf // TODO return array(); } + + /** + * Public Function which retrieves Holds, StorageRetrivalRequests, and Consortial settings from the + * driver ini file. + * + * @param string $function The name of the feature to be checked + * + * @return array An array with key-value pairs. + */ + public function getConfig($function) + { + if ($function == 'Holds') { + return array( + 'HMACKeys' => 'item_id:holdtype:item_agency_id:aggregate_id:bib_id', + 'extraHoldFields' => 'comments:pickUpLocation:requiredByDate', + 'defaultRequiredDate' => '0:2:0', + 'consortium' => $this->consortium, + ); + } + if ($function == 'StorageRetrievalRequests') { + return array( + 'HMACKeys' => 'id:item_id:item_agency_id:aggregate_id:bib_id', + 'extraFields' => 'comments:pickUpLocation:requiredByDate:item-issue', + 'helpText' => 'This is a storage retrieval request help text' . + ' with some <span style="color: red">styling</span>.', + 'defaultRequiredDate' => '0:2:0', + ); + } + return array(); + } + + public function getDefaultPickUpLocation($patron = false, $holdDetails = null) + { + return $this->pickupLocations[$patron['patron_agency_id']][0]['locationID']; + } + + public function getPickUpLocations($patron) + { + $locations = array(); + foreach ($this->agency as $agency => $agencyID) { + foreach ($this->pickupLocations[$agency] as $thisAgency) { + $locations[] = + array( + 'locationID' => $thisAgency['locationID'], + 'locationDisplay' => $thisAgency['locationDisplay'], + ); + } + } + return $locations; + } + + /** + * Get Patron Storage Retrieval Requests + * + * This is responsible for retrieving all call slips by a specific patron. + * + * @param array $patron The patron array from patronLogin + * + * @return array Array of the patron's storage retrieval requests. + */ + public function getMyStorageRetrievalRequests($patron = false) + { + $extras = array('<ns1:RequestedItemsDesired/>'); + $request = $this->getLookupUserRequest( + $patron['cat_username'], $patron['cat_password'], $extras + ); + $response = $this->sendRequest($request); + + $retVal = array(); + $list = $response->xpath('ns1:LookupUserResponse/ns1:RequestedItem'); + foreach ($list as $current) { + $cancelled = true; + $id = $current->xpath('ns1:Ext/ns1:BibliographicDescription/' . + 'ns1:BibliographicRecordId/ns1:BibliographicRecordIdentifier'); + //$created = $current->xpath('ns1:DatePlaced'); + $title = $current->xpath('ns1:Title'); + $pos = $current->xpath('ns1:HoldQueuePosition'); + $pickupLocation = $current->xpath('ns1:PickupLocation'); + $requestId = $current->xpath('ns1:RequestId/ns1:RequestIdentifierValue'); + $requestType = $current->xpath('ns1:RequestType'); + $requestType = (string)$requestType[0]; + $tmpStatus = $current->xpath('ns1:RequestStatusType'); + list($status, $created) = explode(" ", (string)$tmpStatus[0], 2); + if ($status === "Accepted") { + $cancelled = false; + } + // Only return requests of type Stack Retrieval/Callslip. Hold + // and Recall requests are fetched using getMyHolds + if ($requestType === 'Stack Retrieval') + { + $retVal[] = array( + 'id' => (string)$id[0], + 'create' => $created, + 'expire' => '', + 'title' => (string)$title[0], + 'position' => (string)$pos[0], + 'requestId' => (string)$requestId[0], + 'location' => 'test', + 'canceled' => $cancelled, + 'location' => (string)$pickupLocation[0], + 'processed' => false, + ); + } + } + + return $retVal; + } + + public function checkStorageRetrievalRequestIsValid($id, $data, $patron) + { + return true; + } + + /** + * Place Storage Retrieval Request (Call Slip) + * + * Attempts to place a call slip request on a particular item and returns + * an array with result details + * + * @param array $details An array of item and patron data + * + * @return mixed An array of data on the request including + * whether or not it was successful. + */ + public function placeStorageRetrievalRequest($details) + { + $username = $details['patron']['cat_username']; + $password = $details['patron']['cat_password']; + $bibId = $details['bib_id']; + $itemId = $details['item_id']; + $pickUpLocation = $details['pickUpLocation']; + $lastInterestDate = $details['requiredBy']; + $lastInterestDate = substr($lastInterestDate, 6, 10) . '-' . + substr($lastInterestDate, 0, 5); + $lastInterestDate = $lastInterestDate . "T00:00:00.000Z"; + + $request = $this->getRequest($username, $password, $bibId, $itemId, + $details['patron']['patron_agency_id'], + $details['patron']['patron_agency_id'], + $details['item_agency_id'], + "Stack Retrieval", "Item", $lastInterestDate, $pickUpLocation); + + $response = $this->sendRequest($request); + $success = $response->xpath( + 'ns1:RequestItemResponse/ns1:ItemId/ns1:ItemIdentifierValue'); + + if ($success) { + return array( + 'success' => true, + "sysMessage" => 'Storage Retrieval Request Successful.' + ); + } else { + return array( + 'success' => false, + "sysMessage" => 'Storage Retrieval Request Not Successful.' + ); + } + } + + /** + * Get Renew Details + * + * This function returns the item id as a string which is then used + * as submitted form data in checkedOut.php. This value is then extracted by + * the RenewMyItems function. + * + * @param array $checkOutDetails An array of item data + * + * @return string Data for use in a form field + */ + public function getRenewDetails($checkOutDetails) + { + $renewDetails = $checkOutDetails['item_id']; + return $renewDetails; + } + + /** + * Place Hold + * + * Attempts to place a hold or recall on a particular item and returns + * an array with result details or throws an exception on failure of support + * classes + * + * @param array $holdDetails An array of item and patron data + * + * @throws ILSException + * @return mixed An array of data on the request including + * whether or not it was successful + */ + public function placeHold($details) + { + $username = $details['patron']['cat_username']; + $password = $details['patron']['cat_password']; + $bibId = $details['bib_id']; + $itemId = $details['item_id']; + $pickUpLocation = $details['pickUpLocation']; + $holdType = $details['holdtype']; + $lastInterestDate = $details['requiredBy']; + $lastInterestDate = substr($lastInterestDate, 6, 10) . '-' . + substr($lastInterestDate, 0, 5); + $lastInterestDate = $lastInterestDate . "T00:00:00.000Z"; + + $request = $this->getRequest($username, $password, $bibId, $itemId, + $details['patron']['patron_agency_id'], + $details['patron']['patron_agency_id'], + $details['item_agency_id'], + $holdType, "Item", $lastInterestDate, $pickUpLocation); + $response = $this->sendRequest($request); + $success = $response->xpath( + 'ns1:RequestItemResponse/ns1:ItemId/ns1:ItemIdentifierValue'); + + if ($success) { + return array( + 'success' => true, + "sysMessage" => 'Request Successful.' + ); + } else { + return array( + 'success' => false, + "sysMessage" => 'Request Not Successful.' + ); + } + } + + /** + * Cancel Holds + * + * Attempts to Cancel a hold or recall on a particular item. The + * data in $cancelDetails['details'] is determined by getCancelHoldDetails(). + * + * @param array $cancelDetails An array of item and patron data + * + * @return array An array of data on each request including + * whether or not it was successful. + */ + public function cancelHolds($cancelDetails) + { + $count = 0; + $username = $cancelDetails['patron']['cat_username']; + $password = $cancelDetails['patron']['cat_password']; + $details = $cancelDetails['details']; + $response = array(); + + foreach ($details as $cancelDetails) { + list($itemId, $requestId) = explode("|", $cancelDetails); + $request = $this->getCancelRequest( + $username, $password, $requestId, "Hold" + ); + $cancelRequestResponse = $this->sendRequest($request); + $userId = $cancelRequestResponse->xpath( + 'ns1:CancelRequestItemResponse/' . + 'ns1:UserId/ns1:UserIdentifierValue' + ); + $itemId = (string)$itemId; + if($userId) { + $count++; + $response[$itemId] = array( + 'success' => true, + 'status' => 'hold_cancel_success', + ); + } else { + $response[$itemId] = array( + 'success' => false, + 'status' => 'hold_cancel_fail', + ); + } + } + $result = array('count' => $count, 'items' => $response); + return $result; + } + + /** + * Get Cancel Hold Details + * + * This function returns the item id and recall id as a string + * separated by a pipe, which is then submitted as form data in Hold.php. This + * value is then extracted by the CancelHolds function. item id is used as the + * array key in the response. + * + * @param array $holdDetails An array of item data + * + * @return string Data for use in a form field + */ + public function getCancelHoldDetails($holdDetails) + { + $cancelDetails = $holdDetails['id']."|".$holdDetails['requestId']; + return $cancelDetails; + } + + /** + * Cancel Storage Retrieval Requests (Call Slips) + * + * Attempts to Cancel a call slip on a particular item. The + * data in $cancelDetails['details'] is determined by + * getCancelStorageRetrievalRequestDetails(). + * + * @param array $cancelDetails An array of item and patron data + * + * @return array An array of data on each request including + * whether or not it was successful. + */ + public function cancelStorageRetrievalRequests($cancelDetails) + { + $count = 0; + $username = $cancelDetails['patron']['cat_username']; + $password = $cancelDetails['patron']['cat_password']; + $details = $cancelDetails['details']; + $response = array(); + + foreach ($details as $cancelDetails) { + list($itemId, $requestId) = explode("|", $cancelDetails); + $request = $this->getCancelRequest( + $username, $password, $requestId, "Stack Retrieval" + ); + $cancelRequestResponse = $this->sendRequest($request); + $userId = $cancelRequestResponse->xpath( + 'ns1:CancelRequestItemResponse/'. + 'ns1:UserId/ns1:UserIdentifierValue' + ); + $itemId = (string)$itemId; + if($userId) { + $count++; + $response[$itemId] = array( + 'success' => true, + 'status' => 'storage_retrieval_request_cancel_success', + ); + } else { + $response[$itemId] = array( + 'success' => false, + 'status' => 'storage_retrieval_request_cancel_fail', + ); + } + } + $result = array('count' => $count, 'items' => $response); + return $result; + } + + /** + * Get Cancel Storage Retrieval Request (Call Slip) Details + * + * This function returns the item id and call slip id as a + * string separated by a pipe, which is then submitted as form data. This + * value is then extracted by the CancelStorageRetrievalRequests function. + * The item id is used as the key in the return value. + * + * @param array $details An array of item data + * + * @return string Data for use in a form field + */ + public function getCancelStorageRetrievalRequestDetails($callslipDetails) + { + $cancelDetails = $callslipDetails['id']."|".$callslipDetails['requestId']; + return $cancelDetails; + } + + /** + * Renew My Items + * + * Function for attempting to renew a patron's items. The data in + * $renewDetails['details'] is determined by getRenewDetails(). + * + * @param array $renewDetails An array of data required for renewing items + * including the Patron ID and an array of renewal IDS + * + * @return array An array of renewal information keyed by item ID + */ + public function renewMyItems($renewDetails) + { + $details = array(); + foreach ($renewDetails['details'] as $renewId) { + $request = $this->getRenewRequest( + $renewDetails['patron']['cat_username'], + $renewDetails['patron']['cat_password'], $renewId); + $response = $this->sendRequest($request); + $dueDate = $response->xpath('ns1:RenewItemResponse/ns1:DateDue'); + if ($dueDate) { + $tmp = $dueDate; + $newDueDate = (string)$tmp[0]; + $tmp = split("T", $newDueDate); + $splitDate = $tmp[0]; + $splitTime = $tmp[1]; + $details[$renewId] = array( + "success" => true, + "new_date" => $splitDate, + "new_time" => rtrim($splitTime, "Z"), + "item_id" => $renewId, + ); + + } else { + $details[$renewId] = array( + "success" => false, + "item_id" => $renewId, + ); + } + } + + return array(null, "details" => $details); + } + + /** + * Helper function to build the request XML to cancel a request: + * + * @param string $username Username for login + * @param string $password Password for login + * @param string $requestId Id of the request to cancel + * @param string $type The type of request to cancel (Hold, etc) + * + * @return string NCIP request XML + */ + protected function getCancelRequest($username, $password, $requestId, $type) + { + return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . + '<ns1:NCIPMessage xmlns:ns1="http://www.niso.org/2008/ncip" ' . + 'ns1:version="http://www.niso.org/schemas/ncip/v2_0/imp1/' . + 'xsd/ncip_v2_0.xsd">' . + '<ns1:CancelRequestItem>' . + '<ns1:AuthenticationInput>' . + '<ns1:AuthenticationInputData>' . + htmlspecialchars($username) . + '</ns1:AuthenticationInputData>' . + '<ns1:AuthenticationDataFormatType>' . + 'text' . + '</ns1:AuthenticationDataFormatType>' . + '<ns1:AuthenticationInputType>' . + 'Username' . + '</ns1:AuthenticationInputType>' . + '</ns1:AuthenticationInput>' . + '<ns1:AuthenticationInput>' . + '<ns1:AuthenticationInputData>' . + htmlspecialchars($password) . + '</ns1:AuthenticationInputData>' . + '<ns1:AuthenticationDataFormatType>' . + 'text' . + '</ns1:AuthenticationDataFormatType>' . + '<ns1:AuthenticationInputType>' . + 'Password' . + '</ns1:AuthenticationInputType>' . + '</ns1:AuthenticationInput>' . + '<ns1:RequestId>' . + '<ns1:RequestIdentifierValue>' . + htmlspecialchars($requestId) . + '</ns1:RequestIdentifierValue>' . + '</ns1:RequestId>' . + '<ns1:RequestType>' . + htmlspecialchars($type) . + '</ns1:RequestType>' . + '</ns1:CancelRequestItem>' . + '</ns1:NCIPMessage>'; + } + + /** + * Helper function to build the request XML to request an item + * (Hold, Storage Retrieval, etc) + * + * @param string $username Username for login + * @param string $password Password for login + * @param string $bibId Bib Id of item to request + * @param string $itemId Id of item to request + * @param string $requestType Type of the request (Hold, Callslip, etc) + * @param string $requestScope Level of request (title, item, etc) + * @param string $lastInterestDate Last date interested in item + * @param string $pickupLocation Code of location to pickup request + * + * @return string NCIP request XML + */ + protected function getRequest($username, $password, $bibId, $itemId, + $patron_agency_id, $pickup_agency_id, $item_agency_id, + $requestType, $requestScope, $lastInterestDate, $pickupLocation = null) + { + return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . + '<ns1:NCIPMessage xmlns:ns1="http://www.niso.org/2008/ncip" ' . + 'ns1:version="http://www.niso.org/schemas/ncip/v2_0/imp1/' . + 'xsd/ncip_v2_0.xsd">' . + '<ns1:RequestItem>' . + '<ns1:InitiationHeader>' . + '<ns1:FromAgencyId>' . + '<ns1:AgencyId>' . + htmlspecialchars($patron_agency_id) . + '</ns1:AgencyId>' . + '</ns1:FromAgencyId>' . + '<ns1:ToAgencyId>' . + '<ns1:AgencyId>' . + htmlspecialchars($pickup_agency_id) . + '</ns1:AgencyId>' . + '</ns1:ToAgencyId>' . + '</ns1:InitiationHeader>' . + '<ns1:AuthenticationInput>' . + '<ns1:AuthenticationInputData>' . + htmlspecialchars($username) . + '</ns1:AuthenticationInputData>' . + '<ns1:AuthenticationDataFormatType>' . + 'text' . + '</ns1:AuthenticationDataFormatType>' . + '<ns1:AuthenticationInputType>' . + 'Username' . + '</ns1:AuthenticationInputType>' . + '</ns1:AuthenticationInput>' . + '<ns1:AuthenticationInput>' . + '<ns1:AuthenticationInputData>' . + htmlspecialchars($password) . + '</ns1:AuthenticationInputData>' . + '<ns1:AuthenticationDataFormatType>' . + 'text' . + '</ns1:AuthenticationDataFormatType>' . + '<ns1:AuthenticationInputType>' . + 'Password' . + '</ns1:AuthenticationInputType>' . + '</ns1:AuthenticationInput>' . + '<ns1:BibliographicId>' . + '<ns1:BibliographicRecordId>' . + '<ns1:AgencyId>' . + htmlspecialchars($item_agency_id) . + '</ns1:AgencyId>' . + '<ns1:BibliographicRecordIdentifier>' . + htmlspecialchars($bibId) . + '</ns1:BibliographicRecordIdentifier>' . + '</ns1:BibliographicRecordId>' . + '</ns1:BibliographicId>' . + '<ns1:ItemId>' . + '<ns1:ItemIdentifierValue>' . + htmlspecialchars($itemId) . + '</ns1:ItemIdentifierValue>' . + '</ns1:ItemId>' . + '<ns1:RequestType>' . + htmlspecialchars($requestType) . + '</ns1:RequestType>' . + '<ns1:RequestScopeType ' . + 'ns1:Scheme="http://www.niso.org/ncip/v1_0/imp1/schemes' . + '/requestscopetype/requestscopetype.scm">' . + htmlspecialchars($requestScope) . + '</ns1:RequestScopeType>' . + '<ns1:PickupLocation>' . + htmlspecialchars($pickupLocation) . + '</ns1:PickupLocation>' . + '<ns1:PickupExpiryDate>' . + htmlspecialchars($lastInterestDate) . + '</ns1:PickupExpiryDate>' . + '</ns1:RequestItem>' . + '</ns1:NCIPMessage>'; + } + + /** + * Helper function to build the request XML to renew an item: + * + * @param string $username Username for login + * @param string $password Password for login + * @param string $itemId Id of item to renew + * + * @return string NCIP request XML + */ + protected function getRenewRequest($username, $password, $itemId) + { + return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . + '<ns1:NCIPMessage xmlns:ns1="http://www.niso.org/2008/ncip" ' . + 'ns1:version="http://www.niso.org/schemas/ncip/v2_0/imp1/' . + 'xsd/ncip_v2_0.xsd">' . + '<ns1:RenewItem>' . + '<ns1:AuthenticationInput>' . + '<ns1:AuthenticationInputData>' . + htmlspecialchars($username) . + '</ns1:AuthenticationInputData>' . + '<ns1:AuthenticationDataFormatType>' . + 'text' . + '</ns1:AuthenticationDataFormatType>' . + '<ns1:AuthenticationInputType>' . + 'Username' . + '</ns1:AuthenticationInputType>' . + '</ns1:AuthenticationInput>' . + '<ns1:AuthenticationInput>' . + '<ns1:AuthenticationInputData>' . + htmlspecialchars($password) . + '</ns1:AuthenticationInputData>' . + '<ns1:AuthenticationDataFormatType>' . + 'text' . + '</ns1:AuthenticationDataFormatType>' . + '<ns1:AuthenticationInputType>' . + 'Password' . + '</ns1:AuthenticationInputType>' . + '</ns1:AuthenticationInput>' . + '<ns1:ItemId>' . + '<ns1:ItemIdentifierValue>' . + htmlspecialchars($itemId) . + '</ns1:ItemIdentifierValue>' . + '</ns1:ItemId>' . + '</ns1:RenewItem>' . + '</ns1:NCIPMessage>'; + } + + /** + * Helper function to build the request XML to log in a user + * and/or retrieve loaned items / request information + * + * @param string $username Username for login + * @param string $password Password for login + * @param string $extras Extra elements to include in the request + * + * @return string NCIP request XML + */ + protected function getLookupUserRequest($username, $password, $extras = array()) + { + return '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . + '<ns1:NCIPMessage xmlns:ns1="http://www.niso.org/2008/ncip" ' . + 'ns1:version="http://www.niso.org/schemas/ncip/v2_0/imp1/' . + 'xsd/ncip_v2_0.xsd">' . + '<ns1:LookupUser>' . + '<ns1:AuthenticationInput>' . + '<ns1:AuthenticationInputData>' . + htmlspecialchars($username) . + '</ns1:AuthenticationInputData>' . + '<ns1:AuthenticationDataFormatType>' . + 'text' . + '</ns1:AuthenticationDataFormatType>' . + '<ns1:AuthenticationInputType>' . + 'Username' . + '</ns1:AuthenticationInputType>' . + '</ns1:AuthenticationInput>' . + '<ns1:AuthenticationInput>' . + '<ns1:AuthenticationInputData>' . + htmlspecialchars($password) . + '</ns1:AuthenticationInputData>' . + '<ns1:AuthenticationDataFormatType>' . + 'text' . + '</ns1:AuthenticationDataFormatType>' . + '<ns1:AuthenticationInputType>' . + 'Password' . + '</ns1:AuthenticationInputType>' . + '</ns1:AuthenticationInput>' . + implode('', $extras) . + '</ns1:LookupUser>' . + '</ns1:NCIPMessage>'; + } } +