254 lines
12 KiB
PHP
254 lines
12 KiB
PHP
<?php
|
|
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
namespace CloudObjects\SDK\WebAPI;
|
|
|
|
use Exception;
|
|
use ML\IRI\IRI;
|
|
use ML\JsonLD\Node;
|
|
use CloudObjects\SDK\NodeReader;
|
|
use GuzzleHttp\Client, GuzzleHttp\HandlerStack, GuzzleHttp\Middleware;
|
|
use Psr\Http\Message\RequestInterface;
|
|
use CloudObjects\SDK\COIDParser, CloudObjects\SDK\ObjectRetriever;
|
|
use CloudObjects\SDK\Exceptions\InvalidObjectConfigurationException,
|
|
CloudObjects\SDK\Exceptions\CoreAPIException;
|
|
|
|
/**
|
|
* The APIClientFactory can be used to create a preconfigured Guzzle HTTP API client
|
|
* based on the configuration data available for an API on CloudObjects.
|
|
*/
|
|
class APIClientFactory {
|
|
|
|
const DEFAULT_CONNECT_TIMEOUT = 5;
|
|
const DEFAULT_TIMEOUT = 20;
|
|
|
|
private $objectRetriever;
|
|
private $namespace;
|
|
private $reader;
|
|
private $apiClients = [];
|
|
|
|
private function configureAPIKeyAuthentication(Node $api, array $clientConfig) {
|
|
// see also: https://coid.link/webapis.co-n.net/APIKeyAuthentication
|
|
|
|
$apiKey = $this->reader->getFirstValueString($api, 'wa:hasFixedAPIKey');
|
|
|
|
if (!isset($apiKey)) {
|
|
$apiKeyProperty = $this->reader->getFirstValueString($api, 'wa:usesAPIKeyFrom');
|
|
if (!isset($apiKeyProperty))
|
|
throw new InvalidObjectConfigurationException("An API must have either a fixed API key or a defined API key property.");
|
|
$apiKey = $this->reader->getFirstValueString($this->namespace, $apiKeyProperty);
|
|
if (!isset($apiKey))
|
|
throw new InvalidObjectConfigurationException("The namespace does not have a value for <".$apiKeyProperty.">.");
|
|
}
|
|
|
|
$parameter = $this->reader->getFirstValueNode($api, 'wa:usesAuthenticationParameter');
|
|
|
|
if (!isset($parameter) || !$this->reader->hasProperty($parameter, 'wa:hasKey'))
|
|
throw new InvalidObjectConfigurationException("The API does not declare a parameter for inserting the API key.");
|
|
|
|
$parameterName = $this->reader->getFirstValueString($parameter, 'wa:hasKey');
|
|
|
|
if ($this->reader->hasType($parameter, 'wa:HeaderParameter'))
|
|
$clientConfig['headers'][$parameterName] = $apiKey;
|
|
|
|
elseif ($this->reader->hasType($parameter, 'wa:QueryParameter')) {
|
|
// Guzzle currently doesn't merge query strings from default options and the request itself,
|
|
// therefore we're implementing this behavior with a custom middleware
|
|
$handler = HandlerStack::create();
|
|
$handler->push(Middleware::mapRequest(function (RequestInterface $request) use ($parameterName, $apiKey) {
|
|
$uri = $request->getUri();
|
|
$uri = $uri->withQuery(
|
|
(!empty($uri->getQuery()) ? $uri->getQuery().'&' : '')
|
|
. urlencode($parameterName).'='.urlencode($apiKey)
|
|
);
|
|
return $request->withUri($uri);
|
|
}));
|
|
$clientConfig['handler'] = $handler;
|
|
}
|
|
|
|
else
|
|
throw new InvalidObjectConfigurationException("The authentication parameter must be either <wa:HeaderParameter> or <wa:QueryParameter>.");
|
|
|
|
return $clientConfig;
|
|
}
|
|
|
|
private function configureBearerTokenAuthentication(Node $api, array $clientConfig) {
|
|
// see also: https://coid.link/webapis.co-n.net/HTTPBasicAuthentication
|
|
|
|
$accessToken = $this->reader->getFirstValueString($api, 'oauth2:hasFixedBearerToken');
|
|
|
|
if (!isset($accessToken)) {
|
|
$tokenProperty = $this->reader->getFirstValueString($api, 'oauth2:usesFixedBearerTokenFrom');
|
|
if (!isset($tokenProperty))
|
|
throw new InvalidObjectConfigurationException("An API must have either a fixed access token or a defined token property.");
|
|
$accessToken = $this->reader->getFirstValueString($this->namespace, $tokenProperty);
|
|
if (!isset($accessToken))
|
|
throw new InvalidObjectConfigurationException("The namespace does not have a value for <".$tokenProperty.">.");
|
|
}
|
|
|
|
$clientConfig['headers']['Authorization'] = 'Bearer ' . $accessToken;
|
|
|
|
return $clientConfig;
|
|
}
|
|
|
|
private function configureBasicAuthentication(Node $api, array $clientConfig) {
|
|
// see also: https://coid.link/webapis.co-n.net/HTTPBasicAuthentication
|
|
|
|
$username = $this->reader->getFirstValueString($api, 'wa:hasFixedUsername');
|
|
$password = $this->reader->getFirstValueString($api, 'wa:hasFixedPassword');
|
|
|
|
if (!isset($username)) {
|
|
$usernameProperty = $this->reader->getFirstValueString($api, 'wa:usesUsernameFrom');
|
|
if (!isset($usernameProperty))
|
|
throw new InvalidObjectConfigurationException("An API must have either a fixed username or a defined username property.");
|
|
$username = $this->reader->getFirstValueString($this->namespace, $usernameProperty);
|
|
if (!isset($username))
|
|
throw new InvalidObjectConfigurationException("The namespace does not have a value for <".$usernameProperty.">.");
|
|
}
|
|
|
|
if (!isset($password)) {
|
|
$passwordProperty = $this->reader->getFirstValueString($api, 'wa:usesPasswordFrom');
|
|
if (!isset($passwordProperty))
|
|
throw new InvalidObjectConfigurationException("An API must have either a fixed password or a defined password property.");
|
|
$password = $this->reader->getFirstValueString($this->namespace, $passwordProperty);
|
|
if (!isset($password))
|
|
throw new InvalidObjectConfigurationException("The namespace does not have a value for <".$passwordProperty.">.");
|
|
}
|
|
|
|
$clientConfig['auth'] = [$username, $password];
|
|
return $clientConfig;
|
|
}
|
|
|
|
private function configureSharedSecretBasicAuthentication(Node $api, array $clientConfig) {
|
|
// see also: https://coid.link/webapis.co-n.net/SharedSecretAuthenticationViaHTTPBasic
|
|
|
|
$username = COIDParser::fromString($this->namespace->getId())->getHost();
|
|
|
|
$apiCoid = COIDParser::fromString($api->getId());
|
|
$providerNamespaceCoid = COIDParser::getNamespaceCOID($apiCoid);
|
|
$providerNamespace = $this->objectRetriever->get($providerNamespaceCoid);
|
|
$sharedSecret = $this->reader->getAllValuesNode($providerNamespace, 'co:hasSharedSecret');
|
|
if (count($sharedSecret) != 1)
|
|
throw new CoreAPIException("Could not retrieve the shared secret.");
|
|
|
|
$password = $this->reader->getFirstValueString($sharedSecret[0], 'co:hasTokenValue');
|
|
|
|
$clientConfig['auth'] = [$username, $password];
|
|
return $clientConfig;
|
|
}
|
|
|
|
private function createClient(Node $api, bool $specificClient = false) {
|
|
if (!$this->reader->hasType($api, 'wa:HTTPEndpoint'))
|
|
throw new InvalidObjectConfigurationException("The API node must have the type <coid://webapis.co-n.net/HTTPEndpoint>.");
|
|
|
|
$baseUrl = $this->reader->getFirstValueString($api, 'wa:hasBaseURL');
|
|
if (!isset($baseUrl))
|
|
throw new InvalidObjectConfigurationException("The API must have a base URL.");
|
|
|
|
$clientConfig = [
|
|
'base_uri' => $baseUrl,
|
|
'connect_timeout' => self::DEFAULT_CONNECT_TIMEOUT,
|
|
'timeout' => self::DEFAULT_TIMEOUT
|
|
];
|
|
|
|
if ($this->reader->hasProperty($api, 'oauth2:hasAuthorizationServer')) {
|
|
// We have an authorization server for this endpoint/API
|
|
$authServerCoid = $this->reader->getFirstValueIRI($api, 'oauth2:hasAuthorizationServer');
|
|
$authServerObject = $this->objectRetriever->getObject($authServerCoid);
|
|
if (!isset($authServerObject))
|
|
throw new InvalidObjectConfigurationException("Authorization server object <"
|
|
. (string)$authServerCoid . "> not available.");
|
|
|
|
try {
|
|
$authServer = new OAuth2AuthServer($authServerObject, $this->objectRetriever);
|
|
} catch (InvalidObjectConfigurationException $e) {
|
|
throw new InvalidObjectConfigurationException("Authorization server object <"
|
|
. (string)$authServerCoid . "> could not be loaded; error: " . $e->getMessage());
|
|
} catch (Exception $e) {
|
|
throw new InvalidObjectConfigurationException("Authorization server object <"
|
|
. (string)$authServerCoid . "> could not be loaded. Its definition may be invalid.");
|
|
}
|
|
|
|
try {
|
|
$authServer->configureConsumer($this->namespace);
|
|
} catch (Exception $e) {
|
|
throw new InvalidObjectConfigurationException("The namespace <" . $this->namespace->getId()
|
|
. "> does not contain valid configuration to use the authorization server <"
|
|
. (string)$authServerCoid . ">.");
|
|
}
|
|
|
|
// Get access token through the auth server
|
|
$clientConfig['headers']['Authorization'] = 'Bearer ' . $authServer->getAccessToken();
|
|
} elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
|
|
'wa:APIKeyAuthentication')) {
|
|
// API key authentication
|
|
$clientConfig = $this->configureAPIKeyAuthentication($api, $clientConfig);
|
|
} elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
|
|
'oauth2:FixedBearerTokenAuthentication')) {
|
|
// Fixed bearer token authentication
|
|
$clientConfig = $this->configureBearerTokenAuthentication($api, $clientConfig);
|
|
} elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
|
|
'wa:HTTPBasicAuthentication')) {
|
|
// HTTP Basic authentication
|
|
$clientConfig = $this->configureBasicAuthentication($api, $clientConfig);
|
|
} elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
|
|
'wa:SharedSecretAuthenticationViaHTTPBasic')) {
|
|
// HTTP Basic authentication using shared secrets in CloudObjects Core
|
|
$clientConfig = $this->configureSharedSecretBasicAuthentication($api, $clientConfig);
|
|
}
|
|
|
|
if ($specificClient == false)
|
|
return new Client($clientConfig);
|
|
|
|
if ($this->reader->hasType($api, 'wa:GraphQLEndpoint')) {
|
|
if (!class_exists('GraphQL\Client'))
|
|
throw new Exception("Install the gmostafa/php-graphql-client package to retrieve a specific client for wa:GraphQLEndpoint objects.");
|
|
|
|
return new \GraphQL\Client($clientConfig['base_uri'],
|
|
isset($clientConfig['headers']) ? $clientConfig['headers'] : []);
|
|
} else
|
|
return new Client($clientConfig);
|
|
}
|
|
|
|
/**
|
|
* @param ObjectRetriever $objectRetriever An initialized and authenticated object retriever.
|
|
* @param IRI|null $namespaceCoid The namespace of the API client. Used to retrieve credentials. If this parameter is not provided, the namespace provided with the "auth_ns" configuration option from the object retriever is used.
|
|
*/
|
|
public function __construct(ObjectRetriever $objectRetriever, IRI $namespaceCoid = null) {
|
|
$this->objectRetriever = $objectRetriever;
|
|
$this->namespace = isset($namespaceCoid)
|
|
? $objectRetriever->getObject($namespaceCoid)
|
|
: $objectRetriever->getAuthenticatingNamespaceObject();
|
|
|
|
$this->reader = new NodeReader([
|
|
'prefixes' => [
|
|
'co' => 'coid://cloudobjects.io/',
|
|
'wa' => 'coid://webapis.co-n.net/',
|
|
'oauth2' => 'coid://oauth2.co-n.net/'
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Get an API client for the WebAPI with the specified COID.
|
|
*
|
|
* @param IRI $apiCoid WebAPI COID
|
|
* @param boolean $specificClient If TRUE, returns a specific client class based on the API type. If FALSE, always returns a Guzzle client. Defaults to FALSE.
|
|
* @return Client
|
|
*/
|
|
public function getClientWithCOID(IRI $apiCoid, bool $specificClient = false) {
|
|
$idString = (string)$apiCoid.(string)$specificClient;
|
|
if (!isset($this->apiClients[$idString])) {
|
|
$object = $this->objectRetriever->getObject($apiCoid);
|
|
if (!isset($object))
|
|
throw new CoreAPIException("Could not retrieve API <".(string)$apiCoid.">.");
|
|
$this->apiClients[$idString] = $this->createClient($object, $specificClient);
|
|
}
|
|
|
|
return $this->apiClients[$idString];
|
|
}
|
|
|
|
} |