diff --git a/module/finc/config/module.config.php b/module/finc/config/module.config.php index 5c218d3ad92e79b8b75ab603375be7f878e37c63..ce432792059bbbdd5ac1895438b70050b97eb4a3 100644 --- a/module/finc/config/module.config.php +++ b/module/finc/config/module.config.php @@ -73,12 +73,14 @@ $config = [ 'finc\ILS\Driver\FincILS' => 'finc\ILS\Driver\Factory::getFincILS', 'finc\ILS\Driver\PAIA' => 'finc\ILS\Driver\Factory::getPAIA', //finctheca is deprecated: Remove when Bibliotheca support ends - 'finc\ILS\Driver\FincTheca' => 'finc\ILS\Driver\Factory::GetFincTheca' + 'finc\ILS\Driver\FincTheca' => 'finc\ILS\Driver\Factory::getFincTheca', + 'finc\ILS\Driver\FincLibero' => 'finc\ILS\Driver\Factory::getFincLibero', ], 'aliases' => [ 'fincils' => 'finc\ILS\Driver\FincILS', 'paia' => 'finc\ILS\Driver\PAIA', 'finctheca' => 'finc\ILS\Driver\FincTheca', + 'finclibero' => 'finc\ILS\Driver\FincLibero' ], ], 'recommend' => [ diff --git a/module/finc/src/finc/ILS/Driver/Factory.php b/module/finc/src/finc/ILS/Driver/Factory.php index dea94ff068ebacb808f88eb04086a5011f2d9f36..56c3a0e7806bbab31d9a85002f23b3ad4f2ac376 100644 --- a/module/finc/src/finc/ILS/Driver/Factory.php +++ b/module/finc/src/finc/ILS/Driver/Factory.php @@ -133,4 +133,39 @@ class Factory return $fl; } + + /** + * Factory for FincLibero driver. + * + * @param \Psr\Container\ContainerInterface $container Service manager. + * + * @return FincLibero + */ + public static function getFincLibero(ContainerInterface $container) + { + $factory = new \ProxyManager\Factory\LazyLoadingValueHolderFactory($container->get('VuFind\ProxyConfig')); + + $callback = function (& $wrapped, $proxy) use ($container) { + $wrapped = $container->get('ZfcRbac\Service\AuthorizationService'); + + $proxy->setProxyInitializer(null); + }; + + $fl = new FincLibero( + $container->get('VuFind\DateConverter'), + $container->get('VuFind\SessionManager'), + $container->get('VuFind\RecordLoader'), + $container->get('VuFind\Search'), + $container->get('VuFind\Config')->get('config'), + $factory->createProxy('ZfcRbac\Service\AuthorizationService', $callback) + ); + + $fl->setCacheStorage( + $container->get('VuFind\CacheManager')->getCache('object') + ); + + $fl->staticStatusRules = $container->get('VuFind\YamlReader')->get('StaticStatusRules.yaml'); + + return $fl; + } } diff --git a/module/finc/src/finc/ILS/Driver/FincILS.php b/module/finc/src/finc/ILS/Driver/FincILS.php index 14eccacc8509a1c99708c6ba1f1bb731aa5873d1..4f0bdfbf5c1d20cc9add1b2ca78a54c99fc2db11 100644 --- a/module/finc/src/finc/ILS/Driver/FincILS.php +++ b/module/finc/src/finc/ILS/Driver/FincILS.php @@ -1688,4 +1688,17 @@ class FincILS extends PAIA implements LoggerAwareInterface throw new ILSException($e->getMessage()); } } + + /** + * Helper function to filter certain limitations. + * + * @param array $limitations An item's limitations. + * @return mixed + */ + protected function filterNonFunctionalLimitations($limitations) + { + //standard behavior is: do nothing + // overriden in FincLibero + return $limitations; + } } diff --git a/module/finc/src/finc/ILS/Driver/FincLibero.php b/module/finc/src/finc/ILS/Driver/FincLibero.php new file mode 100644 index 0000000000000000000000000000000000000000..f2325e0b0158612d0157af798c1ca1adc290bfad --- /dev/null +++ b/module/finc/src/finc/ILS/Driver/FincLibero.php @@ -0,0 +1,694 @@ +<?php +/** + * Finc specific Libero ILS Driver for VuFind, using PAIA, DAIA and LiberoDing + * services. + * + * PHP version 5 + * + * Copyright (C) Leipzig University Library 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 VuFind2 + * @package ILS_Drivers + * @author André Lahmann <lahmann@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/vufind2:building_an_ils_driver Wiki + */ +namespace finc\ILS\Driver; +use VuFind\I18n\Translator\TranslatorAwareTrait; +use VuFind\I18n\Translator\TranslatorAwareInterface, + VuFind\Exception\ILS as ILSException; + +/** + * Finc specific Libero ILS Driver for VuFind, using PAIA, DAIA and LiberoDing + * services. + * + * @category VuFind2 + * @package ILS_Drivers + * @author André Lahmann <lahmann@ub.uni-leipzig.de> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link http://vufind.org/wiki/vufind2:building_an_ils_driver Wiki + */ +class FincLibero extends FincILS implements TranslatorAwareInterface +{ + const DELETE_NOTIFICATIONS_SUCCESS = '1'; + const DELETE_NOTIFICATIONS_ERROR = '0'; + use LiberoDingTrait; + use TranslatorAwareTrait; + + /** + * Contains after init() is done all the limitations that are configured in the + * FincLibero.ini as being used for triggering actions etc (e.g.: + * requestableLimitations). + * + * @var array + */ + protected $configuredLimitations = []; + + /** + * Limitations that will trigger the placeStorageRetrievalRequest action + * + * @var array + */ + protected $requestableLimitations = []; + + /** + * Limitations that will trigger the placeHold action + * + * @var array + */ + protected $holdableLimitations = []; + + /** + * Limitations that will trigger a recall action (currently not used). + * + * @var array + */ + protected $recallableLimitations = []; + + /** + * Limitations that will identify the item for being bound to another item. + * + * @var array + */ + protected $awlLimitations = []; + + /** + * Patterns that will identify limitations as pickUpLocations + * + * @var array + */ + protected $pickUpLocationPatterns = []; + + /** + * URIs that will be used for stack views + * + * @var array + */ + protected $stackURIs = []; + + /** + * URIs that will be used for reading room views + * + * @var array + */ + protected $readingRoomURIs = []; + + /** + * Helper function to extract the Namespace from the DAIA URI prefix. + * + * @return string + */ + public function getDaiaIdPrefixNamespace() + { + return preg_quote(substr($this->daiaIdPrefix, 0, strpos($this->daiaIdPrefix, ':')+1)); + } + + /** + * FincLibero specific overrides of PAIA methods + */ + + /** + * 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 (Exception $e) { + $this->debug($e->getMessage()); + return [ + 'success' => false, + 'sysMessage' => $e->getMessage(), + ]; + } + + $details = []; + + if (array_key_exists('error', $array_response)) { + $details = [ + 'success' => false, + 'sysMessage' => $array_response['error_description'] + ]; + } else { + $elements = $array_response['doc']; + foreach ($elements as $element) { + if (array_key_exists('error', $element)) { + $details = [ + 'success' => false, + 'sysMessage' => $element['error'] + ]; + } else { + // FincLibero supports more expressive responses from paialibero + // which should be shown instead to the user (localIlsStatus) + $details = [ + 'success' => true, + 'sysMessage' => isset($element['localIlsStatus']) + ? $element['localIlsStatus'] : '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']); + } + if ($this->paiaCacheEnabled) { + $this->removeCachedData($patron['cat_username']); + } + } + } + } + return $details; + } + + /** + * PAIA authentication function with custom signature -- libraryuserid added for + * Libero Id + * + * @param string $username Username + * @param string $password Password + * @param string $libraryuserid Libero ID + * + * @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, $libraryuserid = null) + { + // perform full PAIA auth and get patron info + $post_data = [ + "username" => $username, + "password" => $password, + "grant_type" => "password", + "scope" => self::SCOPE_READ_PATRON . " " . + self::SCOPE_READ_FEES . " " . + self::SCOPE_READ_ITEMS . " " . + self::SCOPE_WRITE_ITEMS . " " . + self::SCOPE_CHANGE_PASSWORD + ]; + + // Customization for internal Libero Id + if (isset($libraryuserid)) { + $post_data['libraryuserid'] = $libraryuserid; + } + + $responseJson = $this->paiaPostRequest('auth/login', $post_data); + + try { + $responseArray = $this->paiaParseJsonAsArray($responseJson); + } catch (Exception $e) { + // all error handling is done in paiaHandleErrors so pass on the excpetion + throw $e; + } + + 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(); + + // as we do not store the password in the database, write it to the + // session (but only if we are not using root-login + if ($libraryuserid === null) { + $session->cat_password = $password; + } + + $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; + // Customization for internal Libero Id + $session->borrower_id + = isset($responseArray['borrower_id']) + ? $responseArray['borrower_id'] : null; + + return true; + } + } + + /** + * PAIA helper function to map session data to return value of patronLogin() + * + * @param $details Patron details returned by patronLogin + * @param $password Patron cataloge password + * @return mixed + */ + protected function enrichUserDetails($details, $password, $username = null) + { + $details = parent::enrichUserDetails($details, $password, $username); + + // store also the Libero Id + $session = $this->getSession(); + $details['borrower_id'] = $session->borrower_id; + return $this->enrichWithUserRestrictions($details); + } + + /** + * Determines for each of the tokens in @param $details['type'] whether it is + * configured to be a restriction type (as configured in FincLibero.ini + * [General] userRestrictionMessagePatterns) and saves positive matches in + * $details['restrictions']. + * + * @param $details patron details + * @return mixed modified patron details + */ + protected function enrichWithUserRestrictions($details) + { + //early return + if (!isset($details['type']) + || empty($details['type']) + || !isset($this->config['General']['userRestrictionMessagePatterns']) + ) { + return $details; + } + + foreach ($this->config['General']['userRestrictionMessagePatterns'] as $pattern) { + foreach ($details['type'] as $type) { + if (preg_match($pattern, $type)) { + $details['restrictions'][] = 'UserRestrictionMessages::' . $type; + } + } + } + return $details; + } + + /** + * FincLibero specific overrides of DAIA methods + */ + + /** + * Returns an array with status information for provided item. + * + * @param array $item Array with DAIA item data + * + * @return array + */ + protected function getItemStatus($item) + { + $return = parent::getItemStatus($item); + + $return['awlRecordId'] = $this->getBoundItemId($item); + // is this item bound with another item? + if ($return['awlRecordId'] != null) { + // overwrite any existing link settings as we need to order this item + // via the bound item + $return['addLink'] = + $return['addStorageRetrievalRequestLink'] = + $return['addILLRequestLink'] = + $return['addEmailHoldLink'] = false; + $return['awlRecordStatus'] = + current($this->getStatus($return['awlRecordId'])); + } + + // add all item specific information from DAIA field about to item_notes + // (https://intern.finc.info/issues/7863) + $about = (isset($item['about'])) ? [$item['about']] : []; + + $return['item_notes'] = array_unique( + array_merge( + (array) $return['status'], + $return['item_notes'], + $about + ) + ); + return $return; + } + + /** + * Helper function to return an appropriate status string for current item + * + * @param $item + * @return string + */ + protected function getStatusString($item) + { + return isset($item['localIlsStatus']) ? $item['localIlsStatus'] : ''; + } + + /** + * FincLibero specific implementation that adds pickUpLocations and the content + * of EmailHold limitations to the customData array. + * + * @param array $item A DAIA item. + * @return array + */ + protected function getCustomData($item) + { + $customData = []; + foreach (['available', 'unavailable'] as $availability) { + if (isset($item[$availability])) { + foreach ($item[$availability] as $available) { + if (isset($available['service']) + && in_array($available['service'], ['presentation', 'loan']) + ) { + // deal with pickuplocations + if (isset($available['limitation']) + && $pickUpLocations = $this->filterPickUpLocations( + $available['limitation'] + ) + ) { + // if we have limitations qualifying for pickUpLocations, + // save those in customData + $customData['pickUpLocations'] = $pickUpLocations; + } elseif (!isset($customData['pickUpLocations']) && isset($item['department'])) { + // if we have no explicit limitations qualifying for + // pickUpLocations, assume the item's department as single + // pickUpLocation + $customData['pickUpLocations'] = [ + [ + 'locationID' => $item['department']['id'], + 'locationDisplay' => $item['department']['content'] + ] + ]; + } + // deal with EmailHold information + if (isset($available['limitation'])) { + foreach ($available['limitation'] as $limitation) { + $criteria = $this->getEmailHoldValidationCriteria(); + // we assume that each configured criteria for + // EmailHold will provide information (if existent in + // current item) supposed to be passed to the view + // via customData array + foreach ($criteria as $key => $value) { + if ($this->checkEmailHoldValidationCriteria( + [$key=>$limitation['id']]) + ) { + $customData['emailHoldLimitationContent'][] = $limitation['content']; + } + } + } + } + } + } + } + } + return $customData; + } + + /** + * 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 getItemDepartment($item) + { + return $this->translate(parent::getItemDepartment($item)); + } + + /** + * Returns the evaluated values of the provided limitations element -- FincLibero + * specific implementation preprocesses limitations by filtering the non + * functional limitations (i.e. limitations that are used for triggering actions + * etc.) + * + * @param array $limitations Array with DAIA limitation data + * + * @return array + */ + protected function getItemLimitationContent($limitations) + { + return parent::getItemLimitationContent( + $this->filterNonFunctionalLimitations($limitations) + ); + } + + /** + * Returns the evaluated values of the provided limitations element -- FincLibero + * specific implementation preprocesses limitations by filtering the non + * functional limitations (i.e. limitations that are used for triggering actions + * etc.) + * + * @param array $limitations Array with DAIA limitation data + * + * @return array + */ + protected function getItemLimitationTypes($limitations) + { + return parent::getItemLimitationTypes( + $this->filterNonFunctionalLimitations($limitations) + ); + } + + /** + * FincLibero specific MyReSearch views + */ + + /** + * Custom FincLibero-method to return items with properties: + * - document.status = 4 + * - document.storage und document.storageid != Lesesaal/Magazin + * + * @param array $patron Array returned from patronLogin() + * + * @return array + */ + public function getMyMediaReadyToPickup($patron) + { + // filters for getMyMediaReadyToPickup are: + // status = 4 - provided (the document is ready to be used by the patron) + // document.storage und document.storageid != Lesesaal/Magazin + $filter = [ + 'status' => [4], + /*'exclude' => [ + 'storageid' => $this->stackURIs + ],*/ + 'endtime' => null, + 'regex' => ['item' => "/^(".$this->getDaiaIdPrefixNamespace().").*$/"] + ]; + // get items-docs for given filters + $items = $this->paiaGetItems($patron, $filter); + + return $this->mapPaiaItems($items, 'myHoldsMapping'); + } + + /** + * Custom FincLibero-method to return items with properties: + * - document.status = 3 + * - document.endtime = 0 + * + * @param array $patron Array returned from patronLogin() + * + * @return array + */ + public function getMyPermanentLoans($patron) + { + // filters for getMyPermanentLoans are: + // status = 3 - held (the document is on loan by the patron) + // endtime = 0 + $filter = [ + 'status' => [3], + 'endtime' => null, + 'regex' => ['item' => "/^(".$this->getDaiaIdPrefixNamespace().").*$/"] + ]; + // get items-docs for given filters + $items = $this->paiaGetItems($patron, $filter); + + return $this->mapPaiaItems( + $this->itemFieldSort($items, 'starttime', SORT_DESC), + 'myTransactionsMapping' + ); + } + + /** + * Customized getMyHolds for FincLibero to return items with properties: + * - document.status = 1 (reserved) + * - document.storage und document.storageid != Magazin + * - document.endtime = Rückgabedatum (not NULL!) + * + * @param array $patron Array returned from patronLogin() + * + * @return array + */ + public function getMyHolds($patron) + { + // filters for getMyHolds are: + // status = 1 - reserved + $filter = [ + 'status' => [1,2], + 'regex' => ['item' => "/^(".$this->getDaiaIdPrefixNamespace().").*$/"] + ]; + // get items-docs for given filters + $items = $this->paiaGetItems($patron, $filter); + + return $this->mapPaiaItems($items, 'myHoldsMapping'); + } + + /** + * Customized getMyTransactions for FincLibero to return items with properties: + * - document.status = 3 (held) + * - document.item = UBL:* + * - document.starttime = Ausleihdatum + * - document.endtime = Rückgabedatum (not NULL!) + * + * @param array $patron Array returned from patronLogin() + * + * @return array + */ + public function getMyTransactions($patron) + { + // filters for getMyPermanentLoans are: + // status = 3 - held (the document is on loan by the patron) + // endtime != null + $filter = [ + 'status' => [3], + 'exclude' => ['endtime' => null], + 'regex' => ['item' => "/^(".$this->getDaiaIdPrefixNamespace().").*$/"] + ]; + // get items-docs for given filters + $items = $this->paiaGetItems($patron, $filter); + + return $this->mapPaiaItems( + $this->itemFieldSort($items, 'endtime'), + 'myTransactionsMapping' + ); + } + + /** + * @param $patron + * @param null $messageIdList + * @param null $toDate won't work here + * @throws ILSException + * @return TRUE on success, FALSE otherwise + */ + public function removeMySystemMessages( + $patron, + $messageIdList = null, + $toDate = null + ) + { + return $this->paiaRemoveSystemMessages($patron,$messageIdList); + } + + /** + * FincLibero specific helper functions + */ + + /** + * Helper function to filter certain limitations. + * + * @param array $limitations An item's limitations. + * @return mixed + */ + protected function filterNonFunctionalLimitations($limitations) + { + // remove the configured limitations from the current set of limitations + foreach ($this->configuredLimitations as $configuredLimitation) { + foreach ($limitations as $key => $limitation) { + if (isset($limitation['id']) + && in_array($limitation['id'], $this->{$configuredLimitation}) + ) { + unset($limitations[$key]); + } + } + } + + // remove all known pickUpLocations from the current set of limitations + $pickUpLocations = $this->filterPickUpLocations($limitations); + foreach ($limitations as $key => $limitation) { + if (isset($limitation['id'])) { + foreach ($pickUpLocations as $pickup) { + if ($limitation['id'] == $pickup['locationID']) { + unset($limitations[$key]); + } + } + } + } + + return $limitations; + } + + /** + * Helper function to compile upon init() a set of limitations that can be used + * for filtering. + * + * @param string $limitation An URI identifying a limitation + */ + protected function setConfiguredLimitation($limitation) + { + if (is_string($limitation)) { + $this->configuredLimitations[] = $limitation; + } + } + + /** + * Helper function to filter PickUpLocations from given limitations based on + * pattern configured in ILS ini + * + * @param array $limitations An item's limitations + * @return array + */ + protected function filterPickUpLocations($limitations) + { + $pickUpLocations = []; + // return array(locationID=>URI, locationDisplay=>Content) + foreach ($limitations as $limitation) { + if (isset($limitation['id'])) { + foreach ($this->pickUpLocationPatterns as $pattern) { + if (preg_match($pattern, $limitation['id']) + && !(filter_var($limitation['id'], FILTER_VALIDATE_URL) === false) + ) { + $pickUpLocations[] = [ + 'locationID' => $limitation['id'], + 'locationDisplay' => isset($limitation['content']) + ? $limitation['content'] : $limitation['id'], + ]; + } + } + } + } + return $pickUpLocations; + } + +}