diff --git a/config/vufind/Alma.ini b/config/vufind/Alma.ini index 4e09d4076151b614a3fb7afa8140f8066b63c611..e6dcfde3a2644716aa3d9fb16e2ab631d45473eb 100644 --- a/config/vufind/Alma.ini +++ b/config/vufind/Alma.ini @@ -4,9 +4,11 @@ apiBaseUrl = "https://api-eu.hosted.exlibrisgroup.com/almaws/v1" ; An API key configured to allow access to Alma: apiKey = "your-key-here" + [Holds] ; HMACKeys - A list of hold form element names that will be analyzed for consistency ; during hold form processing. Most users should not need to change this setting. +; For activating title level hold request, add "description" and "level". HMACKeys = id:item_id:holding_id ; defaultRequiredDate - A colon-separated list used to set the default "not required @@ -25,3 +27,87 @@ extraHoldFields = comments:requiredByDate:pickUpLocation ; 2) "user-selected" to indicate that the user always has to choose the location ; 3) a value within the Location IDs returned by getPickUpLocations() defaultPickUpLocation = "" + + +; The "NewUser" section defines some default values that are used when creating an account +; in Alma via its API. This is only relevant if you use the authentication method "AlmaDatabase" +; in the "Authentication" section of the "config.ini" file. +[NewUser] +; Mandatory. The Alma user record type. Usually "PUBLIC". +recordType = PUBLIC + +; Mandatory. The Alma user account type. Usually this is "INTERNAL" if you use the AlmaDatabase +; authentication method. +accountType = INTERNAL + +; Mandatory. The status of the Alma user account. Usually "ACTIVE". +status = ACTIVE + +; Mandatory. The user group to which the new Alma account should belong. Use the code of one of +; the user groups that are defined in Alma (see "Alma Configuration -> User Management -> User Groups"). +userGroup = + +; Mandatory. The type of ID under which the username should be saved to Alma. Log in to the ExLibris developer +; network and check the Alma API documentation for possible values on this site: +; https://developers.exlibrisgroup.com/alma/apis/xsd/rest_user.xsd?tags=POST#user_identifier +idType = + +; Mandatory. The preferred language of the new Alma account. This should normally be the Alma language +; code of your local language (see "Alma Configuration -> General -> Institution Languages"). +preferredLanguage = + +; Mandatory. The type of eMail of the users eMail address. Log in to the ExLibris developer network and +; check the Alma API documentation for possible values on this site: +; https://developers.exlibrisgroup.com/alma/apis/xsd/rest_user.xsd?tags=POST#email_types +emailType = + +; Optional. Set the time period when the Alma account should expire. The given period will be added to the +; point in time of the Alma account creation. Use the DateInterval notation of PHP to express the period. See: +; https://secure.php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters +; If not set, 1 year (P1Y) will be used as default value. +expiryDate = + +; Optional. Set the time period that should be used for the Alma user account purge date. The given period +; will be added to the point in time of the Alma account creation. Use the DateInterval notation of PHP to +; express the period. See: +; https://secure.php.net/manual/en/dateinterval.construct.php#refsect1-dateinterval.construct-parameters +; If not set, the purge date of the Alma user account will be empty. +purgeDate = + + +[FulfillmentUnits] +; Specify the association of fulfillment units and its locations. Take the codes from: +; Alma Configuration -> Fulfillment -> Fulfillment Units -> [Choose Fulfillment Unit] -> Fulfillment Unit Locations. +; Tip: Export the list from Alma as Excel and use the CONCATENATE() formula to generate this list. +; Format: FULFILLMENT_UNIT_CODE[] = LOCATION_CODE +; Example: +; STACKS[] = stack1 +; STACKS[] = stack2 +; STACKS[] = stack3 +; LIMITED[] = periodicalroom +; LIMITED[] = musicrefs +; SHORTLOAN[] = office1 +; SHORTLOAN[] = office2 + +[Requestable] +; Specify for which combination of user group and fulfillment unit (see above) the request link +; should be displayed (who is allowed to request what). Define every combination of fulfillment unit +; and user group and assign "N" for "No, not requestable for this user group" or "Y" for "Yes, is +; requestable for this user group". You will find the user group codes here: +; Alma Configuration -> User Management -> User Groups +; Format: FULFILLMENT_UNIT_CODE[USER_GROUP_CODE] = N +; Example: +; STACKS[STAFF] = Y +; STACKS[STUDENT] = Y +; STACKS[GUEST] = Y +; LIMITED[STAFF] = Y +; LIMITED[STUDENT] = N +; LIMITED[GUEST] = N +; SHORTLOAN[STAFF] = Y +; SHORTLOAN[STUDENT] = Y +; SHORTLOAN[GUEST] = N + +[Webhook] +; The webhook secret. This must be the same value that was added to the Alma webhook configuration as a secret. +secret = YOUR_WEBHOOK_SECRET_FROM_ALMA + diff --git a/config/vufind/config.ini b/config/vufind/config.ini index c5f897538d5514d42127cd845a4b77ec769c852b..05a335aa1a22b097ce4f76ad9b5804c8e32f4d31 100644 --- a/config/vufind/config.ini +++ b/config/vufind/config.ini @@ -328,13 +328,14 @@ title_level_holds_mode = "disabled" ; This section allows you to determine how the users will authenticate. ; You can use an LDAP directory, the local ILS (or multiple ILSes through -; the MultiILS option), the VuFind database (Database), Shibboleth, SIP2, -; CAS, Facebook or some combination of these (via the MultiAuth or ChoiceAuth -; options). +; the MultiILS option), the VuFind database (Database), AlmaDatabase (combination +; of VuFind database and Alma account), Shibboleth, SIP2, CAS, Facebook or some +; combination of these (via the MultiAuth or ChoiceAuth options). [Authentication] ;method = LDAP ;method = ILS method = Database +;method = AlmaDatabase ;method = Shibboleth ;method = SIP2 ;method = CAS @@ -355,7 +356,7 @@ method = Database hideLogin = false ; Set this to false if you would like to store local passwords in plain text -; (only applies when method = Database above). +; (only applies when method = Database or AlmaDatabase above). hash_passwords = false ; Allow users to recover passwords via email (if supported by Auth method) @@ -387,7 +388,7 @@ ils_encryption_key = false ;ils_encryption_algo = "blowfish" ; This setting may optionally be uncommented to restrict the email domain(s) from -; which users are allowed to register when using the Database method. +; which users are allowed to register when using the Database or AlmaDatabase method. ;domain_whitelist[] = "myuniversity.edu" ;domain_whitelist[] = "mail.myuniversity.edu" @@ -406,8 +407,8 @@ ils_encryption_key = false ; Uncomment this line to switch on "privacy mode" in which no user information ; will be stored in the database. Note that this is incompatible with social ; features, password resets, and many other features. It is not recommended for -; use with "Database" authentication, since the user will be forced to create a -; new account upon every login. +; use with "Database" or "AlmaDatabase" authentication, since the user will be +; forced to create a new account upon every login. ;privacy = true ; Allow a user to delete their account. Default is false. diff --git a/config/vufind/permissions.ini b/config/vufind/permissions.ini index e36cf4e944faa9e1d9f69be38e35dc7b4658d97f..e85dab8330f0cfe51670a2c92b52dd5f26133140 100644 --- a/config/vufind/permissions.ini +++ b/config/vufind/permissions.ini @@ -166,3 +166,10 @@ role = loggedin ;require = ANY ;ipRange[] = '127.0.0.1' ;ipRange[] = '::1' + +; Example permission for Alma webbooks +;[alma.Webhooks] +;permission[] = "access.alma.webhook.user" +;permission[] = "access.alma.webhook.challenge" +;require = ALL +;ipRange[] = "127.0.0.1" diff --git a/languages/de.ini b/languages/de.ini index e93371e08aab69bd7835a0c74a1c34b01510ab69..c397cf2320e339c60f5e1f0e102c8651a7622f25 100644 --- a/languages/de.ini +++ b/languages/de.ini @@ -509,6 +509,7 @@ ill_request_processed = "Bearbeitet" ill_request_profile_html = "Um die Fernleihe zu nutzen, richten Sie bitte ihr <a href="%%url%%">Bibliothekskatalog-Profil</a> ein." ill_request_submit_text = "Anfrage abschicken" Illustrated = "Abbildungen" +ils_account_create_error = "Es konnte kein Konto in unserem Bibliotheksverwaltungssystem für Sie erstellt werden. Wir entschuldigen uns für die Umstände und stehen für weitere Fragen gerne zur Verfügung." ils_action_unavailable = "Diese Funktion ist für das aktuelle Bibliothekskonto nicht verfügbar." ils_connection_failed = "Unser Bibliotheksverwaltungssystem ist momentan wegen Wartungsarbeiten nicht verfügbar." ils_offline_holdings_message = "Bestandes- und Verfügbarkeitsinformationen können momentan leider nicht angezeigt werden. Wir entschuldigen uns für die Umstände und stehen für weitere Fragen gerne zur Verfügung:" diff --git a/languages/en.ini b/languages/en.ini index 3ec6681eb8b7db3f1330c7d02d3bafdc34893634..369d4810717d58a2863061d14d5eda4ab1b4b2ea 100644 --- a/languages/en.ini +++ b/languages/en.ini @@ -510,6 +510,7 @@ ill_request_processed = "Processed" ill_request_profile_html = "For interlibrary loan request information, please establish your <a href="%%url%%">Library Catalog Profile</a>." ill_request_submit_text = "Place Request" Illustrated = "Illustrated" +ils_account_create_error = "Your account could not be created in our library management system. If the problem persists, please contact your library." ils_action_unavailable = "The requested function is not available with the active library card." ils_connection_failed = "Connection to the library management system failed. Information related to your library account cannot be displayed. If the problem persists, please contact your library." ils_offline_holdings_message = "Holdings and item availability information is currently unavailable. Please accept our apologies for any inconvenience this may cause and contact us for further assistance:" diff --git a/module/VuFind/config/module.config.php b/module/VuFind/config/module.config.php index 0bc27f60044088c23ae06b9d75245e288b6eaae2..40ea0d9483f61ea0041a48ab78809070e22cea97 100644 --- a/module/VuFind/config/module.config.php +++ b/module/VuFind/config/module.config.php @@ -18,6 +18,20 @@ $config = [ ], ], ], + 'alma-webhook' => [ + 'type' => 'Zend\Router\Http\Segment', + 'options' => [ + 'route' => '/Alma/Webhook/[:almaWebhookAction]', + 'constraints' => [ + 'controller' => '[a-zA-Z][a-zA-Z0-9_-]*', + 'action' => '[a-zA-Z][a-zA-Z0-9_-]*', + ], + 'defaults' => [ + 'controller' => 'Alma', + 'action' => 'Webhook', + ], + ], + ], 'content-page' => [ 'type' => 'Zend\Router\Http\Segment', 'options' => [ @@ -106,6 +120,7 @@ $config = [ 'controllers' => [ 'factories' => [ 'VuFind\Controller\AjaxController' => 'VuFind\Controller\AjaxControllerFactory', + 'VuFind\Controller\AlmaController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\AlphabrowseController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\AuthorController' => 'VuFind\Controller\AbstractBaseFactory', 'VuFind\Controller\AuthorityController' => 'VuFind\Controller\AbstractBaseFactory', @@ -157,6 +172,8 @@ $config = [ 'aliases' => [ 'AJAX' => 'VuFind\Controller\AjaxController', 'ajax' => 'VuFind\Controller\AjaxController', + 'Alma' => 'VuFind\Controller\AlmaController', + 'alma' => 'VuFind\Controller\AlmaController', 'Alphabrowse' => 'VuFind\Controller\AlphabrowseController', 'alphabrowse' => 'VuFind\Controller\AlphabrowseController', 'Author' => 'VuFind\Controller\AuthorController', diff --git a/module/VuFind/src/VuFind/Auth/AlmaDatabase.php b/module/VuFind/src/VuFind/Auth/AlmaDatabase.php new file mode 100644 index 0000000000000000000000000000000000000000..d4e17a253b0cd2a957a8102d6ffdc6d03513a983 --- /dev/null +++ b/module/VuFind/src/VuFind/Auth/AlmaDatabase.php @@ -0,0 +1,142 @@ +<?php +/** + * Alma Database authentication class + * + * PHP version 5 + * + * Copyright (C) AK Bibliothek Wien für Sozialwissenschaften 2018. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Authentication + * @author Michael Birkner <michael.birkner@akwien.at> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki + */ +namespace VuFind\Auth; + +use VuFind\Exception\Auth as AuthException; + +/** + * Authentication class for Alma. The VuFind database and the Alma API are + * combined for authentication by this classe. + * + * @category VuFind + * @package Authentication + * @author Michael Birkner <michael.birkner@akwien.at> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:authentication_handlers Wiki + */ +class AlmaDatabase extends Database +{ + /** + * ILS Authenticator + * + * @var \VuFind\Auth\ILSAuthenticator + */ + protected $authenticator; + + /** + * Catalog connection + * + * @var \VuFind\ILS\Connection + */ + protected $catalog = null; + + /** + * Alma driver + * + * @var \VuFind\ILS\Driver\Alma + */ + protected $almaDriver = null; + + /** + * Alma config + * + * @var array + */ + protected $almaConfig = null; + + /** + * Constructor + * + * @param \VuFind\ILS\Connection $connection The ILS connection + * @param \VuFind\Auth\ILSAuthenticator $authenticator The ILS authenticator + */ + public function __construct( + \VuFind\ILS\Connection $connection, + \VuFind\Auth\ILSAuthenticator $authenticator + ) { + $this->catalog = $connection; + $this->authenticator = $authenticator; + $this->almaDriver = $connection->getDriver(); + $this->almaConfig = $connection->getDriverConfig(); + } + + /** + * Create a new user account in Alma AND in the VuFind Database. + * + * @param \Zend\Http\PhpEnvironment\Request $request Request object containing + * new account details. + * + * @return NULL|\VuFind\Db\Row\User New user row. + */ + public function create($request) + { + // When in privacy mode, don't create an Alma account and delegate + // further code execution to the parent. + if ($this->getConfig()->Authentication->privacy) { + return parent::create($request); + } + + // User variable + $user = null; + + // Collect POST parameters from request + $params = $this->collectParamsFromRequest($request); + + // Validate username and password + $this->validateUsernameAndPassword($params); + + // Get the user table + $userTable = $this->getUserTable(); + + // Make sure parameters are correct + $this->validateParams($params, $userTable); + + // Create user account in Alma + $almaAnswer = $this->almaDriver->createAlmaUser($params); + + // Create user account in VuFind user table if Alma gave us an answer + if ($almaAnswer !== null) { + // If we got this far, we're ready to create the account: + $user = $this->createUserFromParams($params, $userTable); + + // Add the Alma primary ID as cat_id to the VuFind user table + $user->cat_id = $almaAnswer->primary_id ?? null; + + // Save the new user to the user table + $user->save(); + + // Save the credentials to cat_username and cat_password to bypass + // the ILS login screen from VuFind + $user->saveCredentials($params['username'], $params['password']); + } else { + throw new AuthException($this->translate('ils_account_create_error')); + } + + return $user; + } +} diff --git a/module/VuFind/src/VuFind/Auth/Factory.php b/module/VuFind/src/VuFind/Auth/Factory.php index 8cb3f837239938131da24dfe6632a781c47193db..e7c773b7ca76fb057a9d6bdbdf819961c8346003 100644 --- a/module/VuFind/src/VuFind/Auth/Factory.php +++ b/module/VuFind/src/VuFind/Auth/Factory.php @@ -131,4 +131,19 @@ class Factory $sm->get('Zend\Session\SessionManager') ); } + + /** + * Construct the AlmaDatabase plugin. + * + * @param ServiceManager $sm Service manager. + * + * @return AlmaDatabase + */ + public static function getAlmaDatabase(ServiceManager $sm) + { + return new AlmaDatabase( + $sm->get('VuFind\ILS\Connection'), + $sm->get('VuFind\Auth\ILSAuthenticator') + ); + } } diff --git a/module/VuFind/src/VuFind/Auth/PluginManager.php b/module/VuFind/src/VuFind/Auth/PluginManager.php index 878e214c8f9a38fe1790962d1d86bfd858d7e5f2..f2ba6ecc9ebae9959623d3fa096215181b7e15c0 100644 --- a/module/VuFind/src/VuFind/Auth/PluginManager.php +++ b/module/VuFind/src/VuFind/Auth/PluginManager.php @@ -44,6 +44,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager * @var array */ protected $aliases = [ + 'almadatabase' => 'VuFind\Auth\AlmaDatabase', 'cas' => 'VuFind\Auth\CAS', 'choiceauth' => 'VuFind\Auth\ChoiceAuth', 'database' => 'VuFind\Auth\Database', @@ -65,6 +66,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager * @var array */ protected $factories = [ + 'VuFind\Auth\AlmaDatabase' => 'VuFind\Auth\Factory::getAlmaDatabase', 'VuFind\Auth\CAS' => 'Zend\ServiceManager\Factory\InvokableFactory', 'VuFind\Auth\ChoiceAuth' => 'VuFind\Auth\Factory::getChoiceAuth', 'VuFind\Auth\Database' => 'Zend\ServiceManager\Factory\InvokableFactory', diff --git a/module/VuFind/src/VuFind/Controller/AlmaController.php b/module/VuFind/src/VuFind/Controller/AlmaController.php new file mode 100644 index 0000000000000000000000000000000000000000..d2ef5671e71127aea822ce1262495348475b0f49 --- /dev/null +++ b/module/VuFind/src/VuFind/Controller/AlmaController.php @@ -0,0 +1,500 @@ +<?php +/** + * Alma controller + * + * PHP version 5 + * + * Copyright (C) AK Bibliothek Wien für Sozialwissenschaften 2018. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package Controller + * @author Michael Birkner <michael.birkner@akwien.at> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:controllers Wiki + */ +namespace VuFind\Controller; + +use Zend\ServiceManager\ServiceLocatorInterface; + +/** + * Alma controller, mainly for webhooks. + * + * @category VuFind + * @package Controller + * @author Michael Birkner <michael.birkner@akwien.at> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development:plugins:controllers Wiki + */ +class AlmaController extends AbstractBase +{ + /** + * Http service + * + * @var \VuFindHttp\HttpService + */ + protected $httpService; + + /** + * Http response + * + * @var \Zend\Http\PhpEnvironment\Response + */ + protected $httpResponse; + + /** + * Http headers + * + * @var \Zend\Http\Headers + */ + protected $httpHeaders; + + /** + * Configuration from config.ini + * + * @var \Zend\Config\Config + */ + protected $config; + + /** + * Alma.ini config + * + * @var \Zend\Config\Config + */ + protected $configAlma; + + /** + * User table + * + * @var \VuFind\Db\Table\User + */ + protected $userTable; + + /** + * Alma Controler constructor. + * + * @param ServiceLocatorInterface $sm The ServiceLocatorInterface + */ + public function __construct(ServiceLocatorInterface $sm) + { + parent::__construct($sm); + $this->httpResponse = $this->getResponse(); + $this->httpHeaders = $this->httpResponse->getHeaders(); + $this->config = $this->getConfig('config'); + $this->configAlma = $this->getConfig('Alma'); + $this->userTable = $this->getTable('user'); + } + + /** + * Action that is executed when the webhook page is called. + * + * @return \Zend\Http\Response|NULL + */ + public function webhookAction() + { + // Request from external + $request = $this->getRequest(); + + // Get request method (GET, POST, ...) + $requestMethod = $request->getMethod(); + + // Get request body if method is POST and is not empty + $requestBodyJson = null; + if ($request->getContent() != null + && !empty($request->getContent()) + && $requestMethod == 'POST' + ) { + try { + $this->checkMessageSignature($request); + } catch (\VuFind\Exception\Forbidden $ex) { + return $this->createJsonResponse( + 'Access to Alma Webhook is forbidden. ' . + 'The message signature is not correct.', 403 + ); + } + $requestBodyJson = json_decode($request->getContent()); + } + + // Get webhook action + $webhookAction = $requestBodyJson->action ?? null; + + // Perform webhook action + switch ($webhookAction) { + + case 'USER': + $accessPermission = 'access.alma.webhook.user'; + try { + $this->checkPermission($accessPermission); + } catch (\VuFind\Exception\Forbidden $ex) { + return $this->createJsonResponse( + 'Access to Alma Webhook \'' . $webhookAction . '\' forbidden. ' . + 'Set permission \'' . $accessPermission . + '\' in \'permissions.ini\'.', 403 + ); + } + + return $this->webhookUser($requestBodyJson); + break; + case 'JOB_END': + case 'NOTIFICATION': + case 'LOAN': + case 'REQUEST': + case 'BIB': + case 'ITEM': + return $this->webhookNotImplemented($webhookAction); + break; + default: + $accessPermission = 'access.alma.webhook.challenge'; + try { + $this->checkPermission($accessPermission); + } catch (\VuFind\Exception\Forbidden $ex) { + return $this->createJsonResponse( + 'Access to Alma Webhook challenge forbidden. Set permission \'' . + $accessPermission . '\' in \'permissions.ini\'.', 403 + ); + } + return $this->webhookChallenge(); + break; + } + } + + /** + * Webhook actions related to a newly created, updated or deleted user in Alma. + * + * @param mixed $requestBodyJson A JSON string decode with json_decode() + * + * @return NULL|\Zend\Http\Response + */ + protected function webhookUser($requestBodyJson) + { + + // Initialize user variable that should hold the user table row + $user = null; + + // Initialize response variable + $jsonResponse = null; + + // Get method from webhook (e. g. "create" for "new user") + $method = $requestBodyJson->webhook_user->method ?? null; + + // Get primary ID + $primaryId = $requestBodyJson->webhook_user->user->primary_id ?? null; + + if ($method == 'CREATE' || $method == 'UPDATE') { + // Get username (could e. g. be the barcode) + $username = null; + $userIdentifiers = $requestBodyJson->webhook_user->user->user_identifier + ?? null; + $idTypeConfig = $this->configAlma->NewUser->idType ?? null; + foreach ($userIdentifiers as $userIdentifier) { + $idTypeHook = $userIdentifier->id_type->value ?? null; + if ($idTypeHook != null + && $idTypeHook == $idTypeConfig + && $username == null + ) { + $username = $userIdentifier->value ?? null; + } + } + + // Use primary ID as username as a fallback if no other + // username ID is available + $username = ($username == null) ? $primaryId : $username; + + // Get user details from Alma Webhook message + $firstname = $requestBodyJson->webhook_user->user->first_name ?? null; + $lastname = $requestBodyJson->webhook_user->user->last_name ?? null; + + $allEmails = $requestBodyJson->webhook_user->user->contact_info->email + ?? null; + $email = null; + foreach ($allEmails as $currentEmail) { + $preferred = $currentEmail->preferred ?? false; + if ($preferred && $email == null) { + $email = $currentEmail->email_address ?? null; + } + } + + if ($method == 'CREATE') { + $user = $this->userTable->getByUsername($username, true); + } + + if ($method == 'UPDATE') { + $user = $this->userTable->getByCatalogId($primaryId); + } + + if ($user) { + $user->username = $username; + $user->firstname = $firstname; + $user->lastname = $lastname; + $user->email = $email; + $user->cat_id = $primaryId; + $user->cat_username = $username; + + try { + $user->save(); + if ($method == 'CREATE') { + $this->sendSetPasswordEmail($user, $this->config); + } + $jsonResponse = $this->createJsonResponse( + 'Successfully ' . strtolower($method) . + 'd user with primary ID \'' . $primaryId . + '\' | username \'' . $username . '\'.', 200 + ); + } catch (\Exception $ex) { + $jsonResponse = $this->createJsonResponse( + 'Error when saving new user with primary ID \'' . + $primaryId . '\' | username \'' . $username . + '\' to VuFind database and sending the welcome email: ' . + $ex->getMessage() . '. ', + 400 + ); + } + } else { + $jsonResponse = $this->createJsonResponse( + 'User with primary ID \'' . $primaryId . '\' | username \'' . + $username . '\' was not found in VuFind database and ' . + 'therefore could not be ' . strtolower($method) . 'd.', + 404 + ); + } + } elseif ($method == 'DELETE') { + $user = $this->userTable->getByCatalogId($primaryId); + if ($user) { + $rowsAffected = $user->delete(); + if ($rowsAffected == 1) { + $jsonResponse = $this->createJsonResponse( + 'Successfully deleted use with primary ID \'' . $primaryId . + '\' in VuFind.', 200 + ); + } else { + $jsonResponse = $this->createJsonResponse( + 'Problem when deleting user with \'' . $primaryId . + '\' in VuFind. It is expected that only 1 row of the ' . + 'VuFind user table is affected by the deletion. But ' . + $rowsAffected . ' were affected. Please check the status ' . + 'of the user in the VuFind database.', 400 + ); + } + } else { + $jsonResponse = $this->createJsonResponse( + 'User with primary ID \'' . $primaryId . '\' was not found in ' . + 'VuFind database and therefore could not be deleted.', 404 + ); + } + } + + return $jsonResponse; + } + + /** + * The webhook challenge. This is used to activate the webhook in Alma. Without + * activating it, Alma will not send its webhook messages to VuFind. + * + * @return \Zend\Http\Response + */ + protected function webhookChallenge() + { + // Get challenge string from the get parameter that Alma sends us. We need to + // return this string in the return message. + $secret = $this->params()->fromQuery('challenge'); + + // Create the return array + $returnArray = []; + + if (isset($secret) && !empty(trim($secret))) { + $returnArray['challenge'] = $secret; + $this->httpResponse->setStatusCode(200); + } else { + $returnArray['error'] = 'GET parameter \'challenge\' is empty, not ' . + 'set or not available when receiving webhook challenge from Alma.'; + $this->httpResponse->setStatusCode(500); + } + + // Remove null from array + $returnArray = array_filter($returnArray); + + // Create return JSON value and set it to the response + $returnJson = json_encode($returnArray, JSON_PRETTY_PRINT); + $this->httpHeaders->addHeaderLine('Content-type', 'application/json'); + $this->httpResponse->setContent($returnJson); + + return $this->httpResponse; + } + + /** + * Send the "set password email" to a new user that was created in Alma and sent + * to VuFind via webhook. + * + * @param \VuFind\Db\Row\User $user A user row object from the VuFind + * user table. + * @param \Zend\Config\Config $config A config object of config.ini + * + * @return void + */ + protected function sendSetPasswordEmail($user, $config) + { + // If we can't find a user + if (null == $user) { + error_log( + 'Could not send the email to new user for setting the ' . + 'password because the user object was not found.' + ); + } else { + // Attempt to send the email + try { + // Create a fresh hash + $user->updateHash(); + $config = $this->getConfig(); + $renderer = $this->getViewRenderer(); + $method = $this->getAuthManager()->getAuthMethod(); + + // Custom template for emails (text-only) + $message = $renderer->render( + 'Email/new-user-welcome.phtml', [ + 'library' => $config->Site->title, + 'firstname' => $user->firstname, + 'lastname' => $user->lastname, + 'username' => $user->username, + 'url' => $this->getServerUrl('myresearch-verify') . '?hash=' . + $user->verify_hash . '&auth_method=' . $method + ] + ); + // Send the email + $this->serviceLocator->get('VuFind\Mailer\Mailer')->send( + $user->email, $config->Site->email, + $this->translate( + 'new_user_welcome_subject', + ['%%library%%' => $config->Site->title] + ), + $message + ); + } catch (\VuFind\Exception\Mail $e) { + error_log( + 'Could not send the \'set-password-email\' to user with ' . + 'primary ID \'' . $user->cat_id . '\' | username \'' . + $user->username . '\': ' . $e->getMessage() + ); + } + } + } + + /** + * Create a HTTP response with JSON content and HTTP status codes that Alma takes + * as "answer" to its webhook calls. + * + * @param string $text The text that should be sent back to Alma + * @param int $httpStatusCode The HTTP status code that should be sent back + * to Alma + * + * @return \Zend\Http\Response + */ + protected function createJsonResponse($text, $httpStatusCode) + { + $returnArray = []; + $returnArray[] = $text; + $returnJson = json_encode($returnArray, JSON_PRETTY_PRINT); + $this->httpHeaders->addHeaderLine('Content-type', 'application/json'); + $this->httpResponse->setStatusCode($httpStatusCode); + $this->httpResponse->setContent($returnJson); + return $this->httpResponse; + } + + /** + * A default message to be sent back to Alma if an action for a certain webhook + * type is not implemented (yet). + * + * @param string $webhookType The type of the webhook + * + * @return \Zend\Http\Response + */ + protected function webhookNotImplemented($webhookType) + { + return $this->createJsonResponse( + $webhookType . ' Alma Webhook is not (yet) implemented in VuFind.', 400 + ); + } + + /** + * Helper function to check access permissions defined in permissions.ini. + * The function validateAccessPermission() will throw an exception that can be + * catched when the permission is denied. + * + * @param string $accessPermission The permission name from permissions.ini that + * should be checked. + * + * @return void + */ + protected function checkPermission($accessPermission) + { + $this->accessPermission = $accessPermission; + $this->accessDeniedBehavior = 'exception'; + $this->validateAccessPermission($this->getEvent()); + } + + /** + * Signing and hashing the body content of the Alma POST request with the + * webhook secret in Alma.ini. The calculated hash value must be the same as + * the 'X-Exl-Signature' in the request header. This is a security measure to + * be sure that the request comes from Alma. + * + * @param \Zend\Stdlib\RequestInterface $request The request from Alma. + * + * @throws \VuFind\Exception\Forbidden Throws forbidden exception + * if hash values are not the + * same. + * + * @return void + */ + protected function checkMessageSignature(\Zend\Stdlib\RequestInterface $request) + { + // Get request content + $requestBodyString = $request->getContent(); + + // Get hashed message signature from request header of Alma webhook request + $almaSignature = ($request->getHeaders()->get('X-Exl-Signature')) + ? $request->getHeaders()->get('X-Exl-Signature')->getFieldValue() + : null; + + // Get the webhook secret defined in Alma.ini + $secretConfig = $this->configAlma->Webhook->secret ?? null; + + // Calculate hmac-sha256 hash from request body we get from Alma webhook and + // sign it with the Alma webhook secret from Alma.ini + $calculatedHash = base64_encode( + hash_hmac( + 'sha256', + $requestBodyString, + $secretConfig, + true + ) + ); + + // Check for correct signature + if ($almaSignature != $calculatedHash) { + error_log( + '[Alma] Unauthorized: Signature value not correct! ' . + 'Hash from Alma: "' . $almaSignature . '". ' . + 'Calculated hash: "' . $calculatedHash . '". ' . + 'Body content for calculating the hash was: ' . + '"' . json_encode( + json_decode($requestBodyString), + JSON_UNESCAPED_UNICODE | + JSON_UNESCAPED_SLASHES + ) . '"' + ); + throw new \VuFind\Exception\Forbidden; + } + } +} diff --git a/module/VuFind/src/VuFind/ILS/Driver/Alma.php b/module/VuFind/src/VuFind/ILS/Driver/Alma.php index f826980a074f8b47c315dd40d532763082b4d354..ca16b3407792ad6516a2c5768112ba0db975cd51 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/Alma.php +++ b/module/VuFind/src/VuFind/ILS/Driver/Alma.php @@ -27,6 +27,10 @@ */ namespace VuFind\ILS\Driver; +use SimpleXMLElement; +use VuFind\Exception\ILS as ILSException; +use Zend\Http\Headers; + /** * Alma ILS Driver * @@ -39,6 +43,7 @@ namespace VuFind\ILS\Driver; class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface { use \VuFindHttp\HttpServiceAwareTrait; + use CacheTrait; /** * Alma API base URL. @@ -54,16 +59,32 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface */ protected $apiKey; + /** + * Date converter + * + * @var \VuFind\Date\Converter + */ protected $dateConverter; + /** + * Configuration loader + * + * @var \VuFind\Config\PluginManager + */ + protected $configLoader; + /** * Constructor * - * @param \VuFind\Date\Converter $dateConverter Date converter object + * @param \VuFind\Date\Converter $dateConverter Date converter object + * @param \VuFind\Config\PluginManager $configLoader Plugin manager */ - public function __construct(\VuFind\Date\Converter $dateConverter) - { + public function __construct( + \VuFind\Date\Converter $dateConverter, + \VuFind\Config\PluginManager $configLoader + ) { $this->dateConverter = $dateConverter; + $this->configLoader = $configLoader; } /** @@ -87,30 +108,109 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface /** * Make an HTTP request against Alma * - * @param string $path Path to retrieve from API (excluding base URL/API key) - * @param string $params Additional GET params + * @param string $path Path to retrieve from API (excluding base + * URL/API key) + * @param array $paramsGet Additional GET params + * @param array $paramsPost Additional POST params + * @param string $method GET or POST. Default is GET. + * @param string $rawBody Request body. + * @param Headers|array $headers Add headers to the call. * - * @return \SimpleXMLElement + * @throws ILSException + * @return NULL|SimpleXMLElement */ - protected function makeRequest($path, $params = []) - { - // TODO: Support requests of different methods - if (!isset($params['apiKey'])) { - $params['apiKey'] = $this->apiKey; - } - $url = strpos($path, '://') === false ? $this->baseUrl . $path : $path; - $client = $this->httpService->createClient($url); - $client->setParameterGet($params); - $result = $client->send(); + protected function makeRequest( + $path, + $paramsGet = [], + $paramsPost = [], + $method = 'GET', + $rawBody = null, + $headers = null + ) { + // Set some variables + $result = null; + $statusCode = null; + $returnValue = null; + + try { + // Set API key if it is not already available in the GET params + if (!isset($paramsGet['apiKey'])) { + $paramsGet['apiKey'] = $this->apiKey; + } + + // Create the API URL + $url = strpos($path, '://') === false ? $this->baseUrl . $path : $path; + + // Create client with API URL + $client = $this->httpService->createClient($url); + + // Set method + $client->setMethod($method); + + // Set other GET parameters + if ($method == 'GET') { + $client->setParameterGet($paramsGet); + } else { + // Always set API key as GET parameter + $client->setParameterGet(['apiKey' => $paramsGet['apiKey']]); + + // Set POST parameters + if ($method == 'POST') { + $client->setParameterPost($paramsPost); + } + } + + // Set body if applicable + if (isset($rawBody)) { + $client->setRawBody($rawBody); + } + + // Set headers if applicable + if (isset($headers)) { + $client->setHeaders($headers); + } + + // Execute HTTP call + $result = $client->send(); + } catch (\Exception $e) { + throw new ILSException($e->getMessage()); + } + + // Get the HTTP status code + $statusCode = $result->getStatusCode(); + + // Check for error + if ($result->isServerError()) { + throw new ILSException('HTTP error code: ' . $statusCode, $statusCode); + } + + $answer = $result->getBody(); + $answer = str_replace('xmlns=', 'ns=', $answer); + $xml = simplexml_load_string($answer); + if ($result->isSuccess()) { - return simplexml_load_string($result->getBody()); + if (!$xml && $result->isServerError()) { + throw new ILSException( + 'XML is not valid or HTTP error, URL: ' . $url . + ', HTTP status code: ' . $statusCode, $statusCode + ); + } + $returnValue = $xml; } else { - // TODO: Throw an error - error_log($client->getUri()); - error_log(print_r($params, true)); - error_log($result->getBody()); + $almaErrorMsg = $xml->errorList->error[0]->errorMessage; + error_log( + '[ALMA] ' . $almaErrorMsg . ' | Call to: ' . $client->getUri() . + '. GET params: ' . var_export($paramsGet, true) . '. POST params: ' . + var_export($paramsPost, true) . '. Result body: ' . + $result->getBody() . '. HTTP status code: ' . $statusCode + ); + throw new ILSException( + 'Alma error message: ' . $almaErrorMsg . ' | HTTP error code: ' . + $statusCode, $statusCode + ); } - return null; + + return $returnValue; } /** @@ -134,75 +234,403 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface * @param string $id The record id to retrieve the holdings for * @param array $patron Patron data * - * @return array On success, an associative array with the following - * keys: id, availability (boolean), status, location, reserve, callnumber, - * duedate, number, barcode. + * @return array On success an associative array with the following keys: + * id, source, availability (boolean), status, location, + * reserve, callnumber, duedate, returnDate, number, + * barcode, item_notes, item_id, holding_id, addLink. */ public function getHolding($id, array $patron = null) { + // Get config data: + $fulfillementUnits = $this->config['FulfillmentUnits'] ?? null; + $requestableConfig = $this->config['Requestable'] ?? null; + $results = []; $copyCount = 0; $bibPath = '/bibs/' . urlencode($id) . '/holdings'; if ($holdings = $this->makeRequest($bibPath)) { foreach ($holdings->holding as $holding) { $holdingId = (string)$holding->holding_id; + $locationCode = (string)$holding->location; + $addLink = false; + if ($fulfillementUnits != null && $requestableConfig != null) { + $addLink = $this->requestsAllowed( + $fulfillementUnits, + $locationCode, + $requestableConfig, + $patron + ); + } + $itemPath = $bibPath . '/' . urlencode($holdingId) . '/items'; if ($currentItems = $this->makeRequest($itemPath)) { foreach ($currentItems->item as $item) { + $itemId = (string)$item->item_data->pid; $barcode = (string)$item->item_data->barcode; + $processType = (string)$item->item_data->process_type; + $itemNotes = null; + if ($item->item_data->public_note != null + && !empty($item->item_data->public_note) + ) { + $itemNotes = [(string)$item->item_data->public_note]; + } + $requested = ((string)$item->item_data->requested == 'false') + ? false + : true; + + $number = ++$copyCount; + $description = null; + if ($item->item_data->description != null + && !empty($item->item_data->description) + ) { + $number = (string)$item->item_data->description; + $description = (string)$item->item_data->description; + } + + // For some data we need to do additional API calls + // due to the Alma API architecture + $duedate = ($requested) ? 'requested' : null; + if ($processType == 'LOAN' && !$requested) { + $loanDataPath = '/bibs/' . urlencode($id) . '/holdings/' + . urlencode($holdingId) . '/items/' + . urlencode($itemId) . '/loans'; + $loanData = $this->makeRequest($loanDataPath); + $loan = $loanData->item_loan; + $duedate = $this->parseDate((string)$loan->due_date); + } + $results[] = [ 'id' => $id, 'source' => 'Solr', 'availability' => $this->getAvailabilityFromItem($item), - 'status' => (string)$item->item_data->base_status[0] - ->attributes()['desc'], - 'location' => (string)$holding->library[0] + 'status' => (string)$item + ->item_data + ->base_status[0] ->attributes()['desc'], + 'location' => $locationCode, 'reserve' => 'N', // TODO: support reserve status 'callnumber' => (string)$item->holding_data->call_number, - 'duedate' => null, // TODO: support due dates + 'duedate' => $duedate, 'returnDate' => false, // TODO: support recent returns - 'number' => ++$copyCount, + 'number' => $number,//++$copyCount, 'barcode' => empty($barcode) ? 'n/a' : $barcode, - 'item_id' => (string)$item->item_data->pid, + 'item_notes' => $itemNotes, + 'item_id' => $itemId, 'holding_id' => $holdingId, - 'addLink' => 'check' + 'addLink' => $addLink, + // For Alma title-level hold requests + 'description' => $description ]; } } } } + return $results; } + /** + * Check if the user is allowed to place requests for an Alma fulfillment + * unit in general. We check for blocks on the patron account that could + * block a request in getRequestBlocks(). + * + * @param array $fulfillementUnits An array of fulfillment units and associated + * locations from Alma.ini (see section + * [FulfillmentUnits]) + * @param string $locationCode The location code of the holding to be + * checked + * @param array $requestableConfig An array of fulfillment units and associated + * patron groups and their request policy from + * Alma.ini (see section [Requestable]) + * @param array $patron An array with the patron details (username + * and password) + * + * @return boolean true if the the patron is allowed to place + * requests on holdings of this fulfillment + * unit, false otherwise. + * @author Michael Birkner + */ + protected function requestsAllowed( + $fulfillementUnits, + $locationCode, + $requestableConfig, + $patron + ) { + $requestsAllowed = false; + + // Get user group code + $cacheId = 'alma|user|' . $patron['cat_username'] . '|group_code'; + $userGroupCode = $this->getCachedData($cacheId); + if ($userGroupCode === null) { + $profile = $this->getMyProfile($patron); + $userGroupCode = (string)$profile['group_code']; + } + + // Get the fulfillment unit of the location. + $locationFulfillmentUnit = $this->getFulfillmentUnitByLocation( + $locationCode, + $fulfillementUnits + ); + + // Check if the group of the currently logged in user is allowed to place + // requests on items belonging to current fulfillment unit + if (($locationFulfillmentUnit != null && !empty($locationFulfillmentUnit)) + && ($userGroupCode != null && !empty($userGroupCode)) + ) { + $requestsAllowed = false; + if ($requestableConfig[$locationFulfillmentUnit][$userGroupCode] == 'Y' + ) { + $requestsAllowed = true; + } + } + + return $requestsAllowed; + } + + /** + * Check for request blocks. + * + * @param array $patron The patron array with username and password + * + * @return array|boolean An array of block messages or false if there are no + * blocks + * @author Michael Birkner + */ + public function getRequestBlocks($patron) + { + return $this->getAccountBlocks($patron); + } + + /** + * Check for account blocks in Alma and cache them. + * + * @param array $patron The patron array with username and password + * + * @return array|boolean An array of block messages or false if there are no + * blocks + * @author Michael Birkner + */ + public function getAccountBlocks($patron) + { + $patronId = $patron['cat_username']; + $cacheId = 'alma|user|' . $patronId . '|blocks'; + $cachedBlocks = $this->getCachedData($cacheId); + if ($cachedBlocks !== null) { + return $cachedBlocks; + } + + $xml = $this->makeRequest('/users/' . $patron['cat_username']); + if ($xml == null || empty($xml)) { + return false; + } + + $userBlocks = $xml->user_blocks->user_block; + if ($userBlocks == null || empty($userBlocks)) { + return false; + } + + $blocks = []; + foreach ($userBlocks as $block) { + $blockStatus = (string)$block->block_status; + if ($blockStatus === 'ACTIVE') { + $blockNote = (isset($block->block_note)) + ? (string)$block->block_note + : null; + $blockDesc = (string)$block->block_description->attributes()->desc; + $blockDesc = ($blockNote != null) + ? $blockDesc . '. ' . $blockNote + : $blockDesc; + $blocks[] = $blockDesc; + } + } + + if (!empty($blocks)) { + $this->putCachedData($cacheId, $blocks); + return $blocks; + } else { + $this->putCachedData($cacheId, false); + return false; + } + } + + /** + * Get an Alma fulfillment unit by an Alma location. + * + * @param string $locationCode A location code, e. g. "SCI" + * @param array $fulfillmentUnits An array of fulfillment units with all its + * locations. + * + * @return string|NULL Null if the location was not found or a + * string specifying the fulfillment unit of + * the location that was found. + * @author Michael Birkner + */ + protected function getFulfillmentUnitByLocation($locationCode, $fulfillmentUnits) + { + foreach ($fulfillmentUnits as $key => $val) { + if (array_search($locationCode, $val) !== false) { + return $key; + } + } + return null; + } + + /** + * Create a user in Alma via API call + * + * @param array $formParams The data from the "create new account" form + * + * @throws \VuFind\Exception\Auth + * + * @return NULL|SimpleXMLElement + * @author Michael Birkner + */ + public function createAlmaUser($formParams) + { + + // Get config for creating new Alma users from Alma.ini + $newUserConfig = $this->config['NewUser']; + + // Check if config params are all set + $configParams = [ + 'recordType', 'userGroup', 'preferredLanguage', + 'accountType', 'status', 'emailType', 'idType' + ]; + foreach ($configParams as $configParam) { + if (!isset($newUserConfig[$configParam]) + || empty(trim($newUserConfig[$configParam])) + ) { + $errorMessage = 'Configuration "' . $configParam . '" is not set ' . + 'in Alma.ini in the [NewUser] section!'; + error_log('[ALMA]: ' . $errorMessage); + throw new \VuFind\Exception\Auth($errorMessage); + } + } + + // Calculate expiry date based on config in Alma.ini + $dateNow = new \DateTime('now'); + $expiryDate = null; + if (isset($newUserConfig['expiryDate']) + && !empty(trim($newUserConfig['expiryDate'])) + ) { + try { + $expiryDate = $dateNow->add( + new \DateInterval($newUserConfig['expiryDate']) + ); + } catch (\Exception $exception) { + $errorMessage = 'Configuration "expiryDate" in Alma.ini (see ' . + '[NewUser] section) has the wrong format!'; + error_log('[ALMA]: ' . $errorMessage); + throw new \VuFind\Exception\Auth($errorMessage); + } + } else { + $expiryDate = $dateNow->add(new \DateInterval('P1Y')); + } + $expiryDateXml = ($expiryDate != null) + ? '<expiry_date>' . $expiryDate->format('Y-m-d') . 'Z</expiry_date>' + : ''; + + // Calculate purge date based on config in Alma.ini + $purgeDate = null; + if (isset($newUserConfig['purgeDate']) + && !empty(trim($newUserConfig['purgeDate'])) + ) { + try { + $purgeDate = $dateNow->add( + new \DateInterval($newUserConfig['purgeDate']) + ); + } catch (\Exception $exception) { + $errorMessage = 'Configuration "purgeDate" in Alma.ini (see ' . + '[NewUser] section) has the wrong format!'; + error_log('[ALMA]: ' . $errorMessage); + throw new \VuFind\Exception\Auth($errorMessage); + } + } + $purgeDateXml = ($purgeDate != null) + ? '<purge_date>' . $purgeDate->format('Y-m-d') . 'Z</purge_date>' + : ''; + + // Create user XML for Alma API + $userXml = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' + . '<user>' + . '<record_type>' . $this->config['NewUser']['recordType'] . '</record_type>' + . '<first_name>' . $formParams['firstname'] . '</first_name>' + . '<last_name>' . $formParams['lastname'] . '</last_name>' + . '<user_group>' . $this->config['NewUser']['userGroup'] . '</user_group>' + . '<preferred_language>' . $this->config['NewUser']['preferredLanguage'] . + '</preferred_language>' + . $expiryDateXml + . $purgeDateXml + . '<account_type>' . $this->config['NewUser']['accountType'] . + '</account_type>' + . '<status>' . $this->config['NewUser']['status'] . '</status>' + . '<contact_info>' + . '<emails>' + . '<email preferred="true">' + . '<email_address>' . $formParams['email'] . '</email_address>' + . '<email_types>' + . '<email_type>' . $this->config['NewUser']['emailType'] . '</email_type>' + . '</email_types>' + . '</email>' + . '</emails>' + . '</contact_info>' + . '<user_identifiers>' + . '<user_identifier>' + . '<id_type>' . $this->config['NewUser']['idType'] . '</id_type>' + . '<value>' . $formParams['username'] . '</value>' + . '</user_identifier>' + . '</user_identifiers>' + . '</user>'; + + // Remove whitespaces from XML + $userXml = preg_replace("/\n/i", "", $userXml); + $userXml = preg_replace("/>\s*</i", "><", $userXml); + + // Create user in Alma + $almaAnswer = $this->makeRequest( + '/users', + [], + [], + 'POST', + $userXml, + ['Content-Type' => 'application/xml'] + ); + + // Return the XML from Alma on success. On error, an exception is thrown + // in makeRequest + return $almaAnswer; + } + /** * Patron Login * * This is responsible for authenticating a patron against the catalog. * - * @param string $barcode The patron barcode - * @param string $password The patron password + * @param string $barcode The patrons barcode. + * @param string $password The patrons password. * - * @throws ILSException - * @return mixed Associative array of patron info on successful login, - * null on unsuccessful login. + * @return string[]|NULL */ public function patronLogin($barcode, $password) { - $client = $this->httpService->createClient( - $this->baseUrl . '/users/' . $barcode - . '?apiKey=' . urlencode($this->apiKey) - . '&op=auth&password=' . urlencode(trim($password)) - ); - $client->setMethod(\Zend\Http\Request::METHOD_POST); - $response = $client->send(); - // Test once we have POST access - if ($response->isSuccess()) { + // Create array of get parameters for API call + $getParams = [ + 'user_id_type' => 'all_unique', + 'view' => 'brief', + 'expand' => 'none' + ]; + + // Check for patron in Alma + $response = $this->makeRequest('/users/' . urlencode($barcode), $getParams); + + // Test once we have access + if ($response != null) { return [ 'cat_username' => trim($barcode), 'cat_password' => trim($password) ]; } + return null; } @@ -217,30 +645,59 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface */ public function getMyProfile($patron) { - $xml = $this->makeRequest('/users/' . $patron['cat_username']); + $patronId = $patron['cat_username']; + $xml = $this->makeRequest('/users/' . $patronId); if (empty($xml)) { return []; } $profile = [ - 'firstname' => $xml->first_name, - 'lastname' => $xml->last_name, - 'group' => $xml->user_group['desc'] + 'firstname' => (isset($xml->first_name)) + ? (string)$xml->first_name + : null, + 'lastname' => (isset($xml->last_name)) + ? (string)$xml->last_name + : null, + 'group' => (isset($xml->user_group['desc'])) + ? (string)$xml->user_group['desc'] + : null, + 'group_code' => (isset($xml->user_group)) + ? (string)$xml->user_group + : null ]; $contact = $xml->contact_info; if ($contact) { if ($contact->addresses) { $address = $contact->addresses[0]->address; - $profile['address1'] = $address->line1; - $profile['address2'] = $address->line2; - $profile['address3'] = $address->line3; - $profile['zip'] = $address->postal_code; - $profile['city'] = $address->city; - $profile['country'] = $address->country; + $profile['address1'] = (isset($address->line1)) + ? (string)$address->line1 + : null; + $profile['address2'] = (isset($address->line2)) + ? (string)$address->line2 + : null; + $profile['address3'] = (isset($address->line3)) + ? (string)$address->line3 + : null; + $profile['zip'] = (isset($address->postal_code)) + ? (string)$address->postal_code + : null; + $profile['city'] = (isset($address->city)) + ? (string)$address->city + : null; + $profile['country'] = (isset($address->country)) + ? (string)$address->country + : null; } if ($contact->phones) { - $profile['phone'] = $contact->phones[0]->phone->phone_number; + $profile['phone'] = (isset($contact->phones[0]->phone->phone_number)) + ? (string)$contact->phones[0]->phone->phone_number + : null; } } + + // Cache the user group code + $cacheId = 'alma|user|' . $patronId . '|group_code'; + $this->putCachedData($cacheId, $profile['group_code'] ?? null); + return $profile; } @@ -260,15 +717,17 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface ); $fineList = []; foreach ($xml as $fee) { - $checkout = (string) $fee->status_time; + $checkout = (string)$fee->status_time; $fineList[] = [ - "title" => (string) $fee->type, + "title" => (string)$fee->type, "amount" => $fee->original_amount * 100, "balance" => $fee->balance * 100, "checkout" => $this->dateConverter->convert( - 'Y-m-d H:i', 'm-d-Y', $checkout + 'Y-m-d H:i', + 'm-d-Y', + $checkout ), - "fine" => (string) $fee->type['desc'] + "fine" => (string)$fee->type['desc'] ]; } return $fineList; @@ -294,15 +753,15 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface $holdList = []; foreach ($xml as $request) { $holdList[] = [ - 'create' => (string) $request->request_date, - 'expire' => (string) $request->last_interest_date, - 'id' => (string) $request->request_id, - 'in_transit' => $request->request_status !== 'IN_PROCESS', - 'item_id' => (string) $request->mms_id, - 'location' => (string) $request->pickup_location, + 'create' => (string)$request->request_date, + 'expire' => (string)$request->last_interest_date, + 'id' => (string)$request->request_id, + 'in_transit' => (string)$request->request_status !== 'On Hold Shelf', + 'item_id' => (string)$request->mms_id, + 'location' => (string)$request->pickup_location, 'processed' => $request->item_policy === 'InterlibraryLoan' - && $request->request_status !== 'NOT_STARTED', - 'title' => (string) $request->title, + && (string)$request->request_status !== 'Not Started', + 'title' => (string)$request->title, /* // VuFind keys 'available' => $request->, @@ -336,6 +795,92 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface return $holdList; } + /** + * Cancel hold requests. + * + * @param array $cancelDetails An associative array with two keys: patron + * (array returned by the driver's + * patronLogin method) and details (an array + * of strings eturned by the driver's + * getCancelHoldDetails method) + * + * @return array Associative array containing with keys 'count' + * (number of items successfully cancelled) and + * 'items' (array of successfull cancellations). + */ + public function cancelHolds($cancelDetails) + { + $returnArray = []; + $patronId = $cancelDetails['patron']['cat_username']; + $count = 0; + + foreach ($cancelDetails['details'] as $requestId) { + $item = []; + try { + // Get some details of the requested items as we need them below. + // We only can get them from an API request. + $requestDetails = $this->makeRequest( + $this->baseUrl . + '/users/' . urlencode($patronId) . + '/requests/' . urlencode($requestId) + ); + + $mmsId = (isset($requestDetails->mms_id)) + ? (string)$requestDetails->mms_id + : (string)$requestDetails->mms_id; + + // Delete the request in Alma + $apiResult = $this->makeRequest( + $this->baseUrl . + '/users/' . urlencode($patronId) . + '/requests/' . urlencode($requestId), + ['reason' => 'CancelledAtPatronRequest'], + [], + 'DELETE' + ); + + // Adding to "count" variable and setting values to return array + $count++; + $item[$mmsId]['success'] = true; + $item[$mmsId]['status'] = 'hold_cancel_success'; + } catch (ILSException $e) { + if (isset($apiResult['xml'])) { + $almaErrorCode = $apiResult['xml']->errorList->error->errorCode; + $sysMessage = $apiResult['xml']->errorList->error->errorMessage; + } else { + $almaErrorCode = 'No error code available'; + $sysMessage = 'HTTP status code: ' . + ($e->getCode() ?? 'Code not available'); + } + $item[$mmsId]['success'] = false; + $item[$mmsId]['status'] = 'hold_cancel_fail'; + $item[$mmsId]['sysMessage'] = $sysMessage . '. ' . + 'Alma MMS ID: ' . $mmsId . '. ' . + 'Alma request ID: ' . $requestId . '. ' . + 'Alma error code: ' . $almaErrorCode; + } + + $returnArray['items'] = $item; + } + + $returnArray['count'] = $count; + + return $returnArray; + } + + /** + * Get details of a single hold request. + * + * @param array $holdDetails One of the item arrays returned by the + * getMyHolds method + * + * @return string The Alma request ID + */ + public function getCancelHoldDetails($holdDetails) + { + return $holdDetails['id']; + } + /** * Get Patron Storage Retrieval Requests * @@ -416,6 +961,164 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface return $holdList; } + /** + * Get transactions of the current patron. + * + * @param array $patron The patron array from patronLogin + * + * @return string[] Transaction information as array or empty array if the + * patron has no transactions. + * + * @author Michael Birkner + */ + public function getMyTransactions($patron) + { + // Defining the return value + $returnArray = []; + + // Get the patrons user name + $patronUserName = $patron['cat_username']; + + // Create a timestamp for calculating the due / overdue status + $nowTS = mktime(); + + // Create parameters for the API call + // INFO: "order_by" does not seem to work as expected! + // This is an Alma API problem. + $params = [ + 'limit' => '100', + 'order_by' => 'due_date', + 'direction' => 'DESC', + 'expand' => 'renewable' + ]; + + // Get user loans from Alma API + $apiResult = $this->makeRequest( + '/users/' . $patronUserName . '/loans/', + $params + ); + + // If there is an API result, process it + if ($apiResult) { + // Iterate over all item loans + foreach ($apiResult->item_loan as $itemLoan) { + $loan['duedate'] = $this->parseDate( + (string)$itemLoan->due_date, + true + ); + //$loan['dueTime'] = ; + $loan['dueStatus'] = null; // Calculated below + $loan['id'] = (string)$itemLoan->mms_id; + //$loan['source'] = 'Solr'; + $loan['barcode'] = (string)$itemLoan->item_barcode; + //$loan['renew'] = ; + //$loan['renewLimit'] = ; + //$loan['request'] = ; + //$loan['volume'] = ; + $loan['publication_year'] = (string)$itemLoan->publication_year; + $loan['renewable'] + = (strtolower((string)$itemLoan->renewable) == 'true') + ? true + : false; + //$loan['message'] = ; + $loan['title'] = (string)$itemLoan->title; + $loan['item_id'] = (string)$itemLoan->loan_id; + $loan['institution_name'] = (string)$itemLoan->library; + //$loan['isbn'] = ; + //$loan['issn'] = ; + //$loan['oclc'] = ; + //$loan['upc'] = ; + $loan['borrowingLocation'] = (string)$itemLoan->circ_desk; + + // Calculate due status + $dueDateTS = strtotime($loan['duedate']); + if ($nowTS > $dueDateTS) { + // Loan is overdue + $loan['dueStatus'] = 'overdue'; + } elseif (($dueDateTS - $nowTS) < 86400) { + // Due date within one day + $loan['dueStatus'] = 'due'; + } + + $returnArray[] = $loan; + } + } + + return $returnArray; + } + + /** + * Get Alma loan IDs for use in renewMyItems. + * + * @param array $checkOutDetails An array from getMyTransactions + * + * @return string The Alma loan ID for this loan + * + * @author Michael Birkner + */ + public function getRenewDetails($checkOutDetails) + { + $loanId = $checkOutDetails['item_id']; + return $loanId; + } + + /** + * Renew loans via Alma API. + * + * @param array $renewDetails An array with the IDs of the loans returned by + * getRenewDetails and the patron information + * returned by patronLogin. + * + * @return array[] An array with the renewal details and a success or error + * message. + * + * @author Michael Birkner + */ + public function renewMyItems($renewDetails) + { + $returnArray = []; + $patronUserName = $renewDetails['patron']['cat_username']; + + foreach ($renewDetails['details'] as $loanId) { + // Create an empty array that holds the information for a renewal + $renewal = []; + + try { + // POST the renewals to Alma + $apiResult = $this->makeRequest( + '/users/' . $patronUserName . '/loans/' . $loanId . '/?op=renew', + [], + [], + 'POST' + ); + + // Add information to the renewal array + $blocks = false; + $renewal[$loanId]['success'] = true; + $renewal[$loanId]['new_date'] = $this->parseDate( + (string)$apiResult->due_date, + true + ); + //$renewal[$loanId]['new_time'] = ; + $renewal[$loanId]['item_id'] = (string)$apiResult->loan_id; + $renewal[$loanId]['sysMessage'] = 'renew_success'; + + // Add the renewal to the return array + $returnArray['details'] = $renewal; + } catch (ILSException $ilsEx) { + // Add the empty renewal array to the return array + $returnArray['details'] = $renewal; + + // Add a message that can be translated + $blocks[] = 'renew_fail'; + } + } + + $returnArray['blocks'] = $blocks; + + return $returnArray; + } + /** * Get Status * @@ -458,10 +1161,10 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface ); $status = []; $tmpl = [ - 'id' => (string) $bib->mms_id, + 'id' => (string)$bib->mms_id, 'source' => 'Solr', 'callnumber' => isset($bib->isbn) - ? (string) $bib->isbn + ? (string)$bib->isbn : '' ]; if ($record = $marc->next()) { @@ -471,7 +1174,7 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface $avail = $field->getSubfield('e')->getData(); $item = $tmpl; $item['availability'] = strtolower($avail) === 'available'; - $item['location'] = (string) $field->getSubfield('c') + $item['location'] = (string)$field->getSubfield('c') ->getData(); $status[] = $item; } @@ -540,60 +1243,106 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface } /** - * Ref: https://developers.exlibrisgroup.com/alma/apis/users - * POST /almaws/v1/users/{user_id}/requests + * Place a hold request via Alma API. This could be a title level request or + * an item level request. * * @param array $holdDetails An associative array w/ atleast patron and item_id * * @return array success: bool, sysMessage: string + * + * @link https://developers.exlibrisgroup.com/alma/apis/bibs */ public function placeHold($holdDetails) { - $client = $this->httpService->createClient( - $this->baseUrl . '/bibs/' . $holdDetails['id'] - . '/holdings/' . urlencode($holdDetails['holding_id']) - . '/items/' . urlencode($holdDetails['item_id']) - . '/requests?apiKey=' . urlencode($this->apiKey) - . '&user_id=' . urlencode($holdDetails['patron']['cat_username']) - . '&format=json' - ); + // Check for title or item level request + $level = $holdDetails['level'] ?? 'item'; + + // Get information that is valid for both, item level requests and title + // level requests. + $mmsId = $holdDetails['id']; + $holId = $holdDetails['holding_id']; + $itmId = $holdDetails['item_id']; + $patronCatUsername = $holdDetails['patron']['cat_username']; + $pickupLocation = $holdDetails['pickUpLocation'] ?? null; + $comment = $holdDetails['comment'] ?? null; + $requiredBy = (isset($holdDetails['requiredBy'])) + ? $this->dateConverter->convertFromDisplayDate( + 'Y-m-d', + $holdDetails['requiredBy'] + ) . 'Z' + : null; + + // Create body for API request + $body = []; + $body['request_type'] = 'HOLD'; + $body['pickup_location_type'] = 'LIBRARY'; + $body['pickup_location_library'] = $pickupLocation; + $body['comment'] = $comment; + $body['last_interest_date'] = $requiredBy; + + // Remove "null" values from body array + $body = array_filter($body); + + // Check if we have a title level request or an item level request + if ($level === 'title') { + // Add description if we have one for title level requests as Alma + // needs it under certain circumstances. See: https://developers. + // exlibrisgroup.com/alma/apis/xsd/rest_user_request.xsd?tags=POST + $description = isset($holdDetails['description']) ?? null; + if ($description) { + $body['description'] = $description; + } + + // Create HTTP client with Alma API URL for title level requests + $client = $this->httpService->createClient( + $this->baseUrl . '/bibs/' . urlencode($mmsId) + . '/requests?apiKey=' . urlencode($this->apiKey) + . '&user_id=' . urlencode($patronCatUsername) + . '&format=json' + ); + } else { + // Create HTTP client with Alma API URL for item level requests + $client = $this->httpService->createClient( + $this->baseUrl . '/bibs/' . urlencode($mmsId) + . '/holdings/' . urlencode($holId) + . '/items/' . urlencode($itmId) + . '/requests?apiKey=' . urlencode($this->apiKey) + . '&user_id=' . urlencode($patronCatUsername) + . '&format=json' + ); + } + + // Set headers $client->setHeaders( [ - 'Content-type: application/json', - 'Accept: application/json' + 'Content-type: application/json', + 'Accept: application/json' ] ); + + // Set HTTP method $client->setMethod(\Zend\Http\Request::METHOD_POST); - $body = ['request_type' => 'HOLD']; - if (isset($holdDetails['comment']) && !empty($holdDetails['comment'])) { - $body['comment'] = $holdDetails['comment']; - } - if (isset($holdDetails['requiredBy'])) { - $date = $this->dateConverter->convertFromDisplayDate( - 'Y-m-d', $holdDetails['requiredBy'] - ); - $body['last_interest_date'] = $date; - } - if (isset($holdDetails['pickUpLocation'])) { - $body['pickup_location_type'] = 'LIBRARY'; - $body['pickup_location_library'] = $holdDetails['pickUpLocation']; - } + + // Set body $client->setRawBody(json_encode($body)); + + // Send API call and get response $response = $client->send(); + // Check for success if ($response->isSuccess()) { - return [ - 'success' => true, - 'status' => 'hold_request_success' - ]; + return ['success' => true]; } else { // TODO: Throw an error error_log($response->getBody()); } + + // Get error message $error = json_decode($response->getBody()); if (!$error) { $error = simplexml_load_string($response->getBody()); } + return [ 'success' => false, 'sysMessage' => $error->errorList->error[0]->errorMessage @@ -607,7 +1356,7 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface * retrieval * * @param array $patron Patron information returned by the patronLogin - * method. + * method. * * @return array An array of associative arrays with locationID and * locationDisplay keys @@ -626,11 +1375,14 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface } /** + * Request from /courses. + * * @return array with key = course ID, value = course name */ - public function getCourses() { + public function getCourses() + { // https://developers.exlibrisgroup.com/alma/apis/courses - // GET /​almaws/​v1/​courses + // GET /almaws/v1/courses $xml = $this->makeRequest('/courses'); $courses = []; foreach ($xml as $course) { @@ -640,18 +1392,21 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface } /** + * Get reserves by course + * * @param string $courseID Value from getCourses - * @param string $instructorID Value from getInstructors - * @param string $departmentID Value from getDepartments + * @param string $instructorID Value from getInstructors (not used yet) + * @param string $departmentID Value from getDepartments (not used yet) * * @return array With key BIB_ID - The record ID of the current reserve item. * Not currently used: * DISPLAY_CALL_NO, AUTHOR, TITLE, PUBLISHER, PUBLISHER_DATE */ - public function findReserves($courseID, $instructorID, $departmentID) { + public function findReserves($courseID, $instructorID, $departmentID) + { // https://developers.exlibrisgroup.com/alma/apis/courses - // GET /​almaws/​v1/​courses/​{course_id}/​reading-lists - $xml = $this->makeRequest('/courses/​' . $courseID . '/​reading-lists'); + // GET /almaws/v1/courses/{course_id}/reading-lists + $xml = $this->makeRequest('/courses/' . $courseID . '/reading-lists'); $reserves = []; foreach ($xml as $list) { $listXML = $this->makeRequest( @@ -664,27 +1419,69 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface return $reserves; } - // @codingStandardsIgnoreStart - /** - * @return array with key = course ID, value = course name - * / - public function getFunds() { - // https://developers.exlibrisgroup.com/alma/apis/acq - // GET /​almaws/​v1/​acq/​funds + * Parse a date. + * + * @param string $date Date to parse + * @param boolean $withTime Add time to return if available? + * + * @return string + */ + public function parseDate($date, $withTime = false) + { + // Remove trailing Z from end of date + // e.g. from Alma we get dates like 2012-07-13Z without time, which is wrong) + if (strpos($date, 'Z', (strlen($date) - 1))) { + $date = preg_replace('/Z{1}$/', '', $date); + } + + $compactDate = "/^[0-9]{8}$/"; // e. g. 20120725 + $euroName = "/^[0-9]+\/[A-Za-z]{3}\/[0-9]{4}$/"; // e. g. 13/jan/2012 + $euro = "/^[0-9]+\/[0-9]+\/[0-9]{4}$/"; // e. g. 13/7/2012 + $euroPad = "/^[0-9]{1,2}\/[0-9]{1,2}\/[0-9]{2,4}$/"; // e. g. 13/07/2012 + $datestamp = "/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/"; // e. g. 2012-07-13 + $timestamp = "/^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}$/"; + // e. g. 2017-07-09T18:00:00 + + if ($date == null || $date == '') { + return ''; + } elseif (preg_match($compactDate, $date) === 1) { + return $this->dateConverter->convertToDisplayDate('Ynd', $date); + } elseif (preg_match($euroName, $date) === 1) { + return $this->dateConverter->convertToDisplayDate('d/M/Y', $date); + } elseif (preg_match($euro, $date) === 1) { + return $this->dateConverter->convertToDisplayDate('d/m/Y', $date); + } elseif (preg_match($euroPad, $date) === 1) { + return $this->dateConverter->convertToDisplayDate('d/m/y', $date); + } elseif (preg_match($datestamp, $date) === 1) { + return $this->dateConverter->convertToDisplayDate('Y-m-d', $date); + } elseif (preg_match($timestamp, substr($date, 0, 19)) === 1) { + if ($withTime) { + return $this->dateConverter->convertToDisplayDateAndTime( + 'Y-m-d\TH:i:s', + substr($date, 0, 19) + ); + } else { + return $this->dateConverter->convertToDisplayDate( + 'Y-m-d', + substr($date, 0, 10) + ); + } + } else { + throw new \Exception("Invalid date: $date"); + } } - */ + + // @codingStandardsIgnoreStart /** - * @param string $bibID Bibligraphic ID - * - * @return boolean + * @return array with key = course ID, value = course name * / - public function hasHoldings($bibID) { - // https://developers.exlibrisgroup.com/alma/apis/bibs - // GET /almaws/v1/bibs/{mms_id}/holdings - } - */ + * public function getFunds() { + * // https://developers.exlibrisgroup.com/alma/apis/acq + * // GET /almaws/v1/acq/funds + * } + */ /* ================= METHODS INACCESSIBLE OUTSIDE OF GET ================== */ @@ -699,10 +1496,10 @@ class Alma extends AbstractBase implements \VuFindHttp\HttpServiceAwareInterface * status – A status message from the language file (required) * sysMessage - A system supplied failure message (optional) * / - public function cancelHolds($cancelDetails) { - // https://developers.exlibrisgroup.com/alma/apis/users - // DELETE /almaws/v1/users/{user_id}/requests/{request_id} - } - */ + * public function cancelHolds($cancelDetails) { + * // https://developers.exlibrisgroup.com/alma/apis/users + * // DELETE /almaws/v1/users/{user_id}/requests/{request_id} + * } + */ // @codingStandardsIgnoreEnd } diff --git a/module/VuFind/src/VuFind/ILS/Driver/AlmaFactory.php b/module/VuFind/src/VuFind/ILS/Driver/AlmaFactory.php new file mode 100644 index 0000000000000000000000000000000000000000..fad09dc0c16a72cbc7ecb8c76b1360d2e85a7484 --- /dev/null +++ b/module/VuFind/src/VuFind/ILS/Driver/AlmaFactory.php @@ -0,0 +1,83 @@ +<?php +/** + * Factory for Alma ILS driver. + * + * PHP version 5 + * + * Copyright (C) AK Bibliothek Wien für Sozialwissenschaften 2018. + * + * 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * @category VuFind + * @package ILS_Drivers + * @author Michael Birkner <michael.birkner@akwien.at> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +namespace VuFind\ILS\Driver; + +use Interop\Container\ContainerInterface; +use Interop\Container\Exception\ContainerException; +use Zend\ServiceManager\Exception\ServiceNotCreatedException; +use Zend\ServiceManager\Exception\ServiceNotFoundException; +use Zend\ServiceManager\Factory\FactoryInterface; + +/** + * Alma ILS driver factory. + * + * @category VuFind + * @package ILS_Drivers + * @author Michael Birkner <michael.birkner@akwien.at> + * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License + * @link https://vufind.org/wiki/development Wiki + */ +class AlmaFactory implements FactoryInterface +{ + /** + * Create an object + * + * @param ContainerInterface $container Container interface + * @param string $requestedName Driver name + * @param null|array $options Options + * + * @return object Driver object + * + * @throws ServiceNotFoundException if unable to resolve the service. + * @throws ServiceNotCreatedException if an exception is raised when + * creating a service. + * @throws ContainerException if any other error occurs + */ + public function __invoke( + ContainerInterface $container, + $requestedName, + array $options = null + ) { + // Set up the driver with the date converter (and any extra parameters + // passed in as options): + $driver = new $requestedName( + $container->get('VuFind\Date\Converter'), + $container->get('VuFind\Config\PluginManager'), + ...($options ?: []) + ); + + // Populate cache storage if a setCacheStorage method is present: + if (method_exists($driver, 'setCacheStorage')) { + $driver->setCacheStorage( + $container->get('VuFind\Cache\Manager')->getCache('object') + ); + } + + return $driver; + } +} diff --git a/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php b/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php index d0f185ac4e3e7362babfe3c9d9f18b3e15040795..020df9446d6be9b4faedbb65917ee780841bed14 100644 --- a/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php +++ b/module/VuFind/src/VuFind/ILS/Driver/PluginManager.php @@ -45,6 +45,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager */ protected $aliases = [ 'aleph' => 'VuFind\ILS\Driver\Aleph', + 'alma' => 'VuFind\ILS\Driver\Alma', 'amicus' => 'VuFind\ILS\Driver\Amicus', 'daia' => 'VuFind\ILS\Driver\DAIA', 'demo' => 'VuFind\ILS\Driver\Demo', @@ -78,6 +79,7 @@ class PluginManager extends \VuFind\ServiceManager\AbstractPluginManager */ protected $factories = [ 'VuFind\ILS\Driver\Aleph' => 'VuFind\ILS\Driver\AlephFactory', + 'VuFind\ILS\Driver\Alma' => 'VuFind\ILS\Driver\AlmaFactory', 'VuFind\ILS\Driver\Amicus' => 'Zend\ServiceManager\Factory\InvokableFactory', 'VuFind\ILS\Driver\DAIA' => 'VuFind\ILS\Driver\DriverWithDateConverterFactory', diff --git a/themes/root/templates/Email/new-user-welcome.phtml b/themes/root/templates/Email/new-user-welcome.phtml new file mode 100644 index 0000000000000000000000000000000000000000..fc5ff95d7da760a2c3c4fc6df255f62047eefdc5 --- /dev/null +++ b/themes/root/templates/Email/new-user-welcome.phtml @@ -0,0 +1,10 @@ +<?=$this->translate( + 'new_user_welcome_text', + [ + '%%library%%' => $this->library, + '%%firstname%%' => $this->firstname, + '%%lastname%%' => $this->lastname, + '%%username%%' => $this->username, + '%%url%%' => $this->url + ]); +?> \ No newline at end of file