From 404a36489dd557b720e9fc9c17b9fa18dd08cfb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lahmann?= <lahmann@users.noreply.github.com> Date: Mon, 12 Sep 2016 18:04:05 +0200 Subject: [PATCH] Updated DAIA and new PAIA ILS-driver (#729) --- config/vufind/DAIA.ini | 21 + config/vufind/PAIA.ini | 49 + config/vufind/config.ini | 4 +- module/VuFind/config/module.config.php | 1 + module/VuFind/src/VuFind/ILS/Driver/DAIA.php | 520 ++++- .../VuFind/src/VuFind/ILS/Driver/Factory.php | 29 +- module/VuFind/src/VuFind/ILS/Driver/PAIA.php | 1764 +++++++++++++++++ .../paia/response/changePassword.json | 15 + .../tests/fixtures/paia/response/fees.json | 51 + .../tests/fixtures/paia/response/items.json | 87 + .../tests/fixtures/paia/response/login.json | 18 + .../fixtures/paia/response/login_bad.json | 14 + .../tests/fixtures/paia/response/patron.json | 22 + .../paia/response/patron_expired.json | 22 + .../fixtures/paia/response/patron_locked.json | 22 + .../fixtures/paia/response/renew_error.json | 17 + .../fixtures/paia/response/renew_ok.json | 30 + .../paia/response/storageretrieval.json | 29 + .../src/VuFindTest/ILS/Driver/DAIATest.php | 53 +- .../src/VuFindTest/ILS/Driver/PAIATest.php | 628 ++++++ 20 files changed, 3287 insertions(+), 109 deletions(-) create mode 100644 config/vufind/PAIA.ini create mode 100644 module/VuFind/src/VuFind/ILS/Driver/PAIA.php create mode 100644 module/VuFind/tests/fixtures/paia/response/changePassword.json create mode 100644 module/VuFind/tests/fixtures/paia/response/fees.json create mode 100644 module/VuFind/tests/fixtures/paia/response/items.json create mode 100644 module/VuFind/tests/fixtures/paia/response/login.json create mode 100644 module/VuFind/tests/fixtures/paia/response/login_bad.json create mode 100644 module/VuFind/tests/fixtures/paia/response/patron.json create mode 100644 module/VuFind/tests/fixtures/paia/response/patron_expired.json create mode 100644 module/VuFind/tests/fixtures/paia/response/patron_locked.json create mode 100644 module/VuFind/tests/fixtures/paia/response/renew_error.json create mode 100644 module/VuFind/tests/fixtures/paia/response/renew_ok.json create mode 100644 module/VuFind/tests/fixtures/paia/response/storageretrieval.json create mode 100644 module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PAIATest.php diff --git a/config/vufind/DAIA.ini b/config/vufind/DAIA.ini index aae070fdde5..2986e6b32ea 100644 --- a/config/vufind/DAIA.ini +++ b/config/vufind/DAIA.ini @@ -14,6 +14,12 @@ ; daiaResponseFormat = json ; +; This section contains settings applying to DAIA and its deriving classes such as +; PAIA. +;[General] +; Set the time to live (ttl) for cached data (default is 30 seconds). +;cacheLifetime = 90 + [DAIA] ; The base URL for the DAIA webservice. baseUrl = [your DAIA server base url] @@ -44,3 +50,18 @@ daiaContentTypes['xml'] = "application/xml" ; (separate multiple values by commas, for example: ; daiaContentTypes['json'] = "application/json, application/javascript" daiaContentTypes['json'] = "application/json" + +; Enable caching for DAIA items (default is false). +;daiaCache = false + +; DAIA does not support placing holds (this functionality is covered by PAIA) but is +; able to provide a link to the OPAC to perform such an action. Regarding placing +; holds/recalls such link is usually given as href for an unavailable service. +; Uncomment the below section Holds with the setting 'function' to show a link to +; the OPAC (if it was provided in the DAIA response) instead of a VuFind +; Holds-Button. +; If PAIA is used in combination with the DAIA driver, handling holds etc. should be +; left to the logic in the PAIA driver - thus keep this section commented out if you +; are using PAIA as well. +; [Holds] +; function = getHoldLink diff --git a/config/vufind/PAIA.ini b/config/vufind/PAIA.ini new file mode 100644 index 00000000000..dd7ade3a47e --- /dev/null +++ b/config/vufind/PAIA.ini @@ -0,0 +1,49 @@ +; The PAIA driver extends the DAIA driver and uses the settings for the DAIA driver. +; Either copy & paste the settings from DAIA.ini into this PAIA.ini or reference your +; correctly configured DAIA.ini via [Parent_Config] +;[Parent_Config] +;relative_path = DAIA.ini + +; PAIA configuration +[PAIA] +; base URL of the PAIA server WITH trailing slash +baseUrl = "" + +; Enable caching for PAIA items (default is false). TTL for cached data will be the +; same as for DAIA cache (see cacheLifetime setting in DAIA.ini). +;paiaCache = false + +; Driver configuration, usually you can leave it untouched + +; Without customization the PAIA driver will offer to place a recall for items with +; unavailable service loan but set href for loan. The recall will be performed via +; PAIA request. +; The pre-defined HMACKeys (id:item_id:doc_id) should suffice to place a recall. No +; extra fields are allowed (if you need those you might be able to cover this +; functionality in a custom driver by using PAIA confirm/conditions). +[Holds] +; HMACKeys - A list of form element names that will be analyzed for consistency +; during form processing. Most users should not need to change this setting. +HMACKeys = "id:item_id:doc_id" + +; Without customization the PAIA driver will offer to place a storageretrievalrequest +; for items with available service loan and set href for loan. The +; storageretrievalrequest will be performed via PAIA request (technically the same as +; for recall, but with different frontend templates etc.). +; The pre-defined HMACKeys (id:item_id:doc_id) should suffice to request an item. No +; extra fields are allowed (if you need those you might be able to cover this +; functionality in a custom driver by using PAIA confirm/conditions). +[StorageRetrievalRequests] +; HMACKeys - A list of form element names that will be analyzed for consistency +; during form processing. Most users should not need to change this setting. +HMACKeys = id:item_id:doc_id + +; The PAIA driver supports renewals in MyResearch views. The renewal will be +; performed via PAIA renew. +; The pre-defined HMACKeys (id:item_id:doc_id) should suffice to renew an item. No +; extra fields are allowed (if you need those you might be able to cover this +; functionality in a custom driver by using PAIA confirm/conditions). +[Renewals] +; HMACKeys - A list of form element names that will be analyzed for consistency +; during form processing. Most users should not need to change this setting. +HMACKeys = "id:item_id:doc_id" diff --git a/config/vufind/config.ini b/config/vufind/config.ini index a2259287231..bfb9ceaa587 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -146,11 +146,11 @@ lifetime = 3600 ; Session lasts for 1 hour ; Please set the ILS that VuFind will interact with. ; -; Available drivers: Aleph, Amicus, ClaviusSQL, Evergreen, Horizon (basic database +; Available drivers: Aleph, Amicus, ClaviusSQL, DAIA, Evergreen, Horizon (basic db ; access only), HorizonXMLAPI (more features via API), Innovative, Koha, ; KohaILSDI, LBS4, MultiBackend (to chain together multiple drivers in a ; consortial setting), NewGenLib, NoILS (for users without an ILS, or to -; disable ILS functionality during maintenance), Polaris, Unicorn (which +; disable ILS functionality during maintenance), PAIA, Polaris, Unicorn (which ; also applies to SirsiDynix Symphony), Virtua, Voyager (for Voyager 6+), ; VoyagerRestful (for Voyager 7+ w/ RESTful web services), XCNCIP2 (for XC ; NCIP Tookit v2.x) diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index fc98dae4e1b..12c50d8f86f 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -389,6 +389,7 @@ $config = [ 'lbs4' => 'VuFind\ILS\Driver\Factory::getLBS4', 'multibackend' => 'VuFind\ILS\Driver\Factory::getMultiBackend', 'noils' => 'VuFind\ILS\Driver\Factory::getNoILS', + 'paia' => 'VuFind\ILS\Driver\Factory::getPAIA', 'kohailsdi' => 'VuFind\ILS\Driver\Factory::getKohaILSDI', 'unicorn' => 'VuFind\ILS\Driver\Factory::getUnicorn', 'voyager' => 'VuFind\ILS\Driver\Factory::getVoyager', diff --git a/module/VuFind/src/VuFind/ILS/Driver/DAIA.php b/module/VuFind/src/VuFind/ILS/Driver/DAIA.php index 23dc74cf688..2395dbc0dac 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/DAIA.php +++ b/module/VuFind/src/VuFind/ILS/Driver/DAIA.php @@ -59,6 +59,13 @@ class DAIA extends AbstractBase implements */ protected $baseUrl; + /** + * Flag to switch on/off caching for DAIA items + * + * @var bool + */ + protected $daiaCacheEnabled = false; + /** * DAIA query identifier prefix * @@ -159,6 +166,35 @@ class DAIA extends AbstractBase implements } else { $this->debug('No ContentTypes for response defined. Accepting any.'); } + if (isset($this->config['DAIA']['daiaCache'])) { + $this->daiaCacheEnabled = $this->config['DAIA']['daiaCache']; + } else { + $this->debug('Caching not enabled, disabling it by default.'); + } + if (isset($this->config['General']) + && isset($this->config['General']['cacheLifetime']) + ) { + $this->cacheLifetime = $this->config['General']['cacheLifetime']; + } else { + $this->debug( + 'Cache lifetime not set, using VuFind\ILS\Driver\AbstractBase ' . + 'default value.' + ); + } + } + + /** + * DAIA specific override of method to ensure uniform cache keys for cached + * VuFind objects. + * + * @param string|null $suffix Optional suffix that will get appended to the + * object class name calling getCacheKey() + * + * @return string + */ + protected function getCacheKey($suffix = null) + { + return parent::getCacheKey(md5($this->baseURL) . $suffix); } /** @@ -208,6 +244,15 @@ class DAIA extends AbstractBase implements */ public function getStatus($id) { + // check ids for existing availability data in cache and skip these ids + if ($this->daiaCacheEnabled + && $item = $this->getCachedData($this->generateURI($id)) + ) { + if ($item != null) { + return $item; + } + } + // let's retrieve the DAIA document by URI try { $rawResult = $this->doHTTPRequest($this->generateURI($id)); @@ -216,7 +261,12 @@ class DAIA extends AbstractBase implements $doc = $this->extractDaiaDoc($id, $rawResult); if (!is_null($doc)) { // parse the extracted DAIA document and return the status info - return $this->parseDaiaDoc($id, $doc); + $data = $this->parseDaiaDoc($id, $doc); + // cache the status information + if ($this->daiaCacheEnabled) { + $this->putCachedData($this->generateURI($id), $data); + } + return $data; } } catch (ILSException $e) { $this->debug($e->getMessage()); @@ -247,41 +297,66 @@ class DAIA extends AbstractBase implements { $status = []; - try { - if ($this->multiQuery) { - // perform one DAIA query with multiple URIs - $rawResult = $this - ->doHTTPRequest($this->generateMultiURIs($ids)); - // the id used in VuFind can differ from the document-URI - // (depending on how the URI is generated) - foreach ($ids as $id) { - // it is assumed that each DAIA document has a unique URI, - // so get the document with the corresponding id - $doc = $this->extractDaiaDoc($id, $rawResult); - if (!is_null($doc)) { - // a document with the corresponding id exists, which - // means we got status information for that record - $status[] = $this->parseDaiaDoc($id, $doc); - } - unset($doc); + // check cache for given ids and skip these ids if availability data is found + foreach ($ids as $key => $id) { + if ($this->daiaCacheEnabled + && $item = $this->getCachedData($this->generateURI($id)) + ) { + if ($item != null) { + $status[] = $item; + unset($ids[$key]); } - } else { - // multiQuery is not supported, so retrieve DAIA documents one by - // one - foreach ($ids as $id) { - $rawResult = $this->doHTTPRequest($this->generateURI($id)); - // extract the DAIA document for the current id from the - // HTTPRequest's result - $doc = $this->extractDaiaDoc($id, $rawResult); - if (!is_null($doc)) { - // parse the extracted DAIA document and save the status - // info - $status[] = $this->parseDaiaDoc($id, $doc); + } + } + + // only query DAIA service if we have some ids left + if (count($ids) > 0) { + try { + if ($this->multiQuery) { + // perform one DAIA query with multiple URIs + $rawResult = $this + ->doHTTPRequest($this->generateMultiURIs($ids)); + // the id used in VuFind can differ from the document-URI + // (depending on how the URI is generated) + foreach ($ids as $id) { + // it is assumed that each DAIA document has a unique URI, + // so get the document with the corresponding id + $doc = $this->extractDaiaDoc($id, $rawResult); + if (!is_null($doc)) { + // a document with the corresponding id exists, which + // means we got status information for that record + $data = $this->parseDaiaDoc($id, $doc); + // cache the status information + if ($this->daiaCacheEnabled) { + $this->putCachedData($this->generateURI($id), $data); + } + $status[] = $data; + } + unset($doc); + } + } else { + // multiQuery is not supported, so retrieve DAIA documents one by + // one + foreach ($ids as $id) { + $rawResult = $this->doHTTPRequest($this->generateURI($id)); + // extract the DAIA document for the current id from the + // HTTPRequest's result + $doc = $this->extractDaiaDoc($id, $rawResult); + if (!is_null($doc)) { + // parse the extracted DAIA document and save the status + // info + $data = $this->parseDaiaDoc($id, $doc); + // cache the status information + if ($this->daiaCacheEnabled) { + $this->putCachedData($this->generateURI($id), $data); + } + $status[] = $data; + } } } + } catch (ILSException $e) { + $this->debug($e->getMessage()); } - } catch (ILSException $e) { - $this->debug($e->getMessage()); } return $status; } @@ -430,7 +505,7 @@ class DAIA extends AbstractBase implements * * @return string URI of the DAIA document * - * @see http://gbv.github.io/daiaspec/daia.html#query-api + * @see http://gbv.github.io/daia/daia.html#query-parameters */ protected function generateURI($id) { @@ -445,7 +520,7 @@ class DAIA extends AbstractBase implements * * @return string Combined URIs (delimited by "|") * - * @see http://gbv.github.io/daiaspec/daia.html#query-api + * @see http://gbv.github.io/daia/daia.html#query-parameters */ protected function generateMultiURIs($ids) { @@ -657,6 +732,8 @@ class DAIA extends AbstractBase implements foreach ($daiaArray['item'] as $item) { $result_item = []; $result_item['id'] = $id; + // custom DAIA field + $result_item['doc_id'] = $doc_id; $result_item['item_id'] = $item['id']; // custom DAIA field used in getHoldLink() $result_item['ilslink'] @@ -671,9 +748,17 @@ class DAIA extends AbstractBase implements // get callnumber $result_item['callnumber'] = $this->getItemCallnumber($item); // get location - $result_item['location'] = $this->getItemLocation($item); + $result_item['location'] = $this->getItemDepartment($item); + // custom DAIA field + $result_item['locationid'] = $this->getItemDepartmentId($item); // get location link - $result_item['locationhref'] = $this->getItemLocationLink($item); + $result_item['locationhref'] = $this->getItemDepartmentLink($item); + // custom DAIA field + $result_item['storage'] = $this->getItemStorage($item); + // custom DAIA field + $result_item['storageid'] = $this->getItemStorageId($item); + // custom DAIA field + $result_item['storagehref'] = $this->getItemStorageLink($item); // status and availability will be calculated in own function $result_item = $this->getItemStatus($item) + $result_item; // add result_item to the result array @@ -696,9 +781,10 @@ class DAIA extends AbstractBase implements $availability = false; $status = ''; // status cannot be null as this will crash the translator $duedate = null; - $availableLink = ''; + $serviceLink = ''; $queue = ''; $item_notes = []; + $item_limitation_types = []; $services = []; if (isset($item['available'])) { @@ -720,11 +806,11 @@ class DAIA extends AbstractBase implements // openaccess $availability = true; if ($available['service'] == 'loan' - && isset($available['service']['href']) + && isset($available['href']) ) { // save the link to the ils if we have a href for loan // service - $availableLink = $available['service']['href']; + $serviceLink = $available['href']; } } @@ -732,7 +818,11 @@ class DAIA extends AbstractBase implements if (isset($available['limitation'])) { $item_notes = array_merge( $item_notes, - $this->getItemLimitation($available['limitation']) + $this->getItemLimitationContent($available['limitation']) + ); + $item_limitation_types = array_merge( + $item_limitation_types, + $this->getItemLimitationTypes($available['limitation']) ); } @@ -758,16 +848,23 @@ class DAIA extends AbstractBase implements ) ) { if ($unavailable['service'] == 'loan' - && isset($unavailable['service']['href']) + && isset($unavailable['href']) ) { //save the link to the ils if we have a href for loan service + $serviceLink = $unavailable['href']; } // use limitation element for status string if (isset($unavailable['limitation'])) { $item_notes = array_merge( $item_notes, - $this->getItemLimitation($unavailable['limitation']) + $this->getItemLimitationContent( + $unavailable['limitation'] + ) + ); + $item_limitation_types = array_merge( + $item_limitation_types, + $this->getItemLimitationTypes($unavailable['limitation']) ); } } @@ -796,119 +893,280 @@ class DAIA extends AbstractBase implements } } - /*'availability' => '0', - 'status' => '', // string - needs to be computed from availability info - 'duedate' => '', // if checked_out else null - 'returnDate' => '', // false if not recently returned(?) - 'requests_placed' => '', // total number of placed holds - 'is_holdable' => false, // place holding possible?*/ + /*'returnDate' => '', // false if not recently returned(?)*/ - if (!empty($availableLink)) { - $return['ilslink'] = $availableLink; + if (!empty($serviceLink)) { + $return['ilslink'] = $serviceLink; } $return['item_notes'] = $item_notes; - $return['status'] = $status; + $return['status'] = $this->getStatusString($item); $return['availability'] = $availability; $return['duedate'] = $duedate; $return['requests_placed'] = $queue; $return['services'] = $this->getAvailableItemServices($services); + // In this DAIA driver implementation addLink and is_holdable are assumed + // Boolean as patron based availability requires either a patron-id or -type. + // This should be handled in a custom DAIA driver + $return['addLink'] = $this->checkIsRecallable($item); + $return['is_holdable'] = $this->checkIsRecallable($item); + $return['holdtype'] = $this->getHoldType($item); + + // Check if we the item is available for storage retrieval request if it is + // not holdable. + $return['addStorageRetrievalRequestLink'] = !$return['is_holdable'] + ? $this->checkIsStorageRetrievalRequest($item) : false; + + // add a custom Field to allow passing custom DAIA data to the frontend in + // order to use it for more precise display of availability + $return['customData'] = $this->getCustomData($item); + + $return['limitation_types'] = $item_limitation_types; + return $return; } /** - * Returns the value for "number" in VuFind getStatus/getHolding array + * Helper function to allow custom data in status array. * - * @param array $item Array with DAIA item data - * @param int $counter Integer counting items as alternative return value + * @param array $item Array with DAIA item data * - * @return mixed + * @return array */ - protected function getItemNumber($item, $counter) + protected function getCustomData($item) { - return $counter; + return []; } /** - * Returns the value for "barcode" in VuFind getStatus/getHolding array + * Helper function to return an appropriate status string for current item. * * @param array $item Array with DAIA item data * * @return string */ - protected function getItemBarcode($item) + protected function getStatusString($item) { - return '1'; + // status cannot be null as this will crash the translator + return ''; } /** - * Returns the value for "reserve" in VuFind getStatus/getHolding array + * Helper function to determine if item is recallable. + * DAIA does not genuinly allow distinguishing between holdable and recallable + * items. This could be achieved by usage of limitations but this would not be + * shared functionality between different DAIA implementations (thus should be + * implemented in custom drivers). Therefore this returns whether an item + * is recallable based on unavailable services and the existence of an href. * * @param array $item Array with DAIA item data * - * @return string + * @return bool */ - protected function getItemReserveStatus($item) + protected function checkIsRecallable($item) { - return 'N'; + // This basic implementation checks the item for being unavailable for loan + // and presentation but with an existing href (as a flag for further action). + $services = ['available' => [], 'unavailable' => []]; + $href = false; + if (isset($item['available'])) { + // check if item is loanable or presentation + foreach ($item['available'] as $available) { + if (isset($available['service']) + && in_array($available['service'], ['loan', 'presentation']) + ) { + $services['available'][] = $available['service']; + } + } + } + + if (isset($item['unavailable'])) { + foreach ($item['unavailable'] as $unavailable) { + if (isset($unavailable['service']) + && in_array($unavailable['service'], ['loan', 'presentation']) + ) { + $services['unavailable'][] = $unavailable['service']; + // attribute href is used to determine whether item is recallable + // or not + $href = isset($unavailable['href']) ? true : $href; + } + } + } + + // Check if we have at least one service unavailable and a href field is set + // (either as flag or as actual value for the next action). + return ($href && count( + array_diff($services['unavailable'], $services['available']) + )); } /** - * Returns the value for "callnumber" in VuFind getStatus/getHolding array + * Helper function to determine if the item is available as storage retrieval. + * + * @param array $item Array with DAIA item data + * + * @return bool + */ + protected function checkIsStorageRetrievalRequest($item) + { + // This basic implementation checks the item for being available for loan + // and presentation but with an existing href (as a flag for further action). + $services = ['available' => [], 'unavailable' => []]; + $href = false; + if (isset($item['available'])) { + // check if item is loanable or presentation + foreach ($item['available'] as $available) { + if (isset($available['service']) + && in_array($available['service'], ['loan', 'presentation']) + ) { + $services['available'][] = $available['service']; + // attribute href is used to determine whether item is + // requestable or not + $href = isset($available['href']) ? true : $href; + } + } + } + + if (isset($item['unavailable'])) { + foreach ($item['unavailable'] as $unavailable) { + if (isset($unavailable['service']) + && in_array($unavailable['service'], ['loan', 'presentation']) + ) { + $services['unavailable'][] = $unavailable['service']; + } + } + } + + // Check if we have at least one service unavailable and a href field is set + // (either as flag or as actual value for the next action). + return ($href && count( + array_diff($services['available'], $services['unavailable']) + )); + } + + /** + * Helper function to determine the holdtype availble for current item. + * DAIA does not genuinly allow distinguishing between holdable and recallable + * items. This could be achieved by usage of limitations but this would not be + * shared functionality between different DAIA implementations (thus should be + * implemented in custom drivers). Therefore getHoldType always returns recall. + * + * @param array $item Array with DAIA item data + * + * @return string 'recall'|null + */ + protected function getHoldType($item) + { + // return holdtype (hold, recall or block if patron is not allowed) for item + return $this->checkIsRecallable($item) ? 'recall' : null; + } + + /** + * Returns the evaluated value of the provided limitation element + * + * @param array $limitations Array with DAIA limitation data + * + * @return array + */ + protected function getItemLimitation($limitations) + { + $itemLimitation = []; + foreach ($limitations as $limitation) { + // return the first limitation with content set + if (isset($limitation['content'])) { + $itemLimitation[] = $limitation['content']; + } + } + return $itemLimitation; + } + + /** + * Returns the value of item.department.content (e.g. to be used in VuFind + * getStatus/getHolding array as location) * * @param array $item Array with DAIA item data * * @return string */ - protected function getItemCallnumber($item) + protected function getItemDepartment($item) { - return isset($item['label']) && !empty($item['label']) - ? $item['label'] + return isset($item['department']) && isset($item['department']['content']) + && !empty($item['department']['content']) + ? $item['department']['content'] : 'Unknown'; } /** - * Returns the value for "location" in VuFind getStatus/getHolding array + * Returns the value of item.department.id (e.g. to be used in VuFind + * getStatus/getHolding array as location) * * @param array $item Array with DAIA item data * * @return string */ - protected function getItemLocation($item) + protected function getItemDepartmentId($item) { - $location = ''; + return isset($item['department']) && isset($item['department']['id']) + ? $item['department']['id'] : ''; + } - if (isset($item['department']) - && isset($item['department']['content']) - ) { - $location .= (empty($location) - ? $item['department']['content'] - : ' - ' . $item['department']['content']); - } + /** + * Returns the value of item.department.href (e.g. to be used in VuFind + * getStatus/getHolding array for linking the location) + * + * @param array $item Array with DAIA item data + * + * @return string + */ + protected function getItemDepartmentLink($item) + { + return isset($item['department']['href']) + ? $item['department']['href'] : false; + } - if (isset($item['storage']) - && isset($item['storage']['content']) - ) { - $location .= (empty($location) - ? $item['storage']['content'] - : ' - ' . $item['storage']['content']); - } + /** + * Returns the value of item.storage.content (e.g. to be used in VuFind + * getStatus/getHolding array as location) + * + * @param array $item Array with DAIA item data + * + * @return string + */ + protected function getItemStorage($item) + { + return isset($item['storage']) && isset($item['storage']['content']) + && !empty($item['storage']['content']) + ? $item['storage']['content'] + : 'Unknown'; + } - return (empty($location) ? 'Unknown' : $location); + /** + * Returns the value of item.storage.id (e.g. to be used in VuFind + * getStatus/getHolding array as location) + * + * @param array $item Array with DAIA item data + * + * @return string + */ + protected function getItemStorageId($item) + { + return isset($item['storage']) && isset($item['storage']['id']) + ? $item['storage']['id'] : ''; } /** - * Returns the value for "location" href in VuFind getStatus/getHolding array + * Returns the value of item.storage.href (e.g. to be used in VuFind + * getStatus/getHolding array for linking the location) * * @param array $item Array with DAIA item data * * @return string */ - protected function getItemLocationLink($item) + protected function getItemStorageLink($item) { - return isset($item['storage']['href']) - ? $item['storage']['href'] : false; + return isset($item['storage']) && isset($item['storage']['href']) + ? $item['storage']['href'] : ''; } /** @@ -918,16 +1176,86 @@ class DAIA extends AbstractBase implements * * @return array */ - protected function getItemLimitation($limitations) + protected function getItemLimitationContent($limitations) { - $itemLimitation = []; + $itemLimitationContent = []; foreach ($limitations as $limitation) { - // return the limitations with content set + // return the first limitation with content set if (isset($limitation['content'])) { - $itemLimitation[] = $limitation['content']; + $itemLimitationContent[] = $limitation['content']; } } - return $itemLimitation; + return $itemLimitationContent; + } + + /** + * Returns the evaluated values of the provided limitations element + * + * @param array $limitations Array with DAIA limitation data + * + * @return array + */ + protected function getItemLimitationTypes($limitations) + { + $itemLimitationTypes = []; + foreach ($limitations as $limitation) { + // return the first limitation with content set + if (isset($limitation['id'])) { + $itemLimitationTypes[] = $limitation['id']; + } + } + return $itemLimitationTypes; + } + + /** + * Returns the value for "number" in VuFind getStatus/getHolding array + * + * @param array $item Array with DAIA item data + * @param int $counter Integer counting items as alternative return value + * + * @return mixed + */ + protected function getItemNumber($item, $counter) + { + return $counter; + } + + /** + * Returns the value for "location" in VuFind getStatus/getHolding array + * + * @param array $item Array with DAIA item data + * + * @return string + */ + protected function getItemBarcode($item) + { + return '1'; + } + + /** + * Returns the value for "reserve" in VuFind getStatus/getHolding array + * + * @param array $item Array with DAIA item data + * + * @return string + */ + protected function getItemReserveStatus($item) + { + return 'N'; + } + + /** + * Returns the value for "callnumber" in VuFind getStatus/getHolding array + * + * @param array $item Array with DAIA item data + * + * @return string + */ + protected function getItemCallnumber($item) + { + return isset($item['label']) && !empty($item['label']) + ? $item['label'] + : 'Unknown'; } /** @@ -952,7 +1280,7 @@ class DAIA extends AbstractBase implements } return array_intersect(['loan', 'presentation'], $availableServices); } - + /** * Logs content of message elements in DAIA response for debugging * diff --git a/module/VuFind/src/VuFind/ILS/Driver/Factory.php b/module/VuFind/src/VuFind/ILS/Driver/Factory.php index 251cc697b31..8d6cdd8a21a 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/Factory.php +++ b/module/VuFind/src/VuFind/ILS/Driver/Factory.php @@ -65,9 +65,15 @@ class Factory */ public static function getDAIA(ServiceManager $sm) { - return new DAIA( + $daia = new DAIA( $sm->getServiceLocator()->get('VuFind\DateConverter') ); + + $daia->setCacheStorage( + $sm->getServiceLocator()->get('VuFind\CacheManager')->getCache('object') + ); + + return $daia; } /** @@ -156,6 +162,27 @@ class Factory return new NoILS($sm->getServiceLocator()->get('VuFind\RecordLoader')); } + /** + * Factory for PAIA driver. + * + * @param ServiceManager $sm Service manager. + * + * @return PAIA + */ + public static function getPAIA(ServiceManager $sm) + { + $paia = new PAIA( + $sm->getServiceLocator()->get('VuFind\DateConverter'), + $sm->getServiceLocator()->get('VuFind\SessionManager') + ); + + $paia->setCacheStorage( + $sm->getServiceLocator()->get('VuFind\CacheManager')->getCache('object') + ); + + return $paia; + } + /** * Factory for KohaILSDI driver. * diff --git a/module/VuFind/src/VuFind/ILS/Driver/PAIA.php b/module/VuFind/src/VuFind/ILS/Driver/PAIA.php new file mode 100644 index 00000000000..df079cbaaee --- /dev/null +++ b/module/VuFind/src/VuFind/ILS/Driver/PAIA.php @@ -0,0 +1,1764 @@ +<?php +/** + * PAIA ILS Driver for VuFind to get patron information + * + * PHP version 5 + * + * Copyright (C) Oliver Goldschmidt, Magda Roos, Till Kinstler, André Lahmann 2013, + * 2014, 2015. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind + * @package ILS_Drivers + * @author Oliver Goldschmidt <o.goldschmidt@tuhh.de> + * @author Magdalena Roos <roos@gbv.de> + * @author Till Kinstler <kinstler@gbv.de> + * @author André Lahmann <lahmann@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki + */ +namespace VuFind\ILS\Driver; +use VuFind\Exception\ILS as ILSException; + +/** + * PAIA ILS Driver for VuFind to get patron information + * + * Holding information is obtained by DAIA, so it's not necessary to implement those + * functions here; we just need to extend the DAIA driver. + * + * @category VuFind + * @package ILS_Drivers + * @author Oliver Goldschmidt <o.goldschmidt@tuhh.de> + * @author Magdalena Roos <roos@gbv.de> + * @author Till Kinstler <kinstler@gbv.de> + * @author André Lahmann <lahmann@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:ils_drivers Wiki + */ +class PAIA extends DAIA +{ + /** + * URL of PAIA service + * + * @var + */ + protected $paiaURL; + + /** + * Flag to switch on/off caching for PAIA items + * + * @var bool + */ + protected $paiaCacheEnabled = false; + + /** + * Session containing PAIA login information + * + * @var \Zend\Session\Container + */ + protected $session; + + /** + * SessionManager + * + * @var \VuFind\SessionManager + */ + protected $sessionManager; + + /** + * PAIA status strings + * + * @var array + */ + protected static $statusStrings = [ + '0' => 'no relation', + '1' => 'reserved', + '2' => 'ordered', + '3' => 'held', + '4' => 'provided', + '5' => 'rejected', + ]; + + /** + * PAIA constructor. + * + * @param \VuFind\Date\Converter $converter Date converter + * @param \Zend\Session\SessionManager $sessionManager Session Manager + */ + public function __construct(\VuFind\Date\Converter $converter, + \Zend\Session\SessionManager $sessionManager + ) { + parent::__construct($converter); + $this->sessionManager = $sessionManager; + } + + /** + * PAIA specific override of method to ensure uniform cache keys for cached + * VuFind objects. + * + * @param string|null $suffix Optional suffix that will get appended to the + * object class name calling getCacheKey() + * + * @return string + */ + protected function getCacheKey($suffix = null) + { + return \VuFind\ILS\Driver\AbstractBase::getCacheKey( + md5($this->baseUrl . $this->paiaURL) . $suffix + ); + } + + /** + * Get the session container (constructing it on demand if not already present) + * + * @return SessionContainer + */ + protected function getSession() + { + // SessionContainer not defined yet? Build it now: + if (null === $this->session) { + $this->session = new \Zend\Session\Container( + 'PAIA', $this->sessionManager + ); + } + return $this->session; + } + + /** + * Get the session scope + * + * @return array Array of the Session scope + */ + protected function getScope() + { + return $this->getSession()->scope; + } + + /** + * Initialize the driver. + * + * Validate configuration and perform all resource-intensive tasks needed to + * make the driver active. + * + * @throws ILSException + * @return void + */ + public function init() + { + parent::init(); + + if (!(isset($this->config['PAIA']['baseUrl']))) { + throw new ILSException('PAIA/baseUrl configuration needs to be set.'); + } + $this->paiaURL = $this->config['PAIA']['baseUrl']; + + // do we have caching enabled for PAIA + if (isset($this->config['PAIA']['paiaCache'])) { + $this->paiaCacheEnabled = $this->config['PAIA']['paiaCache']; + } else { + $this->debug('Caching not enabled, disabling it by default.'); + } + } + + // public functions implemented to satisfy Driver Interface + + /* + These methods are not implemented in the PAIA driver as they are probably + not necessary in PAIA context: + - findReserves + - getCancelHoldLink + - getConsortialHoldings + - getCourses + - getDepartments + - getHoldDefaultRequiredDate + - getInstructors + - getOfflineMode + - getSuppressedAuthorityRecords + - getSuppressedRecords + - hasHoldings + - loginIsHidden + - renewMyItemsLink + - supportsMethod + */ + + /** + * This method cancels a list of holds for a specific patron. + * + * @param array $cancelDetails An associative array with two keys: + * patron array returned by the driver's patronLogin method + * details an array of strings returned by the driver's + * getCancelHoldDetails method + * + * @return array Associative array containing: + * count The number of items successfully cancelled + * items Associative array where key matches one of the item_id + * values returned by getMyHolds and the value is an + * associative array with these keys: + * success Boolean true or false + * status A status message from the language file + * (required – VuFind-specific message, + * subject to translation) + * sysMessage A system supplied failure message + */ + public function cancelHolds($cancelDetails) + { + $it = $cancelDetails['details']; + $items = []; + foreach ($it as $item) { + $items[] = ['item' => stripslashes($item)]; + } + $patron = $cancelDetails['patron']; + $post_data = ["doc" => $items]; + + try { + $array_response = $this->paiaPostAsArray( + 'core/' . $patron['cat_username'] . '/cancel', $post_data + ); + } catch (ILSException $e) { + $this->debug($e->getMessage()); + return [ + 'success' => false, + 'status' => $e->getMessage(), + ]; + } + + $details = []; + + if (isset($array_response['error'])) { + $details[] = [ + 'success' => false, + 'status' => $array_response['error_description'], + 'sysMessage' => $array_response['error'] + ]; + } else { + $count = 0; + $elements = $array_response['doc']; + foreach ($elements as $element) { + $item_id = $element['item']; + if ($element['error']) { + $details[$item_id] = [ + 'success' => false, + 'status' => $element['error'], + 'sysMessage' => 'Cancel request rejected' + ]; + } else { + $details[$item_id] = [ + 'success' => true, + 'status' => 'Success', + 'sysMessage' => 'Successfully cancelled' + ]; + $count++; + + // DAIA cache cannot be cleared for particular item as PAIA only + // operates with specific item URIs and the DAIA cache is setup + // by doc URIs (containing items with URIs) + } + } + + // If caching is enabled for PAIA clear the cache as at least for one + // item cancel was successfull and therefore the status changed. + // Otherwise the changed status will not be shown before the cache + // expires. + if ($this->paiaCacheEnabled) { + $this->removeCachedData($patron['cat_username']); + } + } + $returnArray = ['count' => $count, 'items' => $details]; + + return $returnArray; + } + + /** + * Public Function which changes the password in the library system + * (not supported prior to VuFind 2.4) + * + * @param array $details Array with patron information, newPassword and + * oldPassword. + * + * @return array An array with patron information. + */ + public function changePassword($details) + { + $post_data = [ + "patron" => $details['patron']['cat_username'], + "username" => $details['patron']['cat_username'], + "old_password" => $details['oldPassword'], + "new_password" => $details['newPassword'] + ]; + + try { + $array_response = $this->paiaPostAsArray( + 'auth/change', $post_data + ); + } catch (ILSException $e) { + $this->debug($e->getMessage()); + return [ + 'success' => false, + 'status' => $e->getMessage(), + ]; + } + + $details = []; + + if (isset($array_response['error'])) { + // on error + $details = [ + 'success' => false, + 'status' => $array_response['error'], + 'sysMessage' => + isset($array_response['error']) + ? $array_response['error'] : ' ' . + isset($array_response['error_description']) + ? $array_response['error_description'] : ' ' + ]; + } elseif ($array_response['patron'] === $post_data['patron']) { + // on success patron_id is returned + $details = [ + 'success' => true, + 'status' => 'Successfully changed' + ]; + } else { + $details = [ + 'success' => false, + 'status' => 'Failure changing password', + 'sysMessage' => serialize($array_response) + ]; + } + return $details; + } + + /** + * This method returns a string to use as the input form value for + * cancelling each hold item. (optional, but required if you + * implement cancelHolds). Not supported prior to VuFind 1.2 + * + * @param array $checkOutDetails One of the individual item arrays returned by + * the getMyHolds method + * + * @return string A string to use as the input form value for cancelling + * each hold item; you can pass any data that is needed + * by your ILS to identify the hold – the output of this + * method will be used as part of the input to the + * cancelHolds method. + */ + public function getCancelHoldDetails($checkOutDetails) + { + return($checkOutDetails['cancel_details']); + } + + /** + * Get Default Pick Up Location + * + * @param array $patron Patron information returned by the patronLogin + * method. + * @param array $holdDetails Optional array, only passed in when getting a list + * in the context of placing a hold; contains most of the same values passed to + * placeHold, minus the patron data. May be used to limit the pickup options + * or may be ignored. + * + * @return string The default pickup location for the patron. + */ + public function getDefaultPickUpLocation($patron = null, $holdDetails = null) + { + return false; + } + + /** + * Get Funds + * + * Return a list of funds which may be used to limit the getNewItems list. + * + * @return array An associative array with key = fund ID, value = fund name. + */ + public function getFunds() + { + // If you do not want or support such limits, just return an empty + // array here and the limit control on the new item search screen + // will disappear. + return []; + } + + /** + * Cancel Storage Retrieval Request + * + * Attempts to Cancel a Storage Retrieval Request 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 and a system message (if available) + */ + public function cancelStorageRetrievalRequests($cancelDetails) + { + // Not yet implemented + return []; + } + + /** + * Get Cancel Storage Retrieval Request Details + * + * In order to cancel a hold, Voyager requires the patron details an item ID + * and a recall ID. 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. + * + * @param array $details An array of item data + * + * @return string Data for use in a form field + */ + public function getCancelStorageRetrievalRequestDetails($details) + { + // Not yet implemented + return ''; + } + + /** + * Get Patron ILL Requests + * + * This is responsible for retrieving all ILL requests by a specific patron. + * + * @param array $patron The patron array from patronLogin + * + * @return mixed Array of the patron's ILL requests + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getMyILLRequests($patron) + { + // Not yet implemented + return []; + } + + /** + * Check if ILL request available + * + * This is responsible for determining if an item is requestable + * + * @param string $id The Bib ID + * @param array $data An Array of item data + * @param patron $patron An array of patron data + * + * @return bool True if request is valid, false if not + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function checkILLRequestIsValid($id, $data, $patron) + { + // Not yet implemented + return false; + } + /** + * Place ILL Request + * + * Attempts to place an ILL 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 and a system message (if available) + */ + public function placeILLRequest($details) + { + // Not yet implemented + return []; + } + + /** + * Get ILL Pickup Libraries + * + * This is responsible for getting information on the possible pickup libraries + * + * @param string $id Record ID + * @param array $patron Patron + * + * @return bool|array False if request not allowed, or an array of associative + * arrays with libraries. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getILLPickupLibraries($id, $patron) + { + // Not yet implemented + return false; + } + + /** + * Get ILL Pickup Locations + * + * This is responsible for getting a list of possible pickup locations for a + * library + * + * @param string $id Record ID + * @param string $pickupLib Pickup library ID + * @param array $patron Patron + * + * @return bool|array False if request not allowed, or an array of locations. + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function getILLPickupLocations($id, $pickupLib, $patron) + { + // Not yet implemented + return false; + } + + /** + * Cancel ILL Request + * + * Attempts to Cancel an ILL request on a particular item. The + * data in $cancelDetails['details'] is determined by + * getCancelILLRequestDetails(). + * + * @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 and a system message (if available) + */ + public function cancelILLRequests($cancelDetails) + { + // Not yet implemented + return []; + } + + /** + * Get Cancel ILL Request Details + * + * @param array $details An array of item data + * + * @return string Data for use in a form field + */ + public function getCancelILLRequestDetails($details) + { + // Not yet implemented + return ''; + } + + /** + * Get Patron Fines + * + * This is responsible for retrieving all fines by a specific patron. + * + * @param array $patron The patron array from patronLogin + * + * @return mixed Array of the patron's fines on success + */ + public function getMyFines($patron) + { + $fees = $this->paiaGetAsArray( + 'core/' . $patron['cat_username'] . '/fees' + ); + + // PAIA simple data type money: a monetary value with currency (format + // [0-9]+\.[0-9][0-9] [A-Z][A-Z][A-Z]), for instance 0.80 USD. + $feeConverter = function ($fee) { + $paiaCurrencyPattern = "/^([0-9]+\.[0-9][0-9]) ([A-Z][A-Z][A-Z])$/"; + if (preg_match($paiaCurrencyPattern, $fee, $feeMatches)) { + // VuFind expects fees in PENNIES + return ($feeMatches[1] * 100); + } + return $fee; + }; + + $results = []; + if (isset($fees['fee'])) { + foreach ($fees['fee'] as $fee) { + $result = [ + // fee.amount 1..1 money amount of a single fee + 'amount' => $feeConverter($fee['amount']), + 'checkout' => '', + // fee.feetype 0..1 string textual description of the type + // of service that caused the fee + 'fine' => (isset($fee['feetype']) ? $fee['feetype'] : null), + 'balance' => $feeConverter($fee['amount']), + // fee.date 0..1 date date when the fee was claimed + 'createdate' => (isset($fee['date']) + ? $this->convertDate($fee['date']) : null), + 'duedate' => '', + // fee.edition 0..1 URI edition that caused the fee + 'id' => (isset($fee['edition']) + ? $this->getAlternativeItemId($fee['edition']) : ''), + ]; + // custom PAIA fields can get added in getAdditionalFeeData + $results[] = $result + $this->getAdditionalFeeData($fee, $patron); + } + } + return $results; + } + + /** + * Gets additional array fields for the item. + * Override this method in your custom PAIA driver if necessary. + * + * @param array $fee The fee array from PAIA + * @param array $patron The patron array from patronLogin + * + * @return array Additional fee data for the item + */ + protected function getAdditionalFeeData($fee, $patron = null) + { + $additionalData = []; + // Add the item title using the about field, + // but only if this fee is caused by some item + if (isset($fee['item'])) { + $additionalData['title'] = $fee['about']; + } + + // custom PAIA fields + // fee.about 0..1 string textual information about the fee + // fee.item 0..1 URI item that caused the fee + // fee.feeid 0..1 URI URI of the type of service that + // caused the fee + $additionalData['feeid'] = (isset($fee['feeid']) + ? $fee['feeid'] : null); + $additionalData['about'] = (isset($fee['about']) + ? $fee['about'] : null); + $additionalData['item'] = (isset($fee['item']) + ? $fee['item'] : null); + $additionalData['title'] = (isset($fee['title']) + ? $fee['title'] : null); + + return $additionalData; + } + + /** + * Get Patron Holds + * + * This is responsible for retrieving all holds by a specific patron. + * + * @param array $patron The patron array from patronLogin + * + * @return mixed Array of the patron's holds on success. + */ + public function getMyHolds($patron) + { + // filters for getMyHolds are: + // status = 1 - reserved (the document is not accessible for the patron yet, + // but it will be) + // 4 - provided (the document is ready to be used by the patron) + $filter = ['status' => [1, 4]]; + // get items-docs for given filters + $items = $this->paiaGetItems($patron, $filter); + + return $this->mapPaiaItems($items, 'myHoldsMapping'); + } + + /** + * Get Patron Profile + * + * This is responsible for retrieving the profile for a specific patron. + * + * @param array $patron The patron array + * + * @return array Array of the patron's profile data on success, + */ + public function getMyProfile($patron) + { + //todo: read VCard if avaiable in patron info + //todo: make fields more configurable + if (is_array($patron)) { + return [ + 'firstname' => $patron['firstname'], + 'lastname' => $patron['lastname'], + 'address1' => null, + 'address2' => null, + 'city' => null, + 'country' => null, + 'zip' => null, + 'phone' => null, + 'group' => null, + // PAIA specific custom values + 'expires' => isset($patron['expires']) + ? $this->convertDate($patron['expires']) : null, + 'statuscode' => isset($patron['status']) ? $patron['status'] : null, + 'canWrite' => in_array('write_items', $this->getScope()), + ]; + } + return []; + } + + /** + * Get Patron Transactions + * + * This is responsible for retrieving all transactions (i.e. checked out items) + * by a specific patron. + * + * @param array $patron The patron array from patronLogin + * + * @return array Array of the patron's transactions on success, + */ + public function getMyTransactions($patron) + { + // filters for getMyTransactions are: + // status = 3 - held (the document is on loan by the patron) + $filter = ['status' => [3]]; + // get items-docs for given filters + $items = $this->paiaGetItems($patron, $filter); + + return $this->mapPaiaItems($items, 'myTransactionsMapping'); + } + + /** + * Get Patron StorageRetrievalRequests + * + * This is responsible for retrieving all storage retrieval requests + * by a specific patron. + * + * @param array $patron The patron array from patronLogin + * + * @return array Array of the patron's storage retrieval requests on success, + */ + public function getMyStorageRetrievalRequests($patron) + { + // filters for getMyStorageRetrievalRequests are: + // status = 2 - ordered (the document is ordered by the patron) + $filter = ['status' => [2]]; + // get items-docs for given filters + $items = $this->paiaGetItems($patron, $filter); + + return $this->mapPaiaItems($items, 'myStorageRetrievalRequestsMapping'); + } + + /** + * This method queries the ILS for new items + * + * @param string $page page number of results to retrieve (counting starts @1) + * @param string $limit the size of each page of results to retrieve + * @param string $daysOld the maximum age of records to retrieve in days (max 30) + * @param string $fundID optional fund ID to use for limiting results + * + * @return array An associative array with two keys: 'count' (the number of items + * in the 'results' array) and 'results' (an array of associative arrays, each + * with a single key: 'id', a record ID). + */ + public function getNewItems($page, $limit, $daysOld, $fundID) + { + return []; + } + + /** + * Get Pick Up Locations + * + * This is responsible for gettting a list of valid library locations for + * holds / recall retrieval + * + * @param array $patron Patron information returned by the patronLogin + * method. + * @param array $holdDetails Optional array, only passed in when getting a list + * in the context of placing a hold; contains most of the same values passed to + * placeHold, minus the patron data. May be used to limit the pickup options + * or may be ignored. The driver must not add new options to the return array + * based on this data or other areas of VuFind may behave incorrectly. + * + * @return array An array of associative arrays with locationID and + * locationDisplay keys + */ + public function getPickUpLocations($patron = null, $holdDetails = null) + { + // How to get valid PickupLocations for a PICA LBS? + return []; + } + + /** + * This method returns a string to use as the input form value for renewing + * each hold item. (optional, but required if you implement the + * renewMyItems method) Not supported prior to VuFind 1.2 + * + * @param array $checkOutDetails One of the individual item arrays returned by + * the getMyTransactions method + * + * @return string A string to use as the input form value for renewing + * each item; you can pass any data that is needed by your + * ILS to identify the transaction to renew – the output + * of this method will be used as part of the input to the + * renewMyItems method. + */ + public function getRenewDetails($checkOutDetails) + { + return($checkOutDetails['renew_details']); + } + + /** + * Get the callnumber of this item + * + * @param array $doc Array of PAIA item. + * + * @return String + */ + protected function getCallNumber($doc) + { + return isset($doc['label']) ? $doc['label'] : null; + } + + /** + * Patron Login + * + * This is responsible for authenticating a patron against the catalog. + * + * @param string $username The patron's username + * @param string $password The patron's login password + * + * @return mixed Associative array of patron info on successful login, + * null on unsuccessful login. + * + * @throws ILSException + */ + public function patronLogin($username, $password) + { + if ($username == '' || $password == '') { + throw new ILSException('Invalid Login, Please try again.'); + } + + $session = $this->getSession(); + + // if we already have a session with access_token and patron id, try to get + // patron info with session data + if (isset($session->expires) && $session->expires > time()) { + try { + return $this->enrichUserDetails( + $this->paiaGetUserDetails($session->patron), + $password + ); + } catch (ILSException $e) { + $this->debug('Session expired, login again', ['info' => 'info']); + } + } + try { + if ($this->paiaLogin($username, $password)) { + return $this->enrichUserDetails( + $this->paiaGetUserDetails($session->patron), + $password + ); + } + } catch (ILSException $e) { + throw new ILSException($e->getMessage()); + } + } + + /** + * PAIA helper function to map session data to return value of patronLogin() + * + * @param array $details Patron details returned by patronLogin + * @param string $password Patron cataloge password + * + * @return mixed + */ + protected function enrichUserDetails($details, $password) + { + $session = $this->getSession(); + + $details['cat_username'] = $session->patron; + $details['cat_password'] = $password; + return $details; + } + + /** + * Returns an array with PAIA confirmations based on the given holdDetails which + * will be used for a request. + * Currently two condition types are supported: + * - http://purl.org/ontology/paia#StorageCondition to select a document + * location -- mapped to pickUpLocation + * - http://purl.org/ontology/paia#FeeCondition to confirm or select a document + * service causing a fee -- not mapped yet + * + * @param array $holdDetails An array of item and patron data + * + * @return array + */ + protected function getConfirmations($holdDetails) + { + $confirmations = []; + if (isset($holdDetails['pickUpLocation'])) { + $confirmations['http://purl.org/ontology/paia#StorageCondition'] + = [$holdDetails['pickUpLocation']]; + } + return $confirmations; + } + + /** + * Place Hold + * + * Attempts to place a hold or recall on a particular item and returns + * an array with result details + * + * Make a request on a specific record + * + * @param array $holdDetails An array of item and patron data + * + * @return mixed An array of data on the request including + * whether or not it was successful and a system message (if available) + */ + public function placeHold($holdDetails) + { + $item = $holdDetails['item_id']; + $patron = $holdDetails['patron']; + + $doc = []; + $doc['item'] = stripslashes($item); + if ($confirm = $this->getConfirmations($holdDetails)) { + $doc["confirm"] = $confirm; + } + $post_data['doc'][] = $doc; + + try { + $array_response = $this->paiaPostAsArray( + 'core/' . $patron['cat_username'] . '/request', $post_data + ); + } catch (ILSException $e) { + $this->debug($e->getMessage()); + return [ + 'success' => false, + 'sysMessage' => $e->getMessage(), + ]; + } + + $details = []; + if (isset($array_response['error'])) { + $details = [ + 'success' => false, + 'sysMessage' => $array_response['error_description'] + ]; + } else { + $elements = $array_response['doc']; + foreach ($elements as $element) { + if (isset($element['error'])) { + $details = [ + 'success' => false, + 'sysMessage' => $element['error'] + ]; + } else { + $details = [ + 'success' => true, + 'sysMessage' => 'Successfully requested' + ]; + // if caching is enabled for DAIA remove the cached data for the + // current item otherwise the changed status will not be shown + // before the cache expires + if ($this->daiaCacheEnabled) { + $this->removeCachedData($holdDetails['doc_id']); + } + } + } + } + return $details; + } + + /** + * Place a Storage Retrieval Request + * + * Attempts to place a 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 and a system message (if available) + */ + public function placeStorageRetrievalRequest($details) + { + // Making a storage retrieval request is the same in PAIA as placing a Hold + return $this->placeHold($details); + } + + /** + * This method renews a list of items for a specific patron. + * + * @param array $details - An associative array with two keys: + * patron - array returned by patronLogin method + * details - array of values returned by the getRenewDetails method + * identifying which items to renew + * + * @return array - An associative array with two keys: + * blocks - An array of strings specifying why a user is blocked from + * renewing (false if no blocks) + * details - Not set when blocks exist; otherwise, an array of + * associative arrays (keyed by item ID) with each subarray + * containing these keys: + * success – Boolean true or false + * new_date – string – A new due date + * new_time – string – A new due time + * item_id – The item id of the renewed item + * sysMessage – A system supplied renewal message (optional) + */ + public function renewMyItems($details) + { + $it = $details['details']; + $items = []; + foreach ($it as $item) { + $items[] = ['item' => stripslashes($item)]; + } + $patron = $details['patron']; + $post_data = ["doc" => $items]; + + try { + $array_response = $this->paiaPostAsArray( + 'core/' . $patron['cat_username'] . '/renew', $post_data + ); + } catch (ILSException $e) { + $this->debug($e->getMessage()); + return [ + 'success' => false, + 'sysMessage' => $e->getMessage(), + ]; + } + + $details = []; + + if (isset($array_response['error'])) { + $details[] = [ + 'success' => false, + 'sysMessage' => $array_response['error_description'] + ]; + } else { + $elements = $array_response['doc']; + foreach ($elements as $element) { + // VuFind can only assign the response to an id - if none is given + // (which is possible) simply skip this response element + if (isset($element['item'])) { + if (isset($element['error'])) { + $details[$element['item']] = [ + 'success' => false, + 'sysMessage' => $element['error'] + ]; + } elseif ($element['status'] == '3') { + $details[$element['item']] = [ + 'success' => true, + 'new_date' => isset($element['endtime']) + ? $this->convertDatetime($element['endtime']) : '', + 'item_id' => 0, + 'sysMessage' => 'Successfully renewed' + ]; + } else { + $details[$element['item']] = [ + 'success' => false, + 'item_id' => 0, + 'new_date' => isset($element['endtime']) + ? $this->convertDatetime($element['endtime']) : '', + 'sysMessage' => 'Request rejected' + ]; + } + } + + // DAIA cache cannot be cleared for particular item as PAIA only + // operates with specific item URIs and the DAIA cache is setup + // by doc URIs (containing items with URIs) + } + + // If caching is enabled for PAIA clear the cache as at least for one + // item renew was successfull and therefore the status changed. Otherwise + // the changed status will not be shown before the cache expires. + if ($this->paiaCacheEnabled) { + $this->removeCachedData($patron['cat_username']); + } + } + $returnArray = ['blocks' => false, 'details' => $details]; + return $returnArray; + } + + /* + * PAIA functions + */ + + /** + * PAIA support method to return strings for PAIA service status values + * + * @param string $status PAIA service status + * + * @return string Describing PAIA service status + */ + protected function paiaStatusString($status) + { + return isset(self::$statusStrings[$status]) + ? self::$statusStrings[$status] : ''; + } + + /** + * PAIA support method for PAIA core method 'items' returning only those + * documents containing the given service status. + * + * @param array $patron Array with patron information + * @param array $filter Array of properties identifying the wanted items + * + * @return array|mixed Array of documents containing the given filter properties + */ + protected function paiaGetItems($patron, $filter = []) + { + // check for existing data in cache + if ($this->paiaCacheEnabled) { + $itemsResponse = $this->getCachedData($patron['cat_username']); + } + + if (!isset($itemsResponse) || $itemsResponse == null) { + $itemsResponse = $this->paiaGetAsArray( + 'core/' . $patron['cat_username'] . '/items' + ); + if ($this->paiaCacheEnabled) { + $this->putCachedData($patron['cat_username'], $itemsResponse); + } + } + + if (isset($itemsResponse['doc'])) { + if (count($filter)) { + $filteredItems = []; + foreach ($itemsResponse['doc'] as $doc) { + $filterCounter = 0; + foreach ($filter as $filterKey => $filterValue) { + if (isset($doc[$filterKey]) + && in_array($doc[$filterKey], (array)$filterValue) + ) { + $filterCounter++; + } + } + if ($filterCounter == count($filter)) { + $filteredItems[] = $doc; + } + } + return $filteredItems; + } else { + return $itemsResponse; + } + } else { + $this->debug( + "No documents found in PAIA response. Returning empty array." + ); + } + return []; + } + + /** + * PAIA support method to retrieve needed ItemId in case PAIA-response does not + * contain it + * + * @param string $id itemId + * + * @return string $id + */ + protected function getAlternativeItemId($id) + { + return $id; + } + + /** + * PAIA support function to implement ILS specific parsing of user_details + * + * @param string $patron User id + * @param array $user_response Array with PAIA response data + * + * @return array + */ + protected function paiaParseUserDetails($patron, $user_response) + { + $username = trim($user_response['name']); + if (count(explode(',', $username)) == 2) { + $nameArr = explode(',', $username); + $firstname = $nameArr[1]; + $lastname = $nameArr[0]; + } else { + $nameArr = explode(' ', $username); + $firstname = $nameArr[0]; + $lastname = ''; + array_shift($nameArr); + foreach ($nameArr as $value) { + $lastname .= ' ' . $value; + } + $lastname = trim($lastname); + } + + // TODO: implement parsing of user details according to types set + // (cf. https://github.com/gbv/paia/issues/29) + + $user = []; + $user['id'] = $patron; + $user['firstname'] = $firstname; + $user['lastname'] = $lastname; + $user['email'] = (isset($user_response['email']) + ? $user_response['email'] : ''); + $user['major'] = null; + $user['college'] = null; + // add other information from PAIA - we don't want anything to get lost + // while parsing + foreach ($user_response as $key => $value) { + if (!isset($user[$key])) { + $user[$key] = $value; + } + } + return $user; + } + + /** + * PAIA helper function to allow customization of mapping from PAIA response to + * VuFind ILS-method return values. + * + * @param array $items Array of PAIA items to be mapped + * @param string $mapping String identifying a custom mapping-method + * + * @return array + */ + protected function mapPaiaItems($items, $mapping) + { + if (is_callable([$this, $mapping])) { + return $this->$mapping($items); + } + + $this->debug('Could not call method: ' . $mapping . '() .'); + return []; + } + + /** + * This PAIA helper function allows custom overrides for mapping of PAIA response + * to getMyHolds data structure. + * + * @param array $items Array of PAIA items to be mapped. + * + * @return array + */ + protected function myHoldsMapping($items) + { + $results = []; + + foreach ($items as $doc) { + $result = []; + + // item (0..1) URI of a particular copy + $result['item_id'] = (isset($doc['item']) ? $doc['item'] : ''); + + $result['cancel_details'] + = (isset($doc['cancancel']) && $doc['cancancel']) + ? $result['item_id'] : ''; + + // edition (0..1) URI of a the document (no particular copy) + // hook for retrieving alternative ItemId in case PAIA does not + // the needed id + $result['id'] = (isset($doc['edition']) + ? $this->getAlternativeItemId($doc['edition']) : ''); + + $result['type'] = $this->paiaStatusString($doc['status']); + + // storage (0..1) textual description of location of the document + $result['location'] = (isset($doc['storage']) ? $doc['storage'] : null); + + // queue (0..1) number of waiting requests for the document or item + $result['position'] = (isset($doc['queue']) ? $doc['queue'] : null); + + // only true if status == 4 + $result['available'] = false; + + // about (0..1) textual description of the document + $result['title'] = (isset($doc['about']) ? $doc['about'] : null); + + // PAIA custom field + // label (0..1) call number, shelf mark or similar item label + $result['callnumber'] = $this->getCallNumber($doc); + + /* + * meaning of starttime and endtime depends on status: + * + * status | starttime + * | endtime + * -------+-------------------------------- + * 0 | - + * | - + * 1 | when the document was reserved + * | when the reserved document is expected to be available + * 2 | when the document was ordered + * | when the ordered document is expected to be available + * 3 | when the document was lend + * | when the loan period ends or ended (due) + * 4 | when the document is provided + * | when the provision will expire + * 5 | when the request was rejected + * | - + */ + + $result['create'] = (isset($doc['starttime']) + ? $this->convertDatetime($doc['starttime']) : ''); + + if ($doc['status'] == '4') { + $result['expire'] = (isset($doc['endtime']) + ? $this->convertDatetime($doc['endtime']) : ''); + } else { + $result['duedate'] = (isset($doc['endtime']) + ? $this->convertDatetime($doc['endtime']) : ''); + } + + // status: provided (the document is ready to be used by the patron) + $result['available'] = $doc['status'] == 4 ? true : false; + + // Optional VuFind fields + /* + $result['reqnum'] = null; + $result['volume'] = null; + $result['publication_year'] = null; + $result['isbn'] = null; + $result['issn'] = null; + $result['oclc'] = null; + $result['upc'] = null; + */ + + $results[] = $result; + + } + return $results; + } + + /** + * This PAIA helper function allows custom overrides for mapping of PAIA response + * to getMyStorageRetrievalRequests data structure. + * + * @param array $items Array of PAIA items to be mapped. + * + * @return array + */ + protected function myStorageRetrievalRequestsMapping($items) + { + $results = []; + + foreach ($items as $doc) { + $result = []; + + // item (0..1) URI of a particular copy + $result['item_id'] = (isset($doc['item']) ? $doc['item'] : ''); + + $result['cancel_details'] + = (isset($doc['cancancel']) && $doc['cancancel']) + ? $result['item_id'] : ''; + + // edition (0..1) URI of a the document (no particular copy) + // hook for retrieving alternative ItemId in case PAIA does not + // the needed id + $result['id'] = (isset($doc['edition']) + ? $this->getAlternativeItemId($doc['edition']) : ''); + + $result['type'] = $this->paiaStatusString($doc['status']); + + // storage (0..1) textual description of location of the document + $result['location'] = (isset($doc['storage']) ? $doc['storage'] : null); + + // queue (0..1) number of waiting requests for the document or item + $result['position'] = (isset($doc['queue']) ? $doc['queue'] : null); + + // only true if status == 4 + $result['available'] = false; + + // about (0..1) textual description of the document + $result['title'] = (isset($doc['about']) ? $doc['about'] : null); + + // PAIA custom field + // label (0..1) call number, shelf mark or similar item label + $result['callnumber'] = $this->getCallNumber($doc); + + $result['create'] = (isset($doc['starttime']) + ? $this->convertDatetime($doc['starttime']) : ''); + + // Optional VuFind fields + /* + $result['reqnum'] = null; + $result['volume'] = null; + $result['publication_year'] = null; + $result['isbn'] = null; + $result['issn'] = null; + $result['oclc'] = null; + $result['upc'] = null; + */ + + $results[] = $result; + + } + return $results; + } + + /** + * This PAIA helper function allows custom overrides for mapping of PAIA response + * to getMyTransactions data structure. + * + * @param array $items Array of PAIA items to be mapped. + * + * @return array + */ + protected function myTransactionsMapping($items) + { + $results = []; + + foreach ($items as $doc) { + $result = []; + // canrenew (0..1) whether a document can be renewed (bool) + $result['renewable'] = (isset($doc['canrenew']) + ? $doc['canrenew'] : false); + + // item (0..1) URI of a particular copy + $result['item_id'] = (isset($doc['item']) ? $doc['item'] : ''); + + $result['renew_details'] + = (isset($doc['canrenew']) && $doc['canrenew']) + ? $result['item_id'] : ''; + + // edition (0..1) URI of a the document (no particular copy) + // hook for retrieving alternative ItemId in case PAIA does not + // the needed id + $result['id'] = (isset($doc['edition']) + ? $this->getAlternativeItemId($doc['edition']) : ''); + + // requested (0..1) URI that was originally requested + + // about (0..1) textual description of the document + $result['title'] = (isset($doc['about']) ? $doc['about'] : null); + + // queue (0..1) number of waiting requests for the document or item + $result['request'] = (isset($doc['queue']) ? $doc['queue'] : null); + + // renewals (0..1) number of times the document has been renewed + $result['renew'] = (isset($doc['renewals']) ? $doc['renewals'] : null); + + // reminder (0..1) number of times the patron has been reminded + $result['reminder'] = ( + isset($doc['reminder']) ? $doc['reminder'] : null + ); + + // custom PAIA field + // starttime (0..1) date and time when the status began + $result['startTime'] = (isset($doc['starttime']) + ? $this->convertDatetime($doc['starttime']) : ''); + + // endtime (0..1) date and time when the status will expire + $result['dueTime'] = (isset($doc['endtime']) + ? $this->convertDatetime($doc['endtime']) : ''); + + // duedate (0..1) date when the current status will expire (deprecated) + $result['duedate'] = (isset($doc['duedate']) + ? $this->convertDate($doc['duedate']) : ''); + + // cancancel (0..1) whether an ordered or provided document can be + // canceled + + // error (0..1) error message, for instance if a request was rejected + $result['message'] = (isset($doc['error']) ? $doc['error'] : ''); + + // storage (0..1) textual description of location of the document + $result['borrowingLocation'] = (isset($doc['storage']) + ? $doc['storage'] : ''); + + // storageid (0..1) location URI + + // PAIA custom field + // label (0..1) call number, shelf mark or similar item label + $result['callnumber'] = $this->getCallNumber($doc); + + // Optional VuFind fields + /* + $result['barcode'] = null; + $result['dueStatus'] = null; + $result['renewLimit'] = "1"; + $result['volume'] = null; + $result['publication_year'] = null; + $result['isbn'] = null; + $result['issn'] = null; + $result['oclc'] = null; + $result['upc'] = null; + $result['institution_name'] = null; + */ + + $results[] = $result; + } + + return $results; + } + + /** + * Post something to a foreign host + * + * @param string $file POST target URL + * @param string $data_to_send POST data + * @param string $access_token PAIA access token for current session + * + * @return string POST response + * @throws ILSException + */ + protected function paiaPostRequest($file, $data_to_send, $access_token = null) + { + // json-encoding + $postData = stripslashes(json_encode($data_to_send)); + + $http_headers = []; + if (isset($access_token)) { + $http_headers['Authorization'] = 'Bearer ' . $access_token; + } + + try { + $result = $this->httpService->post( + $this->paiaURL . $file, + $postData, + 'application/json; charset=UTF-8', + null, + $http_headers + ); + } catch (\Exception $e) { + throw new ILSException($e->getMessage()); + } + + if (!$result->isSuccess()) { + // log error for debugging + $this->debug( + 'HTTP status ' . $result->getStatusCode() . + ' received' + ); + } + // return any result as error-handling is done elsewhere + return ($result->getBody()); + } + + /** + * GET data from foreign host + * + * @param string $file GET target URL + * @param string $access_token PAIA access token for current session + * + * @return bool|string + * @throws ILSException + */ + protected function paiaGetRequest($file, $access_token) + { + $http_headers = [ + 'Authorization' => 'Bearer ' . $access_token, + 'Content-type' => 'application/json; charset=UTF-8', + ]; + + try { + $result = $this->httpService->get( + $this->paiaURL . $file, + [], null, $http_headers + ); + } catch (\Exception $e) { + throw new ILSException($e->getMessage()); + } + + if (!$result->isSuccess()) { + // log error for debugging + $this->debug( + 'HTTP status ' . $result->getStatusCode() . + ' received' + ); + } + // return any result as error-handling is done elsewhere + return ($result->getBody()); + } + + /** + * Helper function for PAIA to uniformely parse JSON + * + * @param string $file JSON data + * + * @return mixed + * @throws ILSException + */ + protected function paiaParseJsonAsArray($file) + { + $responseArray = json_decode($file, true); + + if (isset($responseArray['error'])) { + throw new ILSException( + $responseArray['error'], + $responseArray['code'] + ); + } + + return $responseArray; + } + + /** + * Retrieve file at given URL and return it as json_decoded array + * + * @param string $file GET target URL + * + * @return array|mixed + * @throws ILSException + */ + protected function paiaGetAsArray($file) + { + $responseJson = $this->paiaGetRequest( + $file, + $this->getSession()->access_token + ); + + try { + $responseArray = $this->paiaParseJsonAsArray($responseJson); + } catch (ILSException $e) { + $this->debug($e->getCode() . ':' . $e->getMessage()); + return []; + } + + return $responseArray; + } + + /** + * Post something at given URL and return it as json_decoded array + * + * @param string $file POST target URL + * @param array $data POST data + * + * @return array|mixed + * @throws ILSException + */ + protected function paiaPostAsArray($file, $data) + { + $responseJson = $this->paiaPostRequest( + $file, + $data, + $this->getSession()->access_token + ); + + try { + $responseArray = $this->paiaParseJsonAsArray($responseJson); + } catch (ILSException $e) { + $this->debug($e->getCode() . ':' . $e->getMessage()); + /* TODO: do not return empty array, this causes eventually confusion */ + return []; + } + + return $responseArray; + } + + /** + * PAIA authentication function + * + * @param string $username Username + * @param string $password Password + * + * @return mixed Associative array of patron info on successful login, + * null on unsuccessful login, PEAR_Error on error. + * @throws ILSException + */ + protected function paiaLogin($username, $password) + { + // perform full PAIA auth and get patron info + $post_data = [ + "username" => $username, + "password" => $password, + "grant_type" => "password", + "scope" => "read_patron read_fees read_items write_items " . + "change_password" + ]; + $responseJson = $this->paiaPostRequest('auth/login', $post_data); + + try { + $responseArray = $this->paiaParseJsonAsArray($responseJson); + } catch (ILSException $e) { + if ($e->getMessage() === 'access_denied') { + return false; + } + throw new ILSException( + $e->getCode() . ':' . $e->getMessage() + ); + } + + if (!isset($responseArray['access_token'])) { + throw new ILSException( + 'Unknown error! Access denied.' + ); + } elseif (!isset($responseArray['patron'])) { + throw new ILSException( + 'Login credentials accepted, but got no patron ID?!?' + ); + } else { + // at least access_token and patron got returned which is sufficient for + // us, now save all to session + $session = $this->getSession(); + + $session->patron + = isset($responseArray['patron']) + ? $responseArray['patron'] : null; + $session->access_token + = isset($responseArray['access_token']) + ? $responseArray['access_token'] : null; + $session->scope + = isset($responseArray['scope']) + ? explode(' ', $responseArray['scope']) : null; + $session->expires + = isset($responseArray['expires_in']) + ? (time() + ($responseArray['expires_in'])) : null; + + return true; + } + } + + /** + * Support method for paiaLogin() -- load user details into session and return + * array of basic user data. + * + * @param array $patron patron ID + * + * @return array + * @throws ILSException + */ + protected function paiaGetUserDetails($patron) + { + $responseJson = $this->paiaGetRequest( + 'core/' . $patron, $this->getSession()->access_token + ); + + try { + $responseArray = $this->paiaParseJsonAsArray($responseJson); + } catch (ILSException $e) { + throw new ILSException( + $e->getMessage(), $e->getCode() + ); + } + return $this->paiaParseUserDetails($patron, $responseArray); + } + + /** + * Check if storage retrieval request available + * + * This is responsible for determining if an item is requestable + * + * @param string $id The Bib ID + * @param array $data An Array of item data + * @param patron $patron An array of patron data + * + * @return bool True if request is valid, false if not + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function checkStorageRetrievalRequestIsValid($id, $data, $patron) + { + return $this->checkRequestIsValid($id, $data, $patron); + } + + /** + * Check if hold or recall available + * + * This is responsible for determining if an item is requestable + * + * @param string $id The Bib ID + * @param array $data An Array of item data + * @param patron $patron An array of patron data + * + * @return bool True if request is valid, false if not + * + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function checkRequestIsValid($id, $data, $patron) + { + // TODO: make this more configurable + if (isset($patron['status']) && $patron['status'] == 0 + && isset($patron['expires']) && $patron['expires'] > date('Y-m-d') + && in_array('write_items', $this->getScope()) + ) { + return true; + } + return false; + } +} diff --git a/module/VuFind/tests/fixtures/paia/response/changePassword.json b/module/VuFind/tests/fixtures/paia/response/changePassword.json new file mode 100644 index 00000000000..3a1981450d9 --- /dev/null +++ b/module/VuFind/tests/fixtures/paia/response/changePassword.json @@ -0,0 +1,15 @@ +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Date: Mon, 20 Jun 2016 10:20:48 GMT +Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016) +Cache-Control: no-cache, no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes +Access-Control-Allow-Headers: Content-Type, Authorization +Pragma: no-cache +X-OAuth-Scopes: change_password read_items read_fees read_patron +X-Accepted-OAuth-Scopes: change_password + +{ + "patron": "08301001001" +} diff --git a/module/VuFind/tests/fixtures/paia/response/fees.json b/module/VuFind/tests/fixtures/paia/response/fees.json new file mode 100644 index 00000000000..010e18a05fd --- /dev/null +++ b/module/VuFind/tests/fixtures/paia/response/fees.json @@ -0,0 +1,51 @@ +HTTP/1.1 200 OK +Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016) +Date: Mon, 20 Jun 2016 11:30:05 GMT +Cache-Control: no-cache +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes +Access-Control-Allow-Headers: Content-Type, Authorization +Pragma: no-cache +X-OAuth-Scopes: read_availability change_password write_items read_items read_fees read_patron +X-Accepted-OAuth-Scopes: read_fees +Content-Type: application/json; charset=UTF-8 + +{ + "amount": "7.40 EUR", + "fee": [{ + "feetypeid": "de-830:fee-type:2", + "amount": "1.60 EUR", + "date": "2016-06-07T11:19:11+02:00", + "feetype": "Vormerkgebuehr", + "about": "Open source licensing : software freedom and intellectual property law ; [open source licensees are free to: use open source software for any purpose, make and distribute copies, create and distribute derivative works, access and use the source code, com / Rosen, Lawrence (c 2005)", + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$28295402" + }, { + "feetypeid": "de-830:fee-type:2", + "amount": "0.80 EUR", + "date": "2016-05-23T15:31:34+02:00", + "feetype": "Vormerkgebuehr", + "about": "Zend framework in action / Allen, Rob (2009)", + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$28323471" + }, { + "feetypeid": "de-830:fee-type:3", + "amount": "3.00 EUR", + "date": "2016-05-24T02:31:30+02:00", + "feetype": "Säumnisgebühr", + "about": "Unsere historischen Gärten / Lutze, Margot (1986)", + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$24476416" + }, { + "feetypeid": "de-830:fee-type:3", + "amount": "1.00 EUR", + "date": "2016-06-17T02:31:48+02:00", + "feetype": "Säumnisgebühr", + "about": "Triumphe des Backsteins = Triumphs of brick / (1992)", + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$33204941" + }, { + "feetypeid": "de-830:fee-type:3", + "amount": "1.00 EUR", + "date": "2016-05-24T02:31:30+02:00", + "feetype": "Säumnisgebühr", + "about": "Lehrbuch der Botanik / Strasburger, Eduard (2008)", + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$26461872" + }] +} diff --git a/module/VuFind/tests/fixtures/paia/response/items.json b/module/VuFind/tests/fixtures/paia/response/items.json new file mode 100644 index 00000000000..15df98c4ee2 --- /dev/null +++ b/module/VuFind/tests/fixtures/paia/response/items.json @@ -0,0 +1,87 @@ +HTTP/1.1 200 OK +Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016) +Date: Mon, 20 Jun 2016 10:38:21 GMT +Cache-Control: no-cache +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes +Access-Control-Allow-Headers: Content-Type, Authorization +Pragma: no-cache +X-OAuth-Scopes: change_password read_items read_fees read_patron +X-Accepted-OAuth-Scopes: read_patron +Content-Type: application/json; charset=UTF-8 + +{ + "doc": [{ + "queue": 0, + "canrenew": false, + "cancancel": false, + "storageid": "de-830:desk:23", + "label": "34:3409-6983", + "reminder": 0, + "edition": "http://uri.gbv.de/document/opac-de-830:ppn:040445623", + "status": 4, + "starttime": "2016-06-17", + "storage": "Test-Theke", + "about": "Praktikum über Entwurf und Manipulation von Datenbanken : SQL/DS (IBM), UDS (Siemens) und MEMODAX / Vossen, Gottfried (1986)", + "renewals": 0, + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$34096983" + }, { + "queue": 0, + "canrenew": false, + "cancancel": false, + "storageid": "de-830:desk:1", + "label": "24:2426-0127", + "reminder": 0, + "edition": "http://uri.gbv.de/document/opac-de-830:ppn:020966334", + "endtime": "2016-05-23", + "status": 2, + "starttime": "2016-04-25T08:50:41+02:00", + "storage": "Ausleihe", + "about": "Gold / Kettell, Brian (1982)", + "renewals": 0, + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$24260127" + },{ + "queue": 0, + "canrenew": false, + "cancancel": false, + "storageid": "de-830:desk:1", + "label": "28:2834-2436", + "reminder": 1, + "edition": "http://uri.gbv.de/document/opac-de-830:ppn:58891861X", + "endtime": "2016-06-15", + "status": 3, + "starttime": "2013-11-15", + "storage": "Ausleihe", + "about": "Theoretische Informatik : mit 22 Tabellen und 78 Aufgaben / Hoffmann, Dirk W. (2009)", + "renewals": 12, + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$28342436" + }, { + "queue": 0, + "canrenew": false, + "cancancel": false, + "storageid": "de-830:desk:612", + "label": "22:2227-8001", + "reminder": 0, + "edition": "http://uri.gbv.de/document/opac-de-830:ppn:659228084", + "endtime": "2016-07-14", + "status": 3, + "starttime": "2011-12-22", + "storage": "Ausleihe", + "about": "Linked Open Library Data : bibliographische Daten und ihre Zugänglichkeit im Web der Daten ; Innovationspreis 2011 / Fürste, Fabian M. (2011)", + "renewals": 9, + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$22278001" + }, { + "queue": 0, + "canrenew": false, + "cancancel": true, + "storageid": "de-830:desk:613", + "label": "28:2829-5402", + "edition": "http://uri.gbv.de/document/opac-de-830:ppn:391260316", + "endtime": "2016-06-15", + "status": 1, + "starttime": "2016-06-15T13:22:27+02:00", + "storage": "Ausleihe", + "about": "Open source licensing : software freedom and intellectual property law ; [open source licensees are free to: use open source software for any purpose, make and distribute copies, create and distribute derivative works, access and use the source code, com / Rosen, Lawrence (c 2005)", + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$28295402" + } ] +} diff --git a/module/VuFind/tests/fixtures/paia/response/login.json b/module/VuFind/tests/fixtures/paia/response/login.json new file mode 100644 index 00000000000..ac3fc2676ee --- /dev/null +++ b/module/VuFind/tests/fixtures/paia/response/login.json @@ -0,0 +1,18 @@ +HTTP/1.1 200 OK +Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016) +Date: Mon, 20 Jun 2016 13:35:01 GMT +Cache-Control: no-cache, no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes +Access-Control-Allow-Headers: Content-Type, Authorization +Pragma: no-cache +Content-Type: application/json; charset=UTF-8 + +{ + "access_token": "AAASECRETBBB", + "expires_in": 3600, + "scope": "read_availability change_password write_items read_items read_fees read_patron", + "token_type": "Bearer", + "patron": "08301001001" +} + diff --git a/module/VuFind/tests/fixtures/paia/response/login_bad.json b/module/VuFind/tests/fixtures/paia/response/login_bad.json new file mode 100644 index 00000000000..e5379d8795c --- /dev/null +++ b/module/VuFind/tests/fixtures/paia/response/login_bad.json @@ -0,0 +1,14 @@ +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016) +Date: Mon, 20 Jun 2016 13:35:01 GMT +Cache-Control: no-cache, no-store +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes +Access-Control-Allow-Headers: Content-Type, Authorization +Pragma: no-cache + +{ + "error": "access_denied", + "error_description": "invalid patron or password" +} diff --git a/module/VuFind/tests/fixtures/paia/response/patron.json b/module/VuFind/tests/fixtures/paia/response/patron.json new file mode 100644 index 00000000000..819320fdb9b --- /dev/null +++ b/module/VuFind/tests/fixtures/paia/response/patron.json @@ -0,0 +1,22 @@ +HTTP/1.1 200 OK +Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016) +Date: Mon, 20 Jun 2016 10:38:21 GMT +Cache-Control: no-cache +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes +Access-Control-Allow-Headers: Content-Type, Authorization +Pragma: no-cache +X-OAuth-Scopes: change_password read_items read_fees read_patron +X-Accepted-OAuth-Scopes: read_patron +Content-Type: application/json; charset=UTF-8 + +{ + "name": " Nobody Nothing", + "expires": "9999-12-31", + "email": "nobody@vufind.org", + "status": 0, + "address": "No Street at all 8, D-21073 Hamburg", + "type": ["de-830:user-type:20"] +} + + diff --git a/module/VuFind/tests/fixtures/paia/response/patron_expired.json b/module/VuFind/tests/fixtures/paia/response/patron_expired.json new file mode 100644 index 00000000000..acf8c631672 --- /dev/null +++ b/module/VuFind/tests/fixtures/paia/response/patron_expired.json @@ -0,0 +1,22 @@ +HTTP/1.1 200 OK +Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016) +Date: Mon, 20 Jun 2016 10:38:21 GMT +Cache-Control: no-cache +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes +Access-Control-Allow-Headers: Content-Type, Authorization +Pragma: no-cache +X-OAuth-Scopes: change_password read_items read_fees read_patron +X-Accepted-OAuth-Scopes: read_patron +Content-Type: application/json; charset=UTF-8 + +{ + "name": " Nobody Nothing", + "expires": "2015-12-31", + "email": "nobody@vufind.org", + "status": 0, + "address": "No Street at all 8, D-21073 Hamburg", + "type": ["de-830:user-type:20"] +} + + diff --git a/module/VuFind/tests/fixtures/paia/response/patron_locked.json b/module/VuFind/tests/fixtures/paia/response/patron_locked.json new file mode 100644 index 00000000000..1392f029859 --- /dev/null +++ b/module/VuFind/tests/fixtures/paia/response/patron_locked.json @@ -0,0 +1,22 @@ +HTTP/1.1 200 OK +Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016) +Date: Mon, 20 Jun 2016 10:38:21 GMT +Cache-Control: no-cache +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes +Access-Control-Allow-Headers: Content-Type, Authorization +Pragma: no-cache +X-OAuth-Scopes: change_password read_items read_fees read_patron +X-Accepted-OAuth-Scopes: read_patron +Content-Type: application/json; charset=UTF-8 + +{ + "name": " Nobody Nothing", + "expires": "9999-12-31", + "email": "nobody@vufind.org", + "status": 9, + "address": "No Street at all 8, D-21073 Hamburg", + "type": ["de-830:user-type:20"] +} + + diff --git a/module/VuFind/tests/fixtures/paia/response/renew_error.json b/module/VuFind/tests/fixtures/paia/response/renew_error.json new file mode 100644 index 00000000000..e1d95e98308 --- /dev/null +++ b/module/VuFind/tests/fixtures/paia/response/renew_error.json @@ -0,0 +1,17 @@ +HTTP/1.1 200 OK +Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016) +Date: Mon, 20 Jun 2016 11:21:15 GMT +Cache-Control: no-cache +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes +Access-Control-Allow-Headers: Content-Type, Authorization +Pragma: no-cache +X-OAuth-Scopes: change_password read_items read_fees read_patron +X-Accepted-OAuth-Scopes: write_items +Content-Type: application/json; charset=UTF-8 + +{ + "error": "insufficient_scope", + "code": 403, + "error_description": "The access token was accepted but it lacks permission for the request" +} \ No newline at end of file diff --git a/module/VuFind/tests/fixtures/paia/response/renew_ok.json b/module/VuFind/tests/fixtures/paia/response/renew_ok.json new file mode 100644 index 00000000000..be60b443e02 --- /dev/null +++ b/module/VuFind/tests/fixtures/paia/response/renew_ok.json @@ -0,0 +1,30 @@ +HTTP/1.1 200 OK +Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016) +Date: Mon, 20 Jun 2016 11:23:46 GMT +Cache-Control: no-cache +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes +Access-Control-Allow-Headers: Content-Type, Authorization +Pragma: no-cache +X-OAuth-Scopes: read_availability change_password write_items read_items read_fees read_patron +X-Accepted-OAuth-Scopes: write_items +Content-Type: application/json; charset=UTF-8 + +{ + "doc": [{ + "requested": "http://uri.gbv.de/document/opac-de-830:bar:830$22061137", + "queue": 0, + "cancancel": false, + "storageid": "de-830:desk:1", + "label": "22:2206-1137", + "reminder": 0, + "edition": "http://uri.gbv.de/document/opac-de-830:ppn:048430196", + "endtime": "2016-07-18", + "status": 3, + "starttime": "2016-06-07", + "storage": "Ausleihe", + "about": "Verteilte Datenbanken / (1991)", + "renewals": 1, + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$22061137" + }] +} diff --git a/module/VuFind/tests/fixtures/paia/response/storageretrieval.json b/module/VuFind/tests/fixtures/paia/response/storageretrieval.json new file mode 100644 index 00000000000..31cfed23463 --- /dev/null +++ b/module/VuFind/tests/fixtures/paia/response/storageretrieval.json @@ -0,0 +1,29 @@ +HTTP/1.1 200 OK +Server: vzg-paia/2.2-RC3 (Wed Jun 01 17:38:24 CEST 2016) +Date: Mon, 20 Jun 2016 11:26:47 GMT +Cache-Control: no-cache +Access-Control-Allow-Origin: * +Access-Control-Expose-Headers: X-OAuth-Scopes, X-Accepted-OAuth-Scopes +Access-Control-Allow-Headers: Content-Type, Authorization +Pragma: no-cache +X-OAuth-Scopes: read_availability change_password write_items read_items read_fees read_patron +X-Accepted-OAuth-Scopes: write_items +Content-Type: application/json; charset=UTF-8 + +{ + "doc": [{ + "queue": 0, + "cancancel": false, + "storageid": "de-830:desk:1", + "label": "24:2401-4292", + "reminder": 0, + "edition": "http://uri.gbv.de/document/opac-de-830:ppn:221142312", + "endtime": "2016-07-18", + "status": 2, + "starttime": "2016-06-20T13:26:46+02:00", + "storage": "Ausleihe", + "about": "Konzepte von Datenbanken / Kuhlen, Rainer (1979)", + "renewals": 0, + "item": "http://uri.gbv.de/document/opac-de-830:bar:830$24014292" + }] +} \ No newline at end of file diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DAIATest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DAIATest.php index 78216e57709..4d83a8d70ec 100644 --- a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DAIATest.php +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/DAIATest.php @@ -54,15 +54,26 @@ class DAIATest extends \VuFindTest\Unit\ILSDriverTestCase 'requests_placed' => '', 'id' => "027586081", 'item_id' => "http://uri.gbv.de/document/opac-de-000:epn:711134758", - 'ilslink' => "http://opac.example-library.edu/DB=1/PPNSET?PPN=027586081", + 'ilslink' => "http://opac.example-library.edu/loan/REQ?EPN=711134758", 'number' => 1, 'barcode' => "1", 'reserve' => "N", 'callnumber' => "ABC 12", - 'location' => "Example Library for DAIA Tests - Abteilung III", - 'locationhref' => false, + 'location' => 'Example Library for DAIA Tests', + 'locationid' => 'http://uri.gbv.de/organization/isil/DE-000', + 'locationhref' => 'http://www.example-library.edu', + 'storage' => 'Abteilung III', + 'storageid' => '', + 'storagehref' => '', 'item_notes' => [], - 'services' => ['loan', 'presentation'] + 'services' => ['loan', 'presentation'], + 'is_holdable' => false, + 'addLink' => false, + 'holdtype' => null, + 'addStorageRetrievalRequestLink' => true, + 'customData' => [], + 'limitation_types' => [], + 'doc_id' => 'http://uri.gbv.de/document/opac-de-000:ppn:027586081' ], 1 => [ @@ -77,10 +88,21 @@ class DAIATest extends \VuFindTest\Unit\ILSDriverTestCase 'barcode' => "1", 'reserve' => "N", 'callnumber' => "DEF 34", - 'location' => "Example Library for DAIA Tests - Abteilung III", - 'locationhref' => false, + 'location' => 'Example Library for DAIA Tests', + 'locationid' => 'http://uri.gbv.de/organization/isil/DE-000', + 'locationhref' => 'http://www.example-library.edu', + 'storage' => 'Abteilung III', + 'storageid' => '', + 'storagehref' => '', 'item_notes' => ['mit Zustimmung', 'nur Kopie'], - 'services' => ['loan', 'presentation'] + 'services' => ['loan', 'presentation'], + 'is_holdable' => false, + 'addLink' => false, + 'holdtype' => null, + 'addStorageRetrievalRequestLink' => false, + 'customData' => [], + 'limitation_types' => [], + 'doc_id' => 'http://uri.gbv.de/document/opac-de-000:ppn:027586081' ], 2 => [ @@ -95,10 +117,21 @@ class DAIATest extends \VuFindTest\Unit\ILSDriverTestCase 'barcode' => "1", 'reserve' => "N", 'callnumber' => "GHI 56", - 'location' => "Example Library for DAIA Tests - Abteilung III", - 'locationhref' => false, + 'location' => 'Example Library for DAIA Tests', + 'locationid' => 'http://uri.gbv.de/organization/isil/DE-000', + 'locationhref' => 'http://www.example-library.edu', + 'storage' => 'Abteilung III', + 'storageid' => '', + 'storagehref' => '', 'item_notes' => [], - 'services' => [] + 'services' => [], + 'is_holdable' => false, + 'addLink' => false, + 'holdtype' => null, + 'addStorageRetrievalRequestLink' => false, + 'customData' => [], + 'limitation_types' => [], + 'doc_id' => 'http://uri.gbv.de/document/opac-de-000:ppn:027586081' ], ]; diff --git a/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PAIATest.php b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PAIATest.php new file mode 100644 index 00000000000..1af911b12ca --- /dev/null +++ b/module/VuFind/tests/unit-tests/src/VuFindTest/ILS/Driver/PAIATest.php @@ -0,0 +1,628 @@ +<?php +/** + * ILS driver test + * + * PHP version 5 + * + * Copyright (C) Villanova University 2011. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 2, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @author Oliver Goldschmidt <o.goldschmidt@tuhh.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +namespace VuFindTest\ILS\Driver; +use VuFind\ILS\Driver\PAIA; + +use Zend\Http\Client\Adapter\Test as TestAdapter; +use Zend\Http\Response as HttpResponse; + +use InvalidArgumentException; + +/** + * ILS driver test + * + * @category VuFind + * @package Tests + * @author Demian Katz <demian.katz@villanova.edu> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org Main Page + */ +class PAIATest extends \VuFindTest\Unit\ILSDriverTestCase +{ + protected $validConfig = [ + 'DAIA' => + [ + 'baseUrl' => 'http://daia.gbv.de/', + ], + 'PAIA' => + [ + 'baseUrl' => 'http://paia.gbv.de/', + ] + ]; + + protected $patron = [ + 'id' => '08301001001', + 'firstname' => 'Nobody', + 'lastname' => 'Nothing', + 'email' => 'nobody@vufind.org', + 'major' => null, + 'college' => null, + 'name' => ' Nobody Nothing', + 'expires' => '9999-12-31', + 'status' => 0, + 'address' => 'No street at all 8, D-21073 Hamburg', + 'type' => [ + 0 => 'de-830:user-type:2' + ], + 'cat_username' => '08301001001', + 'cat_password' => 'NOPASSWORD' + ]; + + protected $patron_bad = [ + 'id' => '08301001011', + 'firstname' => 'Invalid', + 'lastname' => 'Nobody', + 'email' => 'nobody_invalid@vufind.org', + 'major' => null, + 'college' => null, + 'name' => ' Nobody Nothing', + 'expires' => '9999-12-31', + 'status' => 9, + 'address' => 'No street at all 8, D-21073 Hamburg', + 'type' => [ + 0 => 'de-830:user-type:2' + ], + 'cat_username' => '08301001011', + 'cat_password' => 'NOPASSWORD' + ]; + + protected $patron_expired = [ + 'id' => '08301001111', + 'firstname' => 'Expired', + 'lastname' => 'Nobody', + 'email' => 'nobody_expired@vufind.org', + 'major' => null, + 'college' => null, + 'name' => ' Nobody Nothing', + 'expires' => '2015-12-31', + 'status' => 0, + 'address' => 'No street at all 8, D-21073 Hamburg', + 'type' => [ + 0 => 'de-830:user-type:2' + ], + 'cat_username' => '08301001111', + 'cat_password' => 'NOPASSWORD' + ]; + + protected $feeTestResult = [ + 0 => + [ + 'amount' => 160.0, + 'checkout' => '', + 'fine' => 'Vormerkgebuehr', + 'balance' => 160.0, + 'createdate' => '06-07-2016', + 'duedate' => '', + 'id' => '', + 'title' => null, + 'feeid' => null, + 'about' => 'Open source licensing : software freedom and intellectual property law ; [open source licensees are free to: use open source software for any purpose, make and distribute copies, create and distribute derivative works, access and use the source code, com / Rosen, Lawrence (c 2005)', + 'item' => 'http://uri.gbv.de/document/opac-de-830:bar:830$28295402' + ], + 1 => + [ + 'amount' => 80.0, + 'checkout' => '', + 'fine' => 'Vormerkgebuehr', + 'balance' => 80.0, + 'createdate' => '05-23-2016', + 'duedate' => '', + 'id' => '', + 'title' => null, + 'feeid' => null, + 'about' => 'Zend framework in action / Allen, Rob (2009)', + 'item' => 'http://uri.gbv.de/document/opac-de-830:bar:830$28323471' + ], + 2 => + [ + 'amount' => 300.0, + 'checkout' => '', + 'fine' => 'Säumnisgebühr', + 'balance' => 300.0, + 'createdate' => '05-23-2016', + 'duedate' => '', + 'id' => '', + 'title' => null, + 'feeid' => null, + 'about' => 'Unsere historischen Gärten / Lutze, Margot (1986)', + 'item' => 'http://uri.gbv.de/document/opac-de-830:bar:830$24476416' + ], + 3 => + [ + 'amount' => 100.0, + 'checkout' => '', + 'fine' => 'Säumnisgebühr', + 'balance' => 100.0, + 'createdate' => '06-16-2016', + 'duedate' => '', + 'id' => '', + 'title' => null, + 'feeid' => null, + 'about' => 'Triumphe des Backsteins = Triumphs of brick / (1992)', + 'item' => 'http://uri.gbv.de/document/opac-de-830:bar:830$33204941' + ], + 4 => + [ + 'amount' => 100.0, + 'checkout' => '', + 'fine' => 'Säumnisgebühr', + 'balance' => 100.0, + 'createdate' => '05-23-2016', + 'duedate' => '', + 'id' => '', + 'title' => null, + 'feeid' => null, + 'about' => 'Lehrbuch der Botanik / Strasburger, Eduard (2008)', + 'item' => 'http://uri.gbv.de/document/opac-de-830:bar:830$26461872' + ], + ]; + + protected $holdsTestResult = [ + 0 => + [ + 'item_id' => 'http://uri.gbv.de/document/opac-de-830:bar:830$34096983', + 'cancel_details' => '', + 'id' => 'http://uri.gbv.de/document/opac-de-830:ppn:040445623', + 'type' => 'provided', + 'location' => 'Test-Theke', + 'position' => 0, + 'available' => true, + 'title' => 'Praktikum über Entwurf und Manipulation von Datenbanken : SQL/DS (IBM), UDS (Siemens) und MEMODAX / Vossen, Gottfried (1986)', + 'callnumber' => '34:3409-6983', + 'create' => '06-17-2016', + 'expire' => '', + ], + 1 => + [ + 'item_id' => 'http://uri.gbv.de/document/opac-de-830:bar:830$28295402', + 'cancel_details' => 'http://uri.gbv.de/document/opac-de-830:bar:830$28295402', + 'id' => 'http://uri.gbv.de/document/opac-de-830:ppn:391260316', + 'type' => 'reserved', + 'location' => 'Ausleihe', + 'position' => 0, + 'available' => false, + 'title' => 'Open source licensing : software freedom and intellectual property law ; [open source licensees are free to: use open source software for any purpose, make and distribute copies, create and distribute derivative works, access and use the source code, com / Rosen, Lawrence (c 2005)', + 'callnumber' => '28:2829-5402', + 'create' => '06-15-2016', + 'duedate' => '06-15-2016', + ], + ]; + + protected $requestsTestResult = [ + 0 => + [ + 'item_id' => 'http://uri.gbv.de/document/opac-de-830:bar:830$24260127', + 'cancel_details' => '', + 'id' => 'http://uri.gbv.de/document/opac-de-830:ppn:020966334', + 'type' => 'ordered', + 'location' => 'Ausleihe', + 'position' => 0, + 'available' => false, + 'title' => 'Gold / Kettell, Brian (1982)', + 'callnumber' => '24:2426-0127', + 'create' => '04-25-2016', + ], + ]; + + protected $transactionsTestResult = [ + 0 => + [ + 'item_id' => 'http://uri.gbv.de/document/opac-de-830:bar:830$28342436', + 'id' => 'http://uri.gbv.de/document/opac-de-830:ppn:58891861X', + 'title' => 'Theoretische Informatik : mit 22 Tabellen und 78 Aufgaben / Hoffmann, Dirk W. (2009)', + 'callnumber' => '28:2834-2436', + 'renewable' => false, + 'renew_details' => '', + 'request' => 0, + 'renew' => 12, + 'reminder' => 1, + 'startTime' => '11-15-2013', + 'dueTime' => '06-15-2016', + 'duedate' => '', + 'message' => '', + 'borrowingLocation' => 'Ausleihe', + ], + 1 => + [ + 'renewable' => false, + 'item_id' => 'http://uri.gbv.de/document/opac-de-830:bar:830$22278001', + 'renew_details' => '', + 'id' => 'http://uri.gbv.de/document/opac-de-830:ppn:659228084', + 'title' => 'Linked Open Library Data : bibliographische Daten und ihre Zugänglichkeit im Web der Daten ; Innovationspreis 2011 / Fürste, Fabian M. (2011)', + 'request' => 0, + 'renew' => 9, + 'reminder' => 0, + 'startTime' => '12-22-2011', + 'dueTime' => '07-14-2016', + 'duedate' => '', + 'message' => '', + 'borrowingLocation' => 'Ausleihe', + 'callnumber' => '22:2227-8001', + ] + ]; + + protected $renewTestResult = [ + 'blocks' => false, + 'details' => [ + 'http://uri.gbv.de/document/opac-de-830:bar:830$22061137' => [ + 'success' => true, + 'new_date' => "07-18-2016", + 'item_id' => 0, + 'sysMessage' => "Successfully renewed" + ] + ] + ]; + + protected $storageRetrievalTestResult = [ + 'success' => true, + 'sysMessage' => 'Successfully requested' + ]; + + protected $pwchangeTestResult = [ + 'success' => true, + 'status' => "Successfully changed" + ]; + + protected $profileTestResult = [ + 'firstname' => "Nobody", + 'lastname' => "Nothing", + 'address1' => NULL, + 'address2' => NULL, + 'city' => NULL, + 'country' => NULL, + 'zip' => NULL, + 'phone' => NULL, + 'group' => NULL, + 'expires' => "12-31-9999", + 'statuscode' => 0, + 'canWrite' => true + ]; + + /******************* Test cases ***************/ + /* + ok changePassword + ok checkRequestIsValid + ok checkStorageRetrievalRequestIsValid + ok getMyProfile + ok getMyFines + ok getMyHolds + ok getMyTransactions + ok getRenewDetails + ok getMyStorageRetrievalRequests + ok placeHold + ok renewMyItems + ok placeStorageRetrievalRequest + */ + + /** + * Constructor + */ + public function __construct() + { + $this->driver = $this->createConnector(); + } + + /** + * Test + * + * @return void + */ + public function testChangePassword() + { + $changePasswordTestdata = [ + "patron" => [ + "cat_username" => "08301001001" + ], + "oldPassword" => "oldsecret", + "newPassword" => "newsecret" + ]; + + $conn = $this->createConnector('changePassword.json'); + $conn->setConfig($this->validConfig); + $conn->init(); + $result = $conn->changePassword($changePasswordTestdata); + $this->assertEquals($this->pwchangeTestResult, $result); + } + + /** + * Test + * + * @return void + */ + public function testFees() + { + $conn = $this->createConnector('fees.json'); + $conn->setConfig($this->validConfig); + $conn->init(); + $result = $conn->getMyFines($this->patron); + + $this->assertEquals($this->feeTestResult, $result); + } + + /** + * Test + * + * @return void + */ + public function testHolds() + { + $conn = $this->createConnector('items.json'); + $conn->setConfig($this->validConfig); + $conn->init(); + $result = $conn->getMyHolds($this->patron); + + $this->assertEquals($this->holdsTestResult, $result); + } + + /** + * Test + * + * @return void + */ + public function testRequests() + { + $conn = $this->createConnector('items.json'); + $conn->setConfig($this->validConfig); + $conn->init(); + $result = $conn->getMyStorageRetrievalRequests($this->patron); + + $this->assertEquals($this->requestsTestResult, $result); + } + + /** + * Test + * + * @return void + */ + public function testTransactions() + { + $conn = $this->createConnector('items.json'); + $conn->setConfig($this->validConfig); + $conn->init(); + $result = $conn->getMyTransactions($this->patron); + + $this->assertEquals($this->transactionsTestResult, $result); + } + + /** + * Test + * + * @return void + */ + public function testProfile() + { + $conn = $this->createMockConnector('patron.json'); + $result = $conn->getMyProfile($this->patron); + + $this->assertEquals($this->profileTestResult, $result); + } + + /** + * Test + * + * @return void + */ + public function testValidRequest() + { + $conn = $this->createMockConnector('patron.json'); + + $result = $conn->checkRequestIsValid( + 'http://paia.gbv.de/', [], $this->patron + ); + $resultStorageRetrieval = $conn->checkStorageRetrievalRequestIsValid( + 'http://paia.gbv.de/', [], $this->patron + ); + $result_bad = $conn->checkRequestIsValid( + 'http://paia.gbv.de/', [], $this->patron_bad + ); + $resultStorage_bad = $conn->checkStorageRetrievalRequestIsValid( + 'http://paia.gbv.de/', [], $this->patron_bad + ); + $result_expired = $conn->checkRequestIsValid( + 'http://paia.gbv.de/', [], $this->patron_expired + ); + $resultStorage_expired = $conn->checkStorageRetrievalRequestIsValid( + 'http://paia.gbv.de/', [], $this->patron_expired + ); + + $this->assertEquals(true, $result); + $this->assertEquals(true, $resultStorageRetrieval); + $this->assertEquals(false, $result_bad); + $this->assertEquals(false, $resultStorage_bad); + $this->assertEquals(false, $result_expired); + $this->assertEquals(false, $resultStorage_expired); + } + /** + * Test + * + * @return void + */ + public function testRenewDetails() + { + $conn = $this->createConnector(''); + $conn->setConfig($this->validConfig); + $conn->init(); + $result = $conn->getRenewDetails($this->transactionsTestResult[1]); + + $this->assertEquals('', $result); + } + + /** + * Test + * + * @return void + */ + public function testPlaceHold() + { + $sr_request = [ + "item_id" => "http://uri.gbv.de/document/opac-de-830:bar:830$24014292", + "patron" => [ + "cat_username" => "08301001001" + ] + ]; + + $conn = $this->createConnector('storageretrieval.json'); + $conn->setConfig($this->validConfig); + $conn->init(); + $result = $conn->placeHold($sr_request); + $this->assertEquals($this->storageRetrievalTestResult, $result); + } + + /** + * Test + * + * @return void + */ + public function testPlaceStorageRetrievalRequest() + { + $sr_request = [ + "item_id" => "http://uri.gbv.de/document/opac-de-830:bar:830$24014292", + "patron" => [ + "cat_username" => "08301001001" + ] + ]; + + $conn = $this->createConnector('storageretrieval.json'); + $conn->setConfig($this->validConfig); + $conn->init(); + $result = $conn->placeStorageRetrievalRequest($sr_request); + $this->assertEquals($this->storageRetrievalTestResult, $result); + } + + /** + * Test + * + * @return void + */ + public function testRenew() + { + $renew_request = [ + "details" => [ + "item" => "http://uri.gbv.de/document/opac-de-830:bar:830$22061137" + ], + "patron" => [ + "cat_username" => "08301001001" + ] + ]; + + $conn = $this->createConnector('renew_ok.json'); + $conn->setConfig($this->validConfig); + $conn->init(); + $result = $conn->renewMyItems($renew_request); + + $this->assertEquals($this->renewTestResult, $result); + + /* TODO: make me work + $conn_fail = $this->createConnector('renew_error.json'); + $connfail->setConfig($this->validConfig); + $conn_fail->init(); + $result_fail = $conn_fail->renewMyItems($renew_request); + + $this->assertEquals($this->failedRenewTestResult, $result_fail); + */ + } + + /** + * Create connector with fixture file. + * + * @param string $fixture Fixture file + * + * @return Connector + * + * @throws InvalidArgumentException Fixture file does not exist + */ + protected function createConnector($fixture = null) + { + $adapter = new TestAdapter(); + if ($fixture) { + $file = realpath( + __DIR__ . + '/../../../../../../tests/fixtures/paia/response/' . $fixture + ); + if (!is_string($file) || !file_exists($file) || !is_readable($file)) { + throw new InvalidArgumentException( + sprintf('Unable to load fixture file: %s ', $file) + ); + } + $response = file_get_contents($file); + $responseObj = HttpResponse::fromString($response); + $adapter->setResponse($responseObj); + } + $service = new \VuFindHttp\HttpService(); + $service->setDefaultAdapter($adapter); + $conn = new PAIA( + new \VuFind\Date\Converter(), + new \Zend\Session\SessionManager() + ); + $conn->setHttpService($service); + return $conn; + } + + /** + * Create connector with fixture file. + * + * @param string $fixture Fixture file + * + * @return Connector + * + * @throws InvalidArgumentException Fixture file does not exist + */ + protected function createMockConnector($fixture = null) + { + $adapter = new TestAdapter(); + if ($fixture) { + $file = realpath( + __DIR__ . + '/../../../../../../tests/fixtures/paia/response/' . $fixture + ); + if (!is_string($file) || !file_exists($file) || !is_readable($file)) { + throw new InvalidArgumentException( + sprintf('Unable to load fixture file: %s ', $file) + ); + } + $response = file_get_contents($file); + $responseObj = HttpResponse::fromString($response); + $adapter->setResponse($responseObj); + } + $service = new \VuFindHttp\HttpService(); + $service->setDefaultAdapter($adapter); + $conn = $this->getMockBuilder('VuFind\ILS\Driver\PAIA') + ->setConstructorArgs([ new \VuFind\Date\Converter(), + new \Zend\Session\SessionManager() + ]) + ->setMethods(['getScope']) + ->getMock(); + $conn->expects($this->any())->method('getScope') + ->will($this->returnValue([ 'write_items' ])); + $conn->setHttpService($service); + $conn->setConfig($this->validConfig); + $conn->init(); + return $conn; + } +} -- GitLab