diff --git a/src/Authorization/AuthorizationService.php b/src/Authorization/AuthorizationService.php deleted file mode 100644 index 5486600877c8456811197da5b46c0421185a7a45..0000000000000000000000000000000000000000 --- a/src/Authorization/AuthorizationService.php +++ /dev/null @@ -1,12 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Dbp\Relay\CoreBundle\Authorization; - -/** - * @deprecated use AbstractAuthorizationService instead - */ -abstract class AuthorizationService extends AbstractAuthorizationService -{ -} diff --git a/src/Http/ApiConnection.php b/src/Http/ApiConnection.php index 1b22c4f4f08c7262d3ebe7d8fdb05c9c55f38d5b..2bf5fbd1a28aa81733daa124406fb33d235aa7ae 100644 --- a/src/Http/ApiConnection.php +++ b/src/Http/ApiConnection.php @@ -80,7 +80,7 @@ class ApiConnection /** * @throws ConnectionException */ - public function get(string $uri, array $options): ResponseInterface + public function get(string $uri, array $query): ResponseInterface { $requestOptions = [ Connection::REQUEST_OPTION_HEADERS => [ @@ -88,7 +88,21 @@ class ApiConnection ], ]; - return $this->getApiConnection()->get($uri, $options, $requestOptions); + return $this->getConnection()->get($uri, $query, $requestOptions); + } + + /** + * @throws ConnectionException + */ + public function postJSON(string $uri, array $parameters): ResponseInterface + { + $requestOptions = [ + Connection::REQUEST_OPTION_HEADERS => [ + 'Authorization' => 'Bearer '.$this->getAccessToken(), + ], + ]; + + return $this->getConnection()->postJSON($uri, $parameters, $requestOptions); } /** @@ -123,7 +137,7 @@ class ApiConnection return $this->accessToken; } - private function getApiConnection(): Connection + private function getConnection(): Connection { if ($this->connection === null) { $connection = new Connection($this->config[self::API_URL_CONFIG_PARAMETER]); diff --git a/src/Http/ConnectionException.php b/src/Http/ConnectionException.php index 3c3ee6fc1a115cb47090d54eca3ce43ee0452f4a..5ba57a6f1de2b44c49620605cfac706c5a5dcb18 100644 --- a/src/Http/ConnectionException.php +++ b/src/Http/ConnectionException.php @@ -11,6 +11,7 @@ class ConnectionException extends \RuntimeException { public const REQUEST_EXCEPTION = 1; public const JSON_EXCEPTION = 2; + public const INVALID_DATA_EXCEPTION = 3; /** @var RequestInterface|null */ private $request; diff --git a/src/ProxyApi/AbstractProxyDataEventSubscriber.php b/src/ProxyApi/AbstractProxyDataEventSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..02128fd99b49f9d6354267b06ad3fe77a1e952ca --- /dev/null +++ b/src/ProxyApi/AbstractProxyDataEventSubscriber.php @@ -0,0 +1,53 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\ProxyApi; + +use Exception; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Exception\BadRequestException; + +abstract class AbstractProxyDataEventSubscriber implements EventSubscriberInterface +{ + protected const NAMESPACE = ''; + + public static function getSubscribedEvents(): array + { + return [ + ProxyApi::PROXY_DATA_EVENT_NAME.static::NAMESPACE => 'onProxyDataEvent', + ]; + } + + /** + * @throws BadRequestException + */ + public function onProxyDataEvent(ProxyDataEvent $event): void + { + $event->setHandled(); + $proxyData = $event->getProxyData(); + $functionName = $proxyData->getFunctionName(); + $arguments = $proxyData->getArguments(); + $returnValue = null; + + if ($this->isFunctionDefined($functionName) === false) { + throw new BadRequestException(sprintf('unknown function "%s" under namespace "%s"', $functionName, static::NAMESPACE)); + } elseif ($this->areAllRequiredArgumentsDefined($functionName, $arguments) === false) { + throw new BadRequestException(sprintf('incomplete argument list for function "%s" under namespace "%s"', $functionName, static::NAMESPACE)); + } + + try { + $returnValue = $this->callFunction($functionName, $arguments); + } catch (Exception $exception) { + $proxyData->setErrorsFromException($exception); + } + + $proxyData->setData($returnValue); + } + + abstract protected function isFunctionDefined(string $functionName): bool; + + abstract protected function areAllRequiredArgumentsDefined(string $functionName, array $arguments): bool; + + abstract protected function callFunction(string $functionName, array $arguments); +} diff --git a/src/ProxyApi/ApiConnection.php b/src/ProxyApi/ApiConnection.php new file mode 100644 index 0000000000000000000000000000000000000000..9b0e137fe363676259c839f53c96cc1636cd7d22 --- /dev/null +++ b/src/ProxyApi/ApiConnection.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\ProxyApi; + +use Dbp\Relay\CoreBundle\Helpers\Tools; +use Dbp\Relay\CoreBundle\Http\ApiConnection as BaseApiConnection; +use Dbp\Relay\CoreBundle\Http\ConnectionException as BaseConnectionException; + +class ApiConnection extends BaseApiConnection +{ + private const NAMESPACE_PARAMETER_NAME = 'namespace'; + private const FUNCTION_NAME_PARAMETER_NAME = 'functionName'; + private const ARGUMENTS_PARAMETER_NAME = 'arguments'; + + private const PROXY_DATA_URI = 'proxy/proxydata'; + + /** + * @throws ConnectionException + */ + public function callFunction(string $namespace, string $functionName, array $arguments = []) + { + $parameters = [ + self::NAMESPACE_PARAMETER_NAME => $namespace, + self::FUNCTION_NAME_PARAMETER_NAME => $functionName, + self::ARGUMENTS_PARAMETER_NAME => $arguments, + ]; + + $responseBody = (string) $this->postJSON(self::PROXY_DATA_URI, $parameters)->getBody(); + + try { + $proxyData = Tools::decodeJSON($responseBody, true); + } catch (\JsonException $exception) { + throw new ConnectionException('failed to JSON decode API response: '.$exception->getMessage(), BaseConnectionException::JSON_EXCEPTION); + } + + try { + $errors = $proxyData[ProxyApi::PROXY_DATA_ERRORS_KEY]; + $returnValue = $proxyData[ProxyApi::PROXY_DATA_RETURN_VALUE_KEY]; + } catch (\Exception $exception) { + throw new ConnectionException('API returned invalid ProxyData object', BaseConnectionException::INVALID_DATA_EXCEPTION); + } + + if (!empty($errors)) { + $topLevelError = $errors[0]; + throw new ConnectionException(sprintf('call to API function "%s" under namespace "%s" resulted in an error: %s (code: %s)', $functionName, $namespace, $topLevelError['message'] ?? 'message not available', $topLevelError['code'] ?? 'code not available'), ConnectionException::API_ERROR); + } + + return $returnValue; + } +} diff --git a/src/ProxyApi/ConnectionException.php b/src/ProxyApi/ConnectionException.php new file mode 100644 index 0000000000000000000000000000000000000000..f552ff94d9b50a939a2e31f8c2724c0106769a8a --- /dev/null +++ b/src/ProxyApi/ConnectionException.php @@ -0,0 +1,10 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\ProxyApi; + +class ConnectionException extends \Dbp\Relay\CoreBundle\Http\ConnectionException +{ + public const API_ERROR = 101; +} diff --git a/src/ProxyApi/ProxyApi.php b/src/ProxyApi/ProxyApi.php new file mode 100644 index 0000000000000000000000000000000000000000..1e4000f136a5412b4f8e99c017e58fa5bdb6263f --- /dev/null +++ b/src/ProxyApi/ProxyApi.php @@ -0,0 +1,13 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\ProxyApi; + +class ProxyApi +{ + public const PROXY_DATA_EVENT_NAME = ''; + + public const PROXY_DATA_RETURN_VALUE_KEY = 'data'; + public const PROXY_DATA_ERRORS_KEY = 'errors'; +} diff --git a/src/ProxyApi/ProxyDataEvent.php b/src/ProxyApi/ProxyDataEvent.php new file mode 100644 index 0000000000000000000000000000000000000000..8db1e2594faf18c00a4fffc4eb5a4e1c5e5d8b58 --- /dev/null +++ b/src/ProxyApi/ProxyDataEvent.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\ProxyApi; + +use Symfony\Contracts\EventDispatcher\Event; + +class ProxyDataEvent extends Event +{ + public const NAME = 'dbp.relay.proxy_bundle.proxy_data'; + + /** @var ProxyDataInterface */ + private $proxyData; + + /** @var bool */ + private $wasHandled; + + public function __construct(ProxyDataInterface $proxyData) + { + $this->proxyData = $proxyData; + $this->wasHandled = false; + } + + public function getProxyData(): ProxyDataInterface + { + return $this->proxyData; + } + + /** + * Indicate, that the event was handled, e.g. there was an event subscriber for the requested proxy data namespace. + */ + public function setHandled(): void + { + $this->wasHandled = true; + } + + /** + * True, if the event was handled, e.g. there was an event subscriber for the requested proxy data namespace, false otherwise. + */ + public function wasHandled(): bool + { + return $this->wasHandled; + } +} diff --git a/src/ProxyApi/ProxyDataEventSubscriber.php b/src/ProxyApi/ProxyDataEventSubscriber.php new file mode 100644 index 0000000000000000000000000000000000000000..d2c739843d73b6ba8c5a63a2aaeb6107d5fdb31a --- /dev/null +++ b/src/ProxyApi/ProxyDataEventSubscriber.php @@ -0,0 +1,84 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\ProxyApi; + +use Dbp\Relay\CoreBundle\Authorization\AuthorizationDataProviderProvider; +use Dbp\Relay\CoreBundle\Helpers\Tools; +use Exception; + +class ProxyDataEventSubscriber extends AbstractProxyDataEventSubscriber +{ + protected const NAMESPACE = 'core'; + + public const GET_AVAILABLE_ATTRIBUTES_FUNCTION_NAME = 'getAvailableAttributes'; + public const GET_USER_ATTRIBUTES_FUNCTION_NAME = 'getUserAttributes'; + + public const USER_ID_PARAMETER_NAME = 'userId'; + + /** + * @var AuthorizationDataProviderProvider + */ + private $provider; + + public function __construct(AuthorizationDataProviderProvider $provider) + { + $this->provider = $provider; + } + + protected function isFunctionDefined(string $functionName): bool + { + return + $functionName === self::GET_AVAILABLE_ATTRIBUTES_FUNCTION_NAME || + $functionName === self::GET_USER_ATTRIBUTES_FUNCTION_NAME; + } + + protected function areAllRequiredArgumentsDefined(string $functionName, array $arguments): bool + { + return + $functionName !== self::GET_USER_ATTRIBUTES_FUNCTION_NAME || + !Tools::isNullOrEmpty($arguments[self::USER_ID_PARAMETER_NAME] ?? null); + } + + /** + * @throws Exception + */ + protected function callFunction(string $functionName, array $arguments): ?array + { + $returnValue = null; + + switch ($functionName) { + case self::GET_AVAILABLE_ATTRIBUTES_FUNCTION_NAME: + $returnValue = $this->getAvailableAttributes(); + break; + case self::GET_USER_ATTRIBUTES_FUNCTION_NAME: + $returnValue = $this->getUserAttributes($arguments[self::USER_ID_PARAMETER_NAME]); + break; + } + + return $returnValue; + } + + private function getAvailableAttributes(): array + { + $availableAttributes = []; + + foreach ($this->provider->getAuthorizationDataProviders() as $provider) { + $availableAttributes = array_merge($availableAttributes, $provider->getAvailableAttributes()); + } + + return $availableAttributes; + } + + private function getUserAttributes(string $userId): array + { + $userAttributes = []; + + foreach ($this->provider->getAuthorizationDataProviders() as $provider) { + $userAttributes = array_merge($userAttributes, $provider->getUserAttributes($userId)); + } + + return $userAttributes; + } +} diff --git a/src/ProxyApi/ProxyDataInterface.php b/src/ProxyApi/ProxyDataInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..3c5d6e613deb0b11518d4f1bf81d8ced48f4175c --- /dev/null +++ b/src/ProxyApi/ProxyDataInterface.php @@ -0,0 +1,16 @@ +<?php + +declare(strict_types=1); + +namespace Dbp\Relay\CoreBundle\ProxyApi; + +interface ProxyDataInterface +{ + public function getArguments(): array; + + public function getFunctionName(): ?string; + + public function setData($data); + + public function setErrorsFromException(\Exception $exception): void; +}