Newer
Older
* Copyright (C) 2019 Leipzig University Library
*
* 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 Service
* @author Sebastian Kehr <kehr@ub.uni-leipzig.de>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development Wiki
*/
namespace fid\Service;
use fid\Service\DataTransferObject\Library;
use fid\Service\DataTransferObject\Logon;
use fid\Service\DataTransferObject\Order;
use fid\Service\DataTransferObject\Record;
use fid\Service\DataTransferObject\User;
use InvalidArgumentException;
use Laminas\Http\Response;
use Laminas\Session\Container as Session;
use Symfony\Component\Serializer\SerializerInterface;
use VuFind\Cookie\CookieManager;
/**
* FID client class
*
* @category VuFind
* @package Service
* @author Sebastian Kehr <kehr@ub.uni-leipzig.de>
* @author Robert Lange <lange@ub.uni-leipzig.de>
* @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License
* @link https://vufind.org/wiki/development Wiki
*/
class Client
{
protected const ERRMSG_HTTPCLIENT
= 'An unexpected http client error occured.';
protected const ERRMSG_HTTPRESPONSE
= 'An unexcected http response occured.';
protected $serializer;
/* @var HttpService */
protected $httpService;
protected $locale = 'en';
/**
* Client constructor.
*
* @param array $config config
* @param Session $session session
* @param CookieManager $cookies cookies
* @param SerializerInterface $serializer serializer
* @param HttpService $httpService httpService
*/
public function __construct(
Session $session,
CookieManager $cookies,
SerializerInterface $serializer,
$this->config = $config;
$this->session = $session;
$this->cookies = $cookies;
$this->serializer = $serializer;
* Set local for translations
*
* @param string $locale locale
*
* @return void
*/
public function setLocale(string $locale): void
{
$this->locale = $locale;
}
/**
* Get if user is logged in
*
* @return bool
*/
public function isLoggedOn(): bool
{
try {
return !!$this->restoreLogon();
} catch (ClientException $exception) {
return false;
}
}
/**
* Get user logon
*
* @param string ...$credentials credentials
* @throws ClientException
*/
public function logon(string ...$credentials): Logon
{
$this->logoff();
switch (count($credentials)) {
case 1:
$credentials = base64_decode(urldecode($credentials[0]));
$logon = $this->storeLogon($this->parseLogon($credentials));
return $this->refreshLogon($logon);
case 2:

Alexander Purr
committed
[$username, $password] = $credentials;
$body = json_encode(compact('username', 'password'));
$response = $this->sendRequest(false, 'post', 'logons', $body);
if ($response->getStatusCode() !== 201) {
$this->throwException($response);
}
$logon = $this->parseLogon((string)$response->getBody());
return $this->storeLogon($logon);
default:
throw new InvalidArgumentException();
* Check credentials
*
* @param string $username username
* @param string $password password
*
* @return bool
*/
public function checkCredentials(string $username, string $password): bool
{
$body = json_encode(compact('username', 'password'));
$response = $this->sendRequest(false, 'post', 'logons', $body);
if ($response->getStatusCode() !== 201) {
return false;
}
$logon = $this->parseLogon((string)$response->getBody());
$this->storeLogon($logon);
return true;
}
* @throws ClientException
*/
public function logoff(): void
{
try {
$logon = $this->restoreLogon();
} catch (ClientException $exception) {
return;
}
$username = $logon->getUsername();
$response = $this->sendRequest(false, 'delete', "logons/$username");
$this->cookies->set('finc_fid_logon', null);
$this->session->exchangeArray([]);
switch ($response->getStatusCode()) {
case 404:
case 204:
break;
default:
$this->throwException($response);
* Request creation of an order
*
* @param Order $order order
*
* @return Order
* @throws ClientException
*/
public function requestOrderCreation(Order $order): Order
{

Alexander Purr
committed
$order,
'json',
[
'groups' => ['order:creation:request'],
$response = $this->sendRequest(true, 'post', 'orders', $body);
if ($response->getStatusCode() !== 201) {
$this->throwException($response);
}
$result = $this->serializer->deserialize(
(string)$response->getBody(),

Alexander Purr
committed
Order::class,
'json',
['groups' => ['order:creation:response']]
/* refresh user details */
$this->requestUserDetails();

Alexander Purr
committed
return $result;
}
/**
* Get record by id
*
* @param string $recordid record_id
*
* @return Record
*
* @throws ClientException
*/
public function requestRecord(string $recordid) : Record
{
$response = $this->sendRequest(true, 'get', "records/$recordid");
if ($response->getStatusCode() !== 200) {
$this->throwException($response);
}
$record = $this->serializer->deserialize(
(string)$response->getBody(),

Alexander Purr
committed
Record::class,
'json'
* @return array|Order[]
* @throws ClientException
*/
public function requestOrderList(): array
{
$response = $this->sendRequest(true, 'get', 'orders');
if ($response->getStatusCode() !== 200) {
$this->throwException($response);
}
$list = $this->serializer->deserialize(
(string)$response->getBody(),

Alexander Purr
committed
Order::class . '[]',
'json'
$keys = array_map(
function (Order $order) {
return $order->getId();

Alexander Purr
committed
},
$list
return array_combine($keys, $list);
/**
* Get order by id
* TODO: Why can id be null?
*
* @param null $orderId order identifier
*
* @return Order|null
* @throws ClientException
* @throws UserNotAuthorizedException
*/
public function requestOrder($orderId = null): ?Order
{
$response = $this->sendRequest(true, 'get', "orders/$orderId");
if ($response->getStatusCode() !== 200) {
$this->throwException($response);
}
$order = $this->serializer->deserialize(
(string)$response->getBody(),

Alexander Purr
committed
Order::class,
'json'
return $order;
}
/**
* Delete order
*
* @param int $orderId order id
* @return bool
*
* @throws ClientException
*/
public function requestOrderDelete(int $orderId) : bool
{
$response = $this->sendRequest(true, 'delete', "orders/{$orderId}");
if ($response->getStatusCode() !== 204) {
$this->throwException($response);
}
return true;
}
/**
* Update order
*
* @param Order $order order
*
* @return bool
* @throws ClientException
*/
public function requestOrderUpdate(Order $order) : bool
{
$body = $this->serializer->serialize(
$order,
'json',
['groups' => ['order:update:request']]
);
$response = $this->sendRequest(
true,
'put',
"orders/{$order->getId()}",
$body
);
if ($response->getStatusCode() !== 200) {
$this->throwException($response);
return false;
}
return true;
}
* Request registration link
*
* @param string $baseUrl baseUrl
* @param string $username username
* @param string $firstname firstname
* @param string $lastname lastname
*
* @return void
*
* @throws ClientException
*/
public function requestRegistrationLink(
string $baseUrl,
string $username,
string $firstname,
string $lastname
): void {

Alexander Purr
committed
'baseUrl',
'username',
'firstname',
$response = $this->sendRequest(false, 'post', 'mail/registration', $body);
if ($response->getStatusCode() !== 204) {
$this->throwException($response);
}
}
* Request (new) password link by mail
*
* @param string $baseUrl base url
* @param string $username username
* @throws ClientException
*/
public function requestPasswordLink(string $baseUrl, string $username): void
{
$body = json_encode(compact('baseUrl', 'username'));
$response = $this->sendRequest(false, 'post', 'mail/password', $body);
if ($response->getStatusCode() !== 204) {
$this->throwException($response);
}
}
* Request (new) username link by mail
*
* @param string $baseUrl base url
* @param string $username username
*
* @return void
*
* @throws ClientException
*/
public function requestUsernameLink(string $baseUrl, string $username): void
{
$body = json_encode(compact('baseUrl', 'username'));
$response = $this->sendRequest(true, 'post', 'mail/username', $body);
if ($response->getStatusCode() !== 204) {
$this->throwException($response);
}
}
* Get user details
*
* @param null $userId user identifier
* @param bool $forceReload force reload
*
* @throws ClientException
public function requestUserDetails($userId = null, $forceReload = false): ?User
{
$logon = $this->restoreLogon();
$user = $this->session['user'] ?? null;
$ownId = $logon->getOwnerId();
if (null === $userId || $userId === $ownId) {
return $user;
}
$userId = $ownId;
} else {
// user asks for another user's profile
// this shall only be possible for authorized admins
$this->authorize('edit_user');
$response = $this->sendRequest(true, 'get', "users/$userId");
if ($response->getStatusCode() === 401) {
throw new UserNotAuthorizedException();
}
if ($response->getStatusCode() !== 200) {
$this->throwException($response);
}
$user = $this->serializer->deserialize(
(string)$response->getBody(),

Alexander Purr
committed
User::class,
'json',
['groups' => ['user:details:response']]
if ($ownId === $userId) {
$this->session['user'] = $user;
}
return $user;
* Create new user
*
* @param User $user user data
* @throws ClientException
*/
public function requestUserCreation(User $user): User
{

Alexander Purr
committed
$user,
'json',
[
'groups' => ['user:creation:request']
$response = $this->sendRequest(true, 'post', 'users', $body);
if ($response->getStatusCode() !== 201) {
$this->throwException($response);
}
$result = $this->serializer->deserialize(
(string)$response->getBody(),

Alexander Purr
committed
User::class,
'json',
['groups' => ['user:creation:response']]
return $result;
}
/**
* Update user
*
* @param User $user user
* @throws ClientException
*/
public function requestUserUpdate(User $user): User
{
return $this->doRequestUserUpdate($user, ['user:update:request']);
}
* Update password
*
* @param User $user user
* @throws ClientException
*/
public function requestUserPasswordUpdate(User $user): User
{
return $this->doRequestUserUpdate(
$user,
['user:update-password:request']
);
* Update username
*
* @param User $user user
* @throws ClientException
*/
public function requestUserUsernameUpdate(User $user): User
{

Alexander Purr
committed
$user,
[
'user:update-username:request'
* @throws ClientException
* @throws UserNotAuthorizedException
*/
public function requestUserList(): array
{
// user asks for another users' profiles
// this shall only be possible for authorized admins
$this->authorize('read_user_list');
if ($list = $this->session['users'] ?? null) {
return $list;
}
$response = $this->sendRequest(true, 'get', 'users');
if ($response->getStatusCode() !== 200) {
$this->throwException($response);
}
$list = $this->serializer->deserialize(

Alexander Purr
committed
(string)$response->getBody(),
User::class . '[]',
'json'
$keys = array_map(
function (User $libary) {
return $libary->getId();

Alexander Purr
committed
},
$list
return $this->session['users'] = array_combine($keys, $list);
}
/**
* Empty user list from session
*
* @return void
*/
public function flushUserList()
{
unset($this->session['users']);
}
/**
* Authorize user
*
* Throws an Exception in case the user does not have the requested permission
* or the permission cannot be verified
* @param String $permission Name of the permission
* @param User|null $user user or null for the currently logged in user
*
* @return void
*
* @throws UserNotLoggedinException
* @throws UserNotAuthorizedException
*/
protected function authorize(String $permission, User $user = null)
{
try {
$logon = $this->restoreLogon();
} catch (ClientException $exception) {
throw new UserNotLoggedinException();
}
throw new UserNotLoggedinException();
}
$user = $this->requestUserDetails();
if (!$user->hasPermission($permission)) {
throw new UserNotAuthorizedException();
}
}
/**
* Check if user is authorized
*
* @param String $permission name of permission
*
* @return bool
* @throws ClientException
*/
public function isAuthorized(String $permission)
{
try {
$this->authorize($permission);
} catch (UserAuthorizationException $exception) {
return false;
return true;
* @throws ClientException
*/
public function requestLibraryList(): array
{
if ($list = $this->session->libraries[$this->locale] ?? null) {
return $list;
}
$response = $this->sendRequest(true, 'get', 'libraries');
if ($response->getStatusCode() !== 200) {
$this->throwException($response);
}
$list = $this->serializer->deserialize(

Alexander Purr
committed
(string)$response->getBody(),
Library::class . '[]',
'json'
$keys = array_map(
function (Library $libary) {
return $libary->getId();

Alexander Purr
committed
},
$list
if (empty($this->session->libraries)) {
$this->session->libraries = [];
}
return $this->session->libraries[$this->locale] = array_combine(
$keys,
$list
);
* Get library by id
*
* @param $id identifier
*
* @return Library
* @throws ClientException
*/
public function requestLibraryById($id): Library
{
if ($library = $this->session->homeLibrary[$this->locale] ?? null) {
return $library;
}
$response = $this->sendRequest(true, 'get', "libraries/$id");
if ($response->getStatusCode() !== 200) {
$this->throwException($response);
}
$library = $this->serializer->deserialize(

Alexander Purr
committed
(string)$response->getBody(),
Library::class,
'json'
$phpTypeCasting = array_map(
function (Library $library) {
return $library->getId();

Alexander Purr
committed
},
[$library]
if (empty($this->session->homeLibrary)) {
$this->session->homeLibrary = [];
}
return $this->session->homeLibrary[$this->locale]
= array_combine($phpTypeCasting, [$library])[$id];
* Do update user
*
* @param User $user user
* @param array $groups groups
* @throws ClientException
*/
protected function doRequestUserUpdate(User $user, array $groups): User
{
$body = $this->serializer->serialize($user, 'json', compact('groups'));
$response = $this->sendRequest(true, 'put', "users/{$user->getId()}", $body);
if ($response->getStatusCode() !== 200) {
$this->throwException($response);
}
$result = $this->serializer->deserialize(
(string)$response->getBody(),

Alexander Purr
committed
User::class,
'json',
['groups' => ['user:update:response']]
$logon = $this->restoreLogon();
if ($logon->getOwnerId() === $user->getId()) {
// refresh user data
$this->session['user'] = $result;
}
return $result;
* Refresh logon
*
* @param Logon $logon logon
*
* @throws ClientException
*/
protected function refreshLogon(Logon $logon): Logon
{
if (time() < $logon->getStalesAt()) {
return $logon;
}
$username = $logon->getUsername();
$password = $logon->getPassword();
$body = json_encode(compact('username', 'password'));
$response = $this->sendRequest(true, 'post', 'logons', $body, [], false);
if ($response->getStatusCode() === 401) {
throw new UserNotAuthorizedException();
}
if ($response->getStatusCode() !== 201) {
$this->throwException($response);
}
$logon = $this->parseLogon((string)$response->getBody());
return $this->storeLogon($logon);
}
/**
* Send request - retry after 401 by recursive call
* @param string $verb verb
* @param string $path path
* @param string $body body
* @param array $query query
* @param bool $retry retry if http status is Unauthorized
protected function sendRequest(
bool $auth,
string $verb,
string $path,
string $body = '',
array $query = [],
bool $retry = true
): Response {
$baseUrl = $this->config['baseUrl'];
$client = $this->httpService->createClient(
"$baseUrl/$path?" . http_build_query($query),
$verb
);
$client->getAdapter()->setOptions(
[
'sslverifypeer' => false,
'sslverifypeername' => false,
'sslallowselfsigned' => true
]
);
$headers["Content-Type"] = "application/json";
$headers["Accept"] = "application/json";
$headers["Accept-language"] = $this->locale;
if ($auth && $token = ($logon = $this->restoreLogon())->getToken()) {
$headers["Authorization"] = "Bearer $token";
}
if (APPLICATION_ENV !== 'production') {
$xdebugSession = $this->config['xdebug_session'] ?? 'fidis';
$xdebugRemoteAddr = $this->config['xdebug_remote_addr']
?? $_SERVER['REMOTE_ADDR'];
$headers["Cookie"] = "XDEBUG_SESSION=$xdebugSession";
$headers["X-xdebug-remote-addr"] = "$xdebugRemoteAddr";
$client->setRawBody($body);
$client->setHeaders($headers);
try {
$response = $client->send();
if ($response->getStatusCode() === 401 && $retry) {
if ($logon) {
$this->refreshLogon($logon);
}
return $this->sendRequest(
$auth,
$verb,
$path,
$body,
$query,
false
);
}
return $response;
} catch (\Exception $exception) {
throw new ClientException(self::ERRMSG_HTTPCLIENT, 0, $exception);
}
*
* @throws ClientException
*/
protected function throwException(Response $response)
{
$errorCode = $response->getStatusCode();
$error = json_decode((string)$response->getBody(), true);
throw new ClientException($error['detail'], $errorCode);
* @throws ClientException
*/
protected function restoreLogon(): Logon
{
if ($logon = $this->session['logon'] ?? null) {
$logon->setToken($this->cookies->get('finc_fid_logon'));
if (time() < $logon->getExpiresAt()) {
return $logon;
}
}
$this->session->exchangeArray([]);
$this->cookies->set('finc_fid_logon', null);
throw new ClientException('Missing or expired logon.', 401);
}
/**
* Store logon
*
* @param Logon $logon logon
*
* @return Logon
*/
protected function storeLogon(Logon $logon): Logon
{
$this->cookies->set('finc_fid_logon', $token = $logon->getToken());
$logon->setToken(null);
$this->session->exchangeArray(compact('logon'));
$logon->setToken($token);
return $logon;
}
/**
* Parse logon
*
* @param string $logon logon
*
* @return Logon
*/
protected function parseLogon(string $logon): Logon
{
$logon = $this->serializer->deserialize($logon, Logon::class, 'json');
return $logon;
}
}