Skip to content
Snippets Groups Projects
Client.php 23.8 KiB
Newer Older
<?php
/**
 * 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.
 *
 * @author  Sebastian Kehr <kehr@ub.uni-leipzig.de>
 * @license http://opensource.org/licenses/gpl-2.0.php GNU GPLv2
 */
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 Psr\Http\Client\ClientExceptionInterface as HttpClientExceptionInterface;
use Psr\Http\Client\ClientInterface as HttpClientInterface;
use Psr\Http\Message\RequestFactoryInterface;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\StreamFactoryInterface;
use Psr\Http\Message\UriFactoryInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Serializer\SerializerInterface;
use VuFind\Cookie\CookieManager;
use Zend\Session\Container as Session;

class Client
{
    protected const ERRMSG_HTTPCLIENT
        = 'An unexpected http client error occured.';

    protected const ERRMSG_HTTPRESPONSE
        = 'An unexcected http response occured.';

    /**
     * @var string
     */

    /**
     * @var Session
     */
    protected $session;

    /**
     * @var CookieManager
     */
    protected $cookies;

    /**
     * @var SerializerInterface
     */
    protected $serializer;

    /**
     * @var HttpClientInterface
     */
    protected $httpClient;

    /**
     * @var UriFactoryInterface
     */
    protected $uriFactory;

    /**
     * @var StreamFactoryInterface
     */
    protected $streamFactory;

    /**
     * @var RequestFactoryInterface
     */
    protected $requestFactory;

    /**
     * @var string
     */
    protected $locale = 'en';

    /**
     * Client constructor.
     *
     * @param array                   $config
     * @param Session                 $session
     * @param CookieManager           $cookies
     * @param SerializerInterface     $serializer
     * @param HttpClientInterface     $httpClient
     * @param UriFactoryInterface     $uriFactory
     * @param StreamFactoryInterface  $streamFactory
     * @param RequestFactoryInterface $requestFactory
     */
    public function __construct(
        Session $session,
        CookieManager $cookies,
        SerializerInterface $serializer,
        HttpClientInterface $httpClient,
        UriFactoryInterface $uriFactory,
        StreamFactoryInterface $streamFactory,
        RequestFactoryInterface $requestFactory
    ) {
        $this->config = $config;
        $this->session = $session;
        $this->cookies = $cookies;
        $this->serializer = $serializer;
        $this->httpClient = $httpClient;
        $this->uriFactory = $uriFactory;
        $this->streamFactory = $streamFactory;
        $this->requestFactory = $requestFactory;
    }

    /**
     * @param string $locale
     */
    public function setLocale(string $locale): void
    {
        $this->locale = $locale;
    }

    public function isLoggedOn(): bool
    {
        try {
            return !!$this->restoreLogon();
        } catch (ClientException $exception) {
            return false;
        }
    }

    /**
     * @param string[] $credentials
     *
     * @return Logon
     * @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:
            list($username, $password) = $credentials;
            $body = json_encode(compact('username', 'password'));
            $request = $this->buildRequest('post', 'logons', $body);
            $response = $this->sendRequest($request);

            if ($response->getStatusCode() !== 201) {
                $this->throwException($response);
            }
            $logon = $this->parseLogon((string)$response->getBody());
            return $this->storeLogon($logon);
        default:
            throw new InvalidArgumentException();
    /**
     * @param string $username
     * @param string $password
     *
     * @return bool
     */
    public function checkCredentials(string $username, string $password): bool
    {
        $body = json_encode(compact('username', 'password'));
        $request = $this->buildRequest('post', 'logons', $body);
        $response = $this->sendRequest($request);

        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();
        $request = $this->buildRequest('delete', "logons/$username");
        $this->cookies->set('finc_fid_logon', null);
        $this->session->exchangeArray([]);

        $response = $this->sendRequest($request);
        switch ($response->getStatusCode()) {
        case 404:
        case 204:
            break;
        default:
            $this->throwException($response);
    /**
     * @param Order $order
     *
     * @return Order
     * @throws ClientException
     */
    public function requestOrderCreation(Order $order): Order
    {
        $body = $this->serializer->serialize(
            $order, 'json', [
            'groups' => ['order:creation:request'],

        $request = $this->buildRequest('post', 'orders', $body);
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 201) {
            $this->throwException($response);
        }
        /**
         * @var Order $result
         */
        $result = $this->serializer->deserialize(
            (string)$response->getBody(),
            Order::class, 'json', ['groups' => ['order:creation:response']]
        );
        /* refresh user details */
        $this->requestUserDetails();
    public function requestRecord(string $recordid) : Record
    {
        $request = $this->buildRequest('get', "records/$recordid");
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 200) {
            $this->throwException($response);
        }
        /**
         * @var Record $record
         */
        $record = $this->serializer->deserialize(
            (string)$response->getBody(),
            Record::class, 'json'
        );
    /**
     * @return array|Order[]
     * @throws ClientException
     */
    public function requestOrderList(): array
    {
        $request = $this->buildRequest('get', 'orders');
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 200) {
            $this->throwException($response);
        }
        /**
         * @var Order[] $list
         */
        $list = $this->serializer->deserialize(
            (string)$response->getBody(),
            Order::class . '[]', 'json'
        );
        $keys = array_map(
            function (Order $order) {
                return $order->getId();
            }, $list
        );
    /**
     * @return Order|null
     * @throws ClientException
     * @throws UserNotAuthorizedException
     */
    public function requestOrder($orderId = null): ?Order
    {
        $request = $this->buildRequest('get', "orders/$orderId");
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 200) {
            $this->throwException($response);
        }

        /**
         * @var Order $order
         */
        $order = $this->serializer->deserialize(
            (string)$response->getBody(),
            Order::class, 'json'
        );
     * @param  Order $user
     * @return bool
     * @throws ClientException
     */
    public function requestOrderUpdate(Order $order) : bool
    {
        $body = $this->serializer->serialize($order, 'json', ['groups' => ['order:update:request']]);
        $request = $this->buildRequest('put', "orders/{$order->getId()}", $body);
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 200) {
            $this->throwException($response);
            return false;
        }

        return true;
    }

    /**
     * @param string $baseUrl
     * @param string $username
     * @param string $firstname
     * @param string $lastname
     *
     * @throws ClientException
     */
    public function requestRegistrationLink(
        string $baseUrl,
        string $username,
        string $firstname,
        string $lastname
    ): void {
        $body = json_encode(
            compact(
                'baseUrl', 'username', 'firstname',
                'lastname'
            )
        );
        $request = $this->buildRequest('post', 'mail/registration', $body);
        $response = $this->sendRequest($request);

        if ($response->getStatusCode() !== 204) {
            $this->throwException($response);
        }
    }

    /**
     * @param string $baseUrl
     * @param string $username
     *
     * @return void
     * @throws ClientException
     */
    public function requestPasswordLink(string $baseUrl, string $username): void
    {
        $body = json_encode(compact('baseUrl', 'username'));
        $request = $this->buildRequest('post', 'mail/password', $body);
        $response = $this->sendRequest($request);

        if ($response->getStatusCode() !== 204) {
            $this->throwException($response);
        }
    }

    /**
     * @param string $baseUrl
     * @param string $username
     *
     * @throws ClientException
     */
    public function requestUsernameLink(string $baseUrl, string $username): void
    {
        $body = json_encode(compact('baseUrl', 'username'));
        $request = $this->buildRequest('post', 'mail/username', $body);
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 204) {
            $this->throwException($response);
        }
    }
    /**
     * @return User|null
     * @throws ClientException
     * @throws UserNotAuthorizedException
    public function requestUserDetails($userId = null, $forceRelaod = false): ?User
    {
        $logon = $this->restoreLogon();

        /**
         * @var User $user
         */
        $user = $this->session['user'] ?? null;
        $ownId = $logon->getOwnerId();

        if (null === $userId || $userId === $ownId) {
            // user asks for own profile data
            if (!empty($user) && !$forceRelaod) {
                return $user;
            }
            $userId = $ownId;
        } else {
            // user asks for another user's profile
            // this shall only be possible for authorized admins
            $this->authorize('edit_user');
        }
        $request = $this->buildRequest('get', "users/$userId");
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 200) {
            $this->throwException($response);
        }

        /**
         * @var User $user
         */
        $user = $this->serializer->deserialize(
            (string)$response->getBody(),
            User::class, 'json', ['groups' => ['user:details:response']]
        );
        if ($ownId === $userId) {
            $this->session['user'] = $user;
        }
        return $user;
    }

    /**
     * @param User $user
     *
     * @return User
     * @throws ClientException
     */
    public function requestUserCreation(User $user): User
    {
        $body = $this->serializer->serialize(
            $user, 'json', [
            'groups' => ['user:creation:request']

        $request = $this->buildRequest('post', 'users', $body);
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 201) {
            $this->throwException($response);
        }
        /**
         * @var User $result
         */
        $result = $this->serializer->deserialize(
            (string)$response->getBody(),
            User::class, 'json', ['groups' => ['user:creation:response']]
        );

        return $result;
    }

    /**
     * @param User $user
     *
     * @return User
     * @throws ClientException
     */
    public function requestUserUpdate(User $user): User
    {
        return $this->doRequestUserUpdate($user, ['user:update:request']);
    }
    /**
     * @param User $user
     *
     * @return User
     * @throws ClientException
     */
    public function requestUserPasswordUpdate(User $user): User
    {
        return $this->doRequestUserUpdate(
            $user,
            ['user:update-password:request']
        );
    /**
     * @param User $user
     *
     * @return User
     * @throws ClientException
     */
    public function requestUserUsernameUpdate(User $user): User
    {
        return $this->doRequestUserUpdate(
            $user, [
            'user:update-username:request'
    /**
     * @return User[]
     * @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;
        }

        $request = $this->buildRequest('get', 'users');
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 200) {
            $this->throwException($response);
        }
        /**
         * @var Library[] $list
         */
        $list = $this->serializer->deserialize(
            (string)$response->getBody(), User::class . '[]', 'json'
        );
        $keys = array_map(
            function (User $libary) {
                return $libary->getId();
            }, $list
        );

        return $this->session['users'] = array_combine($keys, $list);
    }

        unset($this->session['users']);
    }

    /**
     * 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 object or null if we want to validate the currently logged in user
     * @throws ClientException
     * @throws UserNotLoggedinException
     * @throws UserNotAuthorizedException
     */
Sebastian Kehr's avatar
Sebastian Kehr committed
    protected function authorize(String $permission, User $user = null)
    {
        try {
            $logon = $this->restoreLogon();
        } catch (ClientException $exception) {
            throw new UserNotLoggedinException();
        }
Sebastian Kehr's avatar
Sebastian Kehr committed
        if (!$logon || !$logon->getOwnerId()) {
            throw new UserNotLoggedinException();
        }
        $user = $this->requestUserDetails();
        if (!$user->hasPermission($permission)) {
            throw new UserNotAuthorizedException();
        }
    }

    /**
     * @param String $permission
     *
     * @return bool
     * @throws ClientException
     */
    public function isAuthorized(String $permission)
    {
        try {
            $this->authorize($permission);
        } catch (UserAuthorizationException $exception) {
    /**
     * @return Library[]
     * @throws ClientException
     */
    public function requestLibraryList(): array
    {
        if ($list = $this->session->libraries[$this->locale] ?? null) {
            return $list;
        }

        $request = $this->buildRequest('get', 'libraries');
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 200) {
            $this->throwException($response);
        }
        /**
         * @var Library[] $list
         */
        $list = $this->serializer->deserialize(
            (string)$response->getBody(), Library::class . '[]', 'json'
        );
        $keys = array_map(
            function (Library $libary) {
                return $libary->getId();
            }, $list
        );
        if (empty($this->session->libraries)) {
            $this->session->libraries = [];
        }

        return $this->session->libraries[$this->locale] = array_combine(
            $keys,
            $list
        );
     * @param  $id
     * @return Library
     * @throws ClientException
     */
    public function requestLibraryById($id): Library
    {
        if ($library = $this->session->homeLibrary[$this->locale] ?? null) {
            return $library;
        }

        $request = $this->buildRequest('get', "libraries/$id");
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 200) {
            $this->throwException($response);
        }

        /**
         * @var Library $list
         */
        $library = $this->serializer->deserialize(
            (string)$response->getBody(), Library::class, 'json'
        );
        $phpTypeCasting = array_map(
            function (Library $library) {
                return $library->getId();
            }, [$library]
        );

        if (empty($this->session->homeLibrary)) {
            $this->session->homeLibrary = [];
        }

        return $this->session->homeLibrary[$this->locale] = array_combine($phpTypeCasting, [$library])[$id];
    }

    /**
     * @param User  $user
     * @param array $groups
     *
     * @return User
     * @throws ClientException
     */
    protected function doRequestUserUpdate(User $user, array $groups): User
    {
        $body = $this->serializer->serialize($user, 'json', compact('groups'));
        $request = $this->buildRequest('put', "users/{$user->getId()}", $body);
        $response = $this->sendAuthenticatedRequest($request);

        if ($response->getStatusCode() !== 200) {
            $this->throwException($response);
        }
        /**
         * @var User $result
         */
        $result = $this->serializer->deserialize(
            (string)$response->getBody(),
            User::class, 'json', ['groups' => ['user:update:response']]
        );
        $logon = $this->restoreLogon();
        if ($logon->getOwnerId() === $user->getId()) {
            // refresh user data
            $this->session['user'] = $result;
        }
        return $result;
    /**
     * @return 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'));
        $request = $this->buildRequest('post', 'logons', $body);
        $response = $this->sendAuthenticatedRequest($request, false);

        if ($response->getStatusCode() !== 201) {
            $this->throwException($response);
        }

        $logon = $this->parseLogon((string)$response->getBody());
        return $this->storeLogon($logon);
    }

    /**
     * @param RequestInterface $request
     * @param bool             $retryOn401
     *
     * @return ResponseInterface
     * @throws ClientException
     */
    protected function sendAuthenticatedRequest(
        RequestInterface $request,
        bool $retryOn401 = true
    ): ResponseInterface {
        $token = ($logon = $this->restoreLogon())->getToken();
        $request = $request->withHeader('Authorization', "Bearer $token");
        $response = $this->sendRequest($request);

        if ($response->getStatusCode() === 401 && $retryOn401) {
            return $this->sendAuthenticatedRequest($request, false);
        }

        return $response;
    }

    /**
     * @param RequestInterface $request
     *
     * @return ResponseInterface
     * @throws ClientException
     */
    protected function sendRequest(RequestInterface $request): ResponseInterface
    {
        try {
            return $this->httpClient->sendRequest($request);
        } catch (HttpClientExceptionInterface $exception) {
            throw new ClientException(self::ERRMSG_HTTPCLIENT, 0, $exception);
        }
    }

    protected function buildRequest(
        string $verb,
        string $path,
        string $body = '',
        array $query = []
    ): RequestInterface {
        $baseUrl = $this->config['baseUrl'];
        $uri = $this->uriFactory->createUri("$baseUrl/$path")
            ->withQuery(http_build_query($query));

        $request = $this->requestFactory->createRequest($verb, $uri)
            ->withBody($this->streamFactory->createStream($body))
            ->withHeader('Content-type', 'application/json')
            ->withHeader('Accept', 'application/json')
            ->withHeader('Accept-language', $this->locale);

        if (APPLICATION_ENV === 'production') {
            return $request;
        }

        Request::setTrustedProxies($this->config['xdebug_trusted_proxies'] ?? []);
        $xdebugSession = $this->config['xdebug_session'] ?? 'fidis';
        $xdebugRemoteAddr = $this->config['xdebug_remote_addr']
            ?? Request::createFromGlobals()->getClientIp();

        return APPLICATION_ENV === 'production' ? $request
            : $request->withHeader('Cookie', "XDEBUG_SESSION=$xdebugSession")
            ->withHeader('X-xdebug-remote-addr', $xdebugRemoteAddr);
    }

    /**
     * @param ResponseInterface $response
     *
     * @throws ClientException
     */
    protected function throwException(ResponseInterface $response)
    {
        $errorCode = $response->getStatusCode();
        $error = json_decode((string)$response->getBody(), true);
        throw new ClientException($error['detail'], $errorCode);
    }

    /**
     * @return Logon
     * @throws ClientException
     */
    protected function restoreLogon(): Logon
    {
        /**
         * @var Logon $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);
    }

    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;
    }

    protected function parseLogon(string $logon): Logon
    {
        $logon = $this->serializer->deserialize($logon, Logon::class, 'json');
        /**
         * @var Logon $logon
         */