Imported code from old repository

This commit is contained in:
2022-11-17 11:48:29 +01:00
parent 14dfa45240
commit 0e4ab60d77
31 changed files with 6486 additions and 253 deletions

View File

@@ -0,0 +1,79 @@
<?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\AccountGateway;
use ML\IRI\IRI;
class AAUIDParser {
const AAUID_INVALID = 0;
const AAUID_ACCOUNT = 1;
const AAUID_CONNECTION = 2;
const AAUID_CONNECTED_ACCOUNT = 3;
const REGEX_AAUID = "/^[a-z0-9]{16}$/";
const REGEX_QUALIFIER = "/^[A-Z]{2}$/";
/**
* Creates a new IRI object representing a AAUID from a string.
* Adds the "aauid:" prefix if necessary.
*
* @param string $aauidString An AAUID string.
* @return IRI
*/
public static function fromString($aauidString) {
return new IRI(
(substr($aauidString, 0, 6)=='aauid:') ? $aauidString : 'aauid:'.$aauidString
);
}
public static function getType(IRI $iri) {
if ($iri->getScheme()!='aauid' || $iri->getPath()=='')
return self::AAUID_INVALID;
$segments = explode(':', $iri->getPath());
switch (count($segments)) {
case 1:
return (preg_match(self::REGEX_AAUID, $segments[0]) == 1)
? self::AAUID_ACCOUNT
: self::AAUID_INVALID;
case 3;
if (preg_match(self::REGEX_AAUID, $segments[0]) != 1
|| preg_match(self::REGEX_QUALIFIER, $segments[2]) != 1)
return self::AAUID_INVALID;
switch ($segments[1]) {
case "connection":
return self::AAUID_CONNECTION;
case "account":
return self::AAUID_CONNECTED_ACCOUNT;
default:
return self::AAUID_INVALID;
}
default:
return self::AAUID_INVALID;
}
}
public static function getAAUID(IRI $iri) {
if (self::getType($iri)!=self::AAUID_INVALID) {
$segments = explode(':', $iri->getPath());
return $segments[0];
} else
return null;
}
public static function getQualifier(IRI $iri) {
if (self::getType($iri)==self::AAUID_CONNECTION
|| self::getType($iri)==self::AAUID_CONNECTED_ACCOUNT) {
$segments = explode(':', $iri->getPath());
return $segments[2];
} else
return null;
}
}

View File

@@ -0,0 +1,369 @@
<?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\AccountGateway;
use ML\IRI\IRI;
use ML\JsonLD\Document, ML\JsonLD\JsonLD, ML\JsonLD\Node;
use Symfony\Component\HttpFoundation\Request, Symfony\Component\HttpFoundation\Response;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use GuzzleHttp\Client, GuzzleHttp\HandlerStack, GuzzleHttp\Middleware;
use Psr\Http\Message\RequestInterface, Psr\Http\Message\ResponseInterface;
/**
* The context of an request for an account.
*/
class AccountContext {
private $agwBaseUrl = 'https://{aauid}.aauid.net';
private $aauid;
private $accessToken;
private $dataLoader;
private $accountDomain = null;
private $connectionQualifier = null;
private $installQualifier = null;
private $accessor = null;
private $latestAccessorVersionCOID = null;
private $request; // optional
private $document;
private $client;
private $logCode = null;
/**
* Create a new context using an AAUID and an OAuth 2.0 bearer access token.
*/
public function __construct(IRI $aauid, $accessToken, DataLoader $dataLoader = null) {
if (AAUIDParser::getType($aauid) != AAUIDParser::AAUID_ACCOUNT)
throw new \Exception("Not a valid AAUID");
$this->aauid = $aauid;
$this->accessToken = $accessToken;
if ($dataLoader) {
$this->dataLoader = $dataLoader;
} else {
$this->dataLoader = new DataLoader;
}
}
private function parseHeaderIntoNode($headerName, Node $node) {
$keyValuePairs = explode(',', $this->request->getHeaderLine($headerName));
foreach ($keyValuePairs as $pair) {
$keyValue = explode('=', $pair);
$node->addPropertyValue($keyValue[0], urldecode($keyValue[1]));
}
}
private function parsePsrRequest(RequestInterface $request) {
$this->request = $request;
if ($request->hasHeader('C-Accessor')) {
// Store COID of Accessor
$this->accessor = new IRI($request->getHeaderLine('C-Accessor'));
}
if ($request->hasHeader('C-Account-Domain')) {
// Store account domain
$this->accountDomain = $request->getHeaderLine('C-Account-Domain');
}
if ($request->hasHeader('C-Accessor-Latest-Version')) {
// A new version of thie accessor is available, store its COID
$this->latestAccessorVersionCOID = new IRI($request
->getHeaderLine('C-Accessor-Latest-Version'));
}
if ($request->hasHeader('C-Account-Connection')) {
// For access from connected accounts, store qualifier
$this->connectionQualifier = $request->getHeaderLine('C-Account-Connection');
}
if ($request->hasHeader('C-Install-Connection')) {
// For access from applications, store qualifier
$this->installQualifier = $request->getHeaderLine('C-Install-Connection');
}
if ($request->hasHeader('C-Connection-Data')) {
// Copy Data into document
if (!$this->document) $this->document = new Document();
$this->parseHeaderIntoNode('C-Connection-Data',
$this->document->getGraph()->createNode('aauid:'.$this->getAAUID().':connection:'.$this->connectionQualifier));
}
}
/**
* Create a new context from the current request.
*
* @param Request $request
*/
public static function fromSymfonyRequest(Request $request) {
if (!$request->headers->has('C-AAUID') || !$request->headers->has('C-Access-Token'))
return null;
$context = new AccountContext(
new IRI('aauid:'.$request->headers->get('C-AAUID')),
$request->headers->get('C-Access-Token'));
$psr7Factory = new DiactorosFactory;
$context->parsePsrRequest($psr7Factory->createRequest($request));
return $context;
}
/**
* Create a new context from the current request.
*
* @param RequestInterface $request
*/
public static function fromPsrRequest(RequestInterface $request) {
if (!$request->hasHeader('C-AAUID') || !$request->hasHeader('C-Access-Token'))
return null;
$context = new AccountContext(
new IRI('aauid:'.$request->getHeaderLine('C-AAUID')),
$request->getHeaderLine('C-Access-Token'));
$context->parsePsrRequest($request);
return $context;
}
public function getAAUID() {
return $this->aauid;
}
public function getAccessToken() {
return $this->accessToken;
}
public function getRequest() {
return $this->request;
}
public function getDataLoader() {
return $this->dataLoader;
}
private function getDocument() {
if (!$this->document) {
$this->document = $this->dataLoader->fetchAccountGraphDataDocument($this);
}
return $this->document;
}
public function getAccount() {
return $this->getDocument()->getGraph()->getNode($this->getAAUID());
}
public function getPerson() {
return $this->getDocument()->getGraph()->getNode($this->getAAUID().':person');
}
/**
* Checks whether the context uses an account connection, which is the case when an API
* is requested by a connected account on another service.
*/
public function usesAccountConnection() {
return ($this->connectionQualifier !== null);
}
/**
* Get the qualifier of the account connection used for accessing the API.
*/
public function getConnectionQualifier() {
return $this->connectionQualifier;
}
/**
* Get the qualifier for the connection to the platform service.
* Only available when the accessor is an application.
*/
public function getInstallQualifier() {
return $this->installQualifier;
}
/**
* Get the accessor.
*/
public function getAccessorCOID() {
return $this->accessor;
}
/**
* Get the account's domain.
* Only set from external API requests, null otherwise.
*
* @return string|null
*/
public function getAccountDomain() {
return $this->accountDomain;
}
/**
* Get a connected account.
* @param $qualifier The qualifier for the account connection. If not specified, uses the connection qualifier.
*/
public function getConnectedAccount($qualifier = null) {
if (!$qualifier) $qualifier = $this->getConnectionQualifier();
if (!$qualifier) return null;
return $this->getDocument()->getGraph()->getNode($this->getAAUID().':account:'.$qualifier);
}
/**
* Get an account connection.
* @param $qualifier The qualifier for the account connection. If not specified, uses the connection qualifier.
*/
public function getAccountConnection($qualifier = null) {
if (!$qualifier) $qualifier = $this->getConnectionQualifier();
if (!$qualifier) return null;
return $this->getDocument()->getGraph()->getNode($this->getAAUID().':connection:'.$qualifier);
}
/**
* Get the connected account for a service.
* @param $service COID of the service
*/
public function getConnectedAccountForService($service) {
$accounts = $this->getDocument()->getGraph()->getNodesByType('coid://aauid.net/Account');
foreach ($accounts as $a) {
if ($a->getProperty('coid://aauid.net/isForService')
&& $a->getProperty('coid://aauid.net/isForService')->getId()==$service) return $a;
}
return null;
}
/**
* Get all account connections.
*/
public function getAllAccountConnections() {
$connections = $this->getAccount()->getProperty('coid://aauid.net/hasConnection');
if (!is_array($connections)) $connections = array($connections);
return $connections;
}
/**
* Get all connected accounts.
*/
public function getAllConnectedAccounts() {
$accounts = array();
foreach ($this->getAllAccountConnections() as $ac) {
$accounts[] = $ac->getProperty('coid://aauid.net/connectsTo');
}
return $accounts;
}
/**
* Pushes changes on the Account Graph into the Account Graph.
*/
public function pushGraphUpdates() {
$this->getClient()->post('/~/', [
'headers' => ['Content-Type' => 'application/ld+json'],
'body' => JsonLD::toString($this->getDocument()->toJsonLd())
]);
}
/**
* Specifies a template for the Account Gateway Base URL. Must be a valid URL that
* may contain an {aauid} placeholder. Call this if you want to redirect traffic
* through a proxy or a staging or mock instance of an Account Gateway. Most users
* of this SDK should never call this function.
*/
public function setAccountGatewayBaseURLTemplate($baseUrl) {
$this->agwBaseUrl = $baseUrl;
}
/**
* Get a preconfigured Guzzle client to access the Account Gateway.
* @return Client
*/
public function getClient() {
if (!$this->client) {
// Create custom handler stack with middlewares
$stack = HandlerStack::create();
$context = $this;
$stack->push(Middleware::mapResponse(function (ResponseInterface $response) use ($context) {
// If a new version of this accessor is available, store its COID
if ($response->hasHeader('C-Accessor-Latest-Version'))
$context->setLatestAccessorVersionCOID(
new IRI($response->getHeaderLine('C-Accessor-Latest-Version')));
return $response;
}));
// Prepare client options
$options = [
'base_uri' => str_replace('{aauid}', AAUIDParser::getAAUID($this->getAAUID()), $this->agwBaseUrl),
'headers' => [
'Authorization' => 'Bearer '.$this->getAccessToken()
],
'handler' => $stack
];
if (isset($this->request) && $this->request->hasHeader('X-Forwarded-For')) {
$options['headers']['X-Forwarded-For'] = $this->request->getHeaderLine('X-Forwarded-For');
}
// Create client
$this->client = new Client($options);
}
return $this->client;
}
/**
* Set a custom code for the current request in the Account Gateway logs.
*/
public function setLogCode($logCode) {
if (!$this->request) {
throw new \Exception('Not in a request context.');
}
$this->logCode = $logCode;
}
/**
* Process a response and add headers if applicable.
*/
public function processResponse(Response $response) {
if ($this->logCode) {
$response->headers->set('C-Code-For-Logger', $this->logCode);
}
}
/**
* Check whether a new version of the accessor is available. This information
* is updated from incoming and outgoing requests. If no request was executed,
* returns false.
*
* @return boolean
*/
public function isNewAccessorVersionAvailable() {
return isset($this->latestAccessorVersionCOID);
}
/**
* Get the COID of the latest accessor version, if one is available, or
* null otherwise. This information is updated from incoming and outgoing
* requests. If no request was executed, returns null.
*
* @return IRI|null
*/
public function getLatestAccessorVersionCOID() {
return $this->latestAccessorVersionCOID;
}
/**
* Set the COID of the latest accessor version. This method should only
* called from request processing codes. Most developers should not use it.
*
* @param IRI $latestAccessorVersionCOID
*/
public function setLatestAccessorVersionCOID(IRI $latestAccessorVersionCOID) {
$this->latestAccessorVersionCOID = $latestAccessorVersionCOID;
}
}

View File

@@ -0,0 +1,81 @@
<?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\AccountGateway;
use Doctrine\Common\Cache\Cache;
use ML\JsonLD\JsonLD;
use GuzzleHttp\Psr7\Request;
class DataLoader {
const CACHE_TTL = 172800; // cache at most 48 hours
private $cache;
private $cachePrefix = 'accdata:';
private $mountPointName = '~';
public function getCache() {
return $this->cache;
}
public function setCache(Cache $cache) {
$this->cache = $cache;
return $this;
}
public function getCachePrefix() {
return $this->cachePrefix;
}
public function setCachePrefix($cachePrefix) {
$this->cachePrefix = $cachePrefix;
return $this;
}
public function getMountPointName() {
return $this->mountPointName;
}
public function setMountPointName($mountPointName) {
$this->mountPointName = $mountPointName;
return $this;
}
public function fetchAccountGraphDataDocument(AccountContext $accountContext) {
$dataRequest = new Request('GET', '/'.$this->mountPointName.'/',
['Accept' => 'application/ld+json']);
if (!$this->cache || !$accountContext->getRequest()
|| !$accountContext->getRequest()->hasHeader('C-Data-Updated')) {
// No cache or no timestamp available, so always fetch from Account Gateway
$dataString = (string)$accountContext->getClient()->send($dataRequest)->getBody();
} else {
$key = $this->cachePrefix.$accountContext->getAAUID();
$remoteTimestamp = $accountContext->getRequest()->getHeaderLine('C-Data-Updated');
if ($this->cache->contains($key)) {
// Check timestamp
$cacheEntry = $this->cache->fetch($key);
$timestamp = substr($cacheEntry, 0, strpos($cacheEntry, '|'));
if ($timestamp==$remoteTimestamp) {
// Cache data is up to date, can be returned
$dataString = substr($cacheEntry, strpos($cacheEntry, '|')+1);
} else {
// Fetch from Account Gateway and update cache entry
$dataString = (string)$accountContext->getClient()->send($dataRequest)->getBody();
$this->cache->save($key, $remoteTimestamp.'|'.$dataString, self::CACHE_TTL);
}
} else {
// Fetch from Account Gateway and store in cache
$dataString = (string)$accountContext->getClient()->send($dataRequest)->getBody();
$this->cache->save($key, $remoteTimestamp.'|'.$dataString, self::CACHE_TTL);
}
}
return JsonLD::getDocument($dataString);
}
}

View File

@@ -0,0 +1,152 @@
<?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;
use ML\IRI\IRI;
/**
* The COIDParser can be used to validate COIDs and extract information.
*/
class COIDParser {
const COID_INVALID = 0;
const COID_ROOT = 1;
const COID_UNVERSIONED = 2;
const COID_VERSIONED = 3;
const COID_VERSION_WILDCARD = 4;
const REGEX_HOSTNAME = "/^([a-z0-9-]+\.)?[a-z0-9-]+\.[a-z]+$/";
const REGEX_SEGMENT = "/^[A-Za-z-_0-9\.]+$/";
const REGEX_VERSION_WILDCARD = "/^((\^|~)(\d+\.)?\d|(\d+\.){1,2}\*)$/";
/**
* Creates a new IRI object representing a COID from a string.
* Adds the "coid://" prefix if necessary and normalizes case.
*
* @param string $coidString A COID string.
* @return IRI
*/
public static function fromString($coidString) {
$coidPre = new IRI(
(strtolower(substr($coidString, 0, 7))=='coid://') ? $coidString : 'coid://'.$coidString
);
// Normalize scheme and host segments to lower case
return new IRI('coid://'.strtolower($coidPre->getHost()).$coidPre->getPath());
}
/**
* Get the type of a COID.
*
* @param IRI $coid
* @return int|null
*/
public static function getType(IRI $coid) {
if ($coid->getScheme()!='coid' || $coid->getHost()==''
|| preg_match(self::REGEX_HOSTNAME, $coid->getHost()) != 1)
return self::COID_INVALID;
if ($coid->getPath()=='' || $coid->getPath()=='/')
return self::COID_ROOT;
$segments = explode('/', $coid->getPath());
switch (count($segments)) {
case 2:
return (preg_match(self::REGEX_SEGMENT, $segments[1]) == 1)
? self::COID_UNVERSIONED
: self::COID_INVALID;
case 3:
if (preg_match(self::REGEX_SEGMENT, $segments[1]) != 1)
return self::COID_INVALID;
if (preg_match(self::REGEX_SEGMENT, $segments[2]) == 1)
return self::COID_VERSIONED;
else
if (preg_match(self::REGEX_VERSION_WILDCARD, $segments[2]) == 1)
return self::COID_VERSION_WILDCARD;
else
return self::COID_INVALID;
default:
return self::COID_INVALID;
}
}
/**
* Checks whether the given IRI object is a valid COID.
*
* @param IRI $coid
* @return boolean
*/
public static function isValidCOID(IRI $coid) {
return (self::getType($coid)!=self::COID_INVALID);
}
/**
* Get the name segment of a valid COID or null if not available.
*
* @param IRI $coid
* @return string|null
*/
public static function getName(IRI $coid) {
if (self::getType($coid)!=self::COID_INVALID
&& self::getType($coid)!=self::COID_ROOT) {
$segments = explode('/', $coid->getPath());
return $segments[1];
} else
return null;
}
/**
* Get the version segment of a valid, versioned COID or null if not available.
*
* @param IRI $coid
* @return string|null
*/
public static function getVersion(IRI $coid) {
if (self::getType($coid)==self::COID_VERSIONED) {
$segments = explode('/', $coid->getPath());
return $segments[2];
} else
return null;
}
/**
* Get the version segment of a versioned or version wildcard COID or
* null if not available.
*
* @param IRI $coid
* @return string|null
*/
public static function getVersionWildcard(IRI $coid) {
if (self::getType($coid)==self::COID_VERSION_WILDCARD) {
$segments = explode('/', $coid->getPath());
return $segments[2];
} else
return null;
}
/**
* Returns the COID itself if it is a root COID or a new IRI object
* representing the namespace underlying the given COID.
*
* @param IRI $coid
* @return IRI|null
*/
public static function getNamespaceCOID(IRI $coid) {
switch (self::getType($coid)) {
case self::COID_ROOT:
return $coid;
case self::COID_UNVERSIONED:
case self::COID_VERSIONED:
case self::COID_VERSION_WILDCARD:
return new IRI('coid://'.$coid->getHost());
default:
return null;
}
}
}

View File

@@ -0,0 +1,72 @@
<?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\Common;
use Exception;
use ML\IRI\IRI;
use CloudObjects\SDK\NodeReader;
use Defuse\Crypto\Key, Defuse\Crypto\Crypto;
use CloudObjects\SDK\COIDParser, CloudObjects\SDK\ObjectRetriever;
use CloudObjects\SDK\Exceptions\InvalidObjectConfigurationException;
/**
* The crypto helper can be used to encrypt or decrypt data with
* the defuse PHP encryption library.
*/
class CryptoHelper {
private $objectRetriever;
private $namespace;
private $reader;
/**
* Gets a key based on the coid://common.cloudobjects.io/usesSharedEncryptionKey value
* for the default namespace.
*/
public function getSharedEncryptionKey() {
$keyValue = $this->reader->getFirstValueString($this->namespace, 'common:usesSharedEncryptionKey');
if (!isset($keyValue))
throw new InvalidObjectConfigurationException("The namespace doesn't have an encryption key.");
return Key::loadFromAsciiSafeString($keyValue);
}
/**
* Encrypt data with the default namespace's shared encryption key.
*/
public function encryptWithSharedEncryptionKey($data) {
return Crypto::encrypt($data, $this->getSharedEncryptionKey());
}
/**
* Decrypt data with the default namespace's shared encryption key.
*/
public function decryptWithSharedEncryptionKey($data) {
return Crypto::decrypt($data, $this->getSharedEncryptionKey());
}
/**
* @param ObjectRetriever $objectRetriever An initialized and authenticated object retriever.
* @param IRI|null $namespaceCoid The namespace used to retrieve keys. 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) {
if (!class_exists('Defuse\Crypto\Crypto'))
throw new Exception("Run composer require defuse/php-encryption before using CryptoHelper.");
$this->objectRetriever = $objectRetriever;
$this->namespace = isset($namespaceCoid)
? $objectRetriever->getObject($namespaceCoid)
: $objectRetriever->getAuthenticatingNamespaceObject();
$this->reader = new NodeReader([
'prefixes' => [
'common' => 'coid://common.cloudobjects.io/'
]
]);
}
}

View File

@@ -0,0 +1,14 @@
<?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\Exceptions;
/**
* An Exception that is thrown when the Core API returned an error.
*/
class CoreAPIException extends \Exception {
}

View File

@@ -0,0 +1,15 @@
<?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\Exceptions;
/**
* An Exception that is thrown when an object's configuration
* doesn't match the client's expectations.
*/
class InvalidObjectConfigurationException extends \Exception {
}

View File

@@ -0,0 +1,85 @@
<?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\Helpers;
use Exception;
use CloudObjects\SDK\NodeReader, CloudObjects\SDK\ObjectRetriever;
/**
* The SDKLoader helper allows developers to quickly load common PHP SDKs
* from API providers and apply configuration stored in CloudObjects.
*/
class SDKLoader {
private $objectRetriever;
private $reader;
private $classes = [];
/**
* @param ObjectRetriever $objectRetriever An initialized and authenticated object retriever.
*/
public function __construct(ObjectRetriever $objectRetriever) {
$this->objectRetriever = $objectRetriever;
$this->reader = new NodeReader;
}
/**
* Initialize and return the SDK with the given classname.
* Throws Exception if the SDK is not supported.
*
* @param $classname Classname for the SDK's main class
* @param array $options Additional options for the SDK (if necessary)
*/
public function get($classname, array $options) {
if (!class_exists($classname))
throw new Exception("<".$classname."> is not a valid classname.");
$hashkey = md5($classname.serialize($options));
if (!isset($this->classes[$hashkey])) {
$nsNode = $this->objectRetriever->getAuthenticatingNamespaceObject();
// --- Amazon Web Services (https://aws.amazon.com/) ---
// has multiple classnames, so check for common superclass
if (is_a($classname, 'Aws\AwsClient', true)) {
$class = new $classname(array_merge($options, [
'credentials' => [
'key' => $this->reader->getFirstValueString($nsNode, 'coid://aws.3rd-party.co/accessKeyId'),
'secret' => $this->reader->getFirstValueString($nsNode, 'coid://aws.3rd-party.co/secretAccessKey')
]
]));
} else {
switch ($classname) {
// --- stream (https://getstream.io/) ---
case "GetStream\Stream\Client":
$class = new $classname(
$this->reader->getFirstValueString($nsNode, 'coid://getstreamio.3rd-party.co/key'),
$this->reader->getFirstValueString($nsNode, 'coid://getstreamio.3rd-party.co/secret')
);
break;
// --- Pusher (https://pusher.com/) ---
case "Pusher":
$class = new $classname(
$this->reader->getFirstValueString($nsNode, 'coid://pusher.3rd-party.co/key'),
$this->reader->getFirstValueString($nsNode, 'coid://pusher.3rd-party.co/secret'),
$this->reader->getFirstValueString($nsNode, 'coid://pusher.3rd-party.co/appId'),
$options
);
break;
}
}
}
if (!isset($class))
throw new Exception("No rules defined to initialize <".$classname.">.");
$this->classes[$hashkey] = $class;
return $this->classes[$hashkey];
}
}

View File

@@ -0,0 +1,110 @@
<?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\Helpers;
use ML\IRI\IRI;
use CloudObjects\SDK\COIDParser, CloudObjects\SDK\NodeReader, CloudObjects\SDK\ObjectRetriever;
/**
* The SharedSecretAuthentication helper allows developers to quickly
* implement authentication based on CloudObjects shared secrets.
*/
class SharedSecretAuthentication {
const RESULT_OK = 0;
const RESULT_INVALID_USERNAME = 1;
const RESULT_INVALID_PASSWORD = 2;
const RESULT_NAMESPACE_NOT_FOUND = 3;
const RESULT_SHARED_SECRET_NOT_RETRIEVABLE = 4;
const RESULT_SHARED_SECRET_INCORRECT = 5;
private $objectRetriever;
/**
* @param ObjectRetriever $objectRetriever An initialized and authenticated object retriever.
*/
public function __construct(ObjectRetriever $objectRetriever) {
$this->objectRetriever = $objectRetriever;
}
/**
* Verifies credentials.
* @deprecated
*
* @param ObjectRetriever $retriever Provides access to CloudObjects.
* @param string $username Username; a domain.
* @param string $password Password; a shared secret.
*
* @return integer A result constant, RESULT_OK if successful.
*/
public static function verifyCredentials(ObjectRetriever $retriever, $username, $password) {
// Validate input
$namespaceCoid = new IRI('coid://'.$username);
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
return self::RESULT_INVALID_USERNAME;
if (strlen($password) != 40)
return self::RESULT_INVALID_PASSWORD;
// Retrieve namespace
$namespace = $retriever->getObject($namespaceCoid);
if (!isset($namespace))
return self::RESULT_NAMESPACE_NOT_FOUND;
// Read and validate shared secret
$reader = new NodeReader([
'prefixes' => [
'co' => 'coid://cloudobjects.io/'
]
]);
$sharedSecret = $reader->getAllValuesNode($namespace, 'co:hasSharedSecret');
if (count($sharedSecret) != 1)
return self::RESULT_SHARED_SECRET_NOT_RETRIEVABLE;
if ($reader->getFirstValueString($sharedSecret[0], 'co:hasTokenValue') == $password)
return self::RESULT_OK;
else
return self::RESULT_SHARED_SECRET_INCORRECT;
}
/**
* Verifies credentials.
*
* @param string $username Username; a domain.
* @param string $password Password; a shared secret.
*
* @return integer A result constant, RESULT_OK if successful.
*/
public function verify($username, $password) {
// Validate input
$namespaceCoid = new IRI('coid://'.$username);
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
return self::RESULT_INVALID_USERNAME;
if (strlen($password) != 40)
return self::RESULT_INVALID_PASSWORD;
// Retrieve namespace
$namespace = $this->objectRetriever->getObject($namespaceCoid);
if (!isset($namespace))
return self::RESULT_NAMESPACE_NOT_FOUND;
// Read and validate shared secret
$reader = new NodeReader([
'prefixes' => [
'co' => 'coid://cloudobjects.io/'
]
]);
$sharedSecret = $reader->getAllValuesNode($namespace, 'co:hasSharedSecret');
if (count($sharedSecret) != 1)
return self::RESULT_SHARED_SECRET_NOT_RETRIEVABLE;
if ($reader->getFirstValueString($sharedSecret[0], 'co:hasTokenValue') == $password)
return self::RESULT_OK;
else
return self::RESULT_SHARED_SECRET_INCORRECT;
}
}

View File

@@ -0,0 +1,76 @@
<?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\JSON;
use ML\IRI\IRI;
use ML\JsonLD\Node;
use Webmozart\Assert\Assert;
use CloudObjects\SDK\ObjectRetriever, CloudObjects\SDK\NodeReader;
/**
* The schema validator enables the validation of data against
* JSON schemas in the CloudObjects RDF format.
*/
class SchemaValidator {
private $objectRetriever;
private $reader;
/**
* @param ObjectRetriever $objectRetriever An initialized and authenticated object retriever.
*/
public function __construct(ObjectRetriever $objectRetriever) {
$this->objectRetriever = $objectRetriever;
$this->reader = new NodeReader([
'prefixes' => [
'json' => 'coid://json.co-n.net/'
]
]);
}
/**
* Validate data against an element specification in an RDF node.
*
* @param mixed $data The data to validate.
* @param Node $node The specification to validate against.
*/
public function validateAgainstNode($data, Node $node) {
if ($this->reader->hasType($node, 'json:String'))
Assert::string($data);
elseif ($this->reader->hasType($node, 'json:Boolean'))
Assert::boolean($data);
elseif ($this->reader->hasType($node, 'json:Number'))
Assert::numeric($data);
elseif ($this->reader->hasType($node, 'json:Integer'))
Assert::integer($data);
elseif ($this->reader->hasType($node, 'json:Array'))
Assert::isArray($data);
elseif ($this->reader->hasType($node, 'json:Object')) {
Assert::isArrayAccessible($data);
foreach ($this->reader->getAllValuesNode($node, 'json:hasProperty') as $prop) {
$key = $this->reader->getFirstValueString($prop, 'json:hasKey');
if ($this->reader->getFirstValueBool($prop, 'json:isRequired') == true)
Assert::keyExists($data, $key);
if (isset($data[$key]))
$this->validateAgainstNode($data[$key], $prop);
}
}
}
/**
* Validate data against a specification stored in CloudObjects.
*
* @param mixed $data The data to validate.
* @param Node $node The COID of the specification.
*/
public function validateAgainstCOID($data, IRI $coid) {
$object = $this->objectRetriever->getObject($coid);
Assert::true($this->reader->hasType($object, 'json:Element'),
"You can only validate data against JSON elements!");
$this->validateAgainstNode($data, $object);
}
}

View File

@@ -0,0 +1,364 @@
<?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;
use ML\JsonLD\Node;
use ML\IRI\IRI;
/**
* The NodeReader provides some convenience methods for reading information
* from an object graph node.
*/
class NodeReader {
private $prefixes = [];
public function __construct(array $options = []) {
if (isset($options['prefixes']))
$this->prefixes = $options['prefixes'];
}
private function expand($uri) {
if (!is_string($uri)) $uri = (string)$uri;
$scheme = parse_url($uri, PHP_URL_SCHEME);
if (isset($scheme) && isset($this->prefixes[$scheme]))
return str_replace($scheme.':', $this->prefixes[$scheme], $uri);
else
return $uri;
}
/**
* Checks whether a node has a certain type.
*
* @param Node $node The node to work on.
* @param string|object $type The type to check for.
* @return boolean
*/
public function hasType(Node $node = null, $type) {
if (!isset($node))
return false;
$type = $this->expand($type);
$typesFromNode = $node->getType();
if (!isset($typesFromNode))
return false;
if (is_array($typesFromNode)) {
foreach ($typesFromNode as $t)
if (is_a($t, 'ML\JsonLD\Node')
&& $t->getId() == $type)
return true;
} else
if (is_a($typesFromNode, 'ML\JsonLD\Node')
&& $typesFromNode->getId() == $type)
return true;
else
return false;
return false;
}
private function getFirstValue(Node $node = null, $property, $default = null) {
if (!isset($node))
return $default;
$valueFromNode = $node->getProperty($this->expand($property));
if (!isset($valueFromNode))
return $default;
if (is_array($valueFromNode))
return $valueFromNode[0];
return $valueFromNode;
}
/**
* Reads a property from a node and converts it into a string.
* If the property has multiple values only the first is returned.
* If no value is found or the node is null, the default is returned.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @param $default The default that is returned if no value for the property exists on the node.
* @return string|null
*/
public function getFirstValueString(Node $node = null, $property, $default = null) {
$valueFromNode = $this->getFirstValue($node, $property, $default);
if ($valueFromNode == $default)
return $default;
if (is_a($valueFromNode, 'ML\JsonLD\Node'))
return $valueFromNode->getId();
else
return $valueFromNode->getValue();
}
/**
* Reads a property from a node and converts it into a boolean.
* If the property has multiple values only the first is returned.
* If no value is found or the node is null, the default is returned.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @param $default The default that is returned if no value for the property exists on the node.
* @return bool|null
*/
public function getFirstValueBool(Node $node = null, $property, $default = null) {
return (in_array(
$this->getFirstValueString($node, $property, $default),
[ '1', 'true' ]
));
}
/**
* Reads a property from a node and converts it into an integer.
* If the property has multiple values only the first is returned.
* If no value is found or the node is null, the default is returned.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @param $default The default that is returned if no value for the property exists on the node.
* @return int|null
*/
public function getFirstValueInt(Node $node = null, $property, $default = null) {
$value = $this->getFirstValueString($node, $property);
if (is_numeric($value))
return (int)($value);
return $default;
}
/**
* Reads a property from a node and converts it into an float.
* If the property has multiple values only the first is returned.
* If no value is found or the node is null, the default is returned.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @param $default The default that is returned if no value for the property exists on the node.
* @return float|null
*/
public function getFirstValueFloat(Node $node = null, $property, $default = null) {
$value = $this->getFirstValueString($node, $property);
if (is_numeric($value))
return (float)($value);
return $default;
}
/**
* Reads a property from a node and converts it into a IRI.
* If the property has multiple values only the first is returned.
* If no value is found, value is a literal or the node is null, the default is returned.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @param $default The default that is returned if no value for the property exists on the node.
* @return string|null
*/
public function getFirstValueIRI(Node $node = null, $property, IRI $default = null) {
$valueFromNode = $this->getFirstValue($node, $property, $default);
if ($valueFromNode == $default)
return $default;
if (is_a($valueFromNode, 'ML\JsonLD\Node'))
return new IRI($valueFromNode->getId());
else
return $default;
}
/**
* Reads a property from a node and returns it as a Node.
* If the property has multiple values only the first is returned.
* If no value is found, value is a literal or the node is null, the default is returned.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @param $default The default that is returned if no value for the property exists on the node.
* @return string|null
*/
public function getFirstValueNode(Node $node = null, $property, Node $default = null) {
$valueFromNode = $this->getFirstValue($node, $property, $default);
if ($valueFromNode == $default)
return $default;
if (is_a($valueFromNode, 'ML\JsonLD\Node'))
return $valueFromNode;
else
return $default;
}
/**
* Checks whether a node has a specific value for a property.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @param string|object $value The expected value.
* @return boolean
*/
public function hasPropertyValue(Node $node = null, $property, $value) {
if (!isset($node))
return false;
$valuesFromNode = $node->getProperty($this->expand($property));
if (!isset($valuesFromNode))
return false;
if (!is_array($valuesFromNode))
$valuesFromNode = array($valuesFromNode);
foreach ($valuesFromNode as $v) {
if (is_a($v, 'ML\JsonLD\Node')) {
if ($v->getId() == $this->expand($value))
return true;
} else {
if ($v->getValue() == $value)
return true;
}
}
return false;
}
/**
* Checks whether the node has at least one value for a property.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @return boolean
*/
public function hasProperty(Node $node = null, $property) {
if (!isset($node))
return false;
return ($node->getProperty($this->expand($property)) != null);
}
private function getAllValues(Node $node = null, $property) {
if (!isset($node))
return [];
$valueFromNode = $node->getProperty($this->expand($property));
if (!isset($valueFromNode))
return [];
if (!is_array($valueFromNode))
$valueFromNode = [$valueFromNode];
return $valueFromNode;
}
/**
* Get the language-tagged-string for the property in the specified language.
* If no value is found for the specified language, the default is returned.
*/
public function getLocalizedString(Node $node = null, $property, $language, $default = null) {
$values = $this->getAllValues($node, $property);
foreach ($values as $v) {
if (is_a($v, 'ML\JsonLD\LanguageTaggedString') && $v->getLanguage() == $language)
return $v->getValue();
}
return $default;
}
/**
* Reads all values from a node and returns them as a string array.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @return array<string>
*/
public function getAllValuesString(Node $node = null, $property) {
$allValues = $this->getAllValues($node, $property);
$output = [];
foreach ($allValues as $a)
if (is_a($a, 'ML\JsonLD\Node'))
$output[] = $a->getId();
else
$output[] = $a->getValue();
return $output;
}
/**
* Reads all values from a node and returns them as a boolean array.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @return array<bool>
*/
public function getAllValuesBool(Node $node = null, $property) {
$allValues = $this->getAllValuesString($node, $property);
$output = [];
foreach ($allValues as $a)
$output = in_array($a, [ '1', 'true' ]);
return $output;
}
/**
* Reads all values from a node and returns them as an integer array.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @return array<bool>
*/
public function getAllValuesInt(Node $node = null, $property) {
$allValues = $this->getAllValuesString($node, $property);
$output = [];
foreach ($allValues as $a)
$output = (int)$a;
return $output;
}
/**
* Reads all values from a node and returns them as a float array.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @return array<bool>
*/
public function getAllValuesFloat(Node $node = null, $property) {
$allValues = $this->getAllValuesString($node, $property);
$output = [];
foreach ($allValues as $a)
$output = (float)$a;
return $output;
}
/**
* Reads all values from a node and returns them as a IRI array.
* Only converts the Node IDs of nodes into IRI, literal values are skipped.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @return array<IRI>
*/
public function getAllValuesIRI(Node $node = null, $property) {
$allValues = $this->getAllValues($node, $property);
$output = [];
foreach ($allValues as $a)
if (is_a($a, 'ML\JsonLD\Node'))
$output[] = new IRI($a->getId());
return $output;
}
/**
* Reads all values from a node and returns them as a Node array.
* Returns only nodes, literal values are skipped.
*
* @param Node $node The node to work on.
* @param string|object $property The property to read.
* @return array<Node>
*/
public function getAllValuesNode(Node $node = null, $property) {
$allValues = $this->getAllValues($node, $property);
$output = [];
foreach ($allValues as $a)
if (is_a($a, 'ML\JsonLD\Node'))
$output[] = $a;
return $output;
}
}

View File

@@ -0,0 +1,464 @@
<?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;
use Exception;
use ML\IRI\IRI, ML\JsonLD\JsonLD;
use Doctrine\Common\Cache\RedisCache;
use Psr\Log\LoggerInterface, Psr\Log\LoggerAwareTrait;
use GuzzleHttp\ClientInterface, GuzzleHttp\Client, GuzzleHttp\HandlerStack;
use GuzzleHttp\Exception\RequestException;
use Kevinrob\GuzzleCache\CacheMiddleware, Kevinrob\GuzzleCache\Storage\DoctrineCacheStorage;
use Kevinrob\GuzzleCache\Strategy\PrivateCacheStrategy;
use CloudObjects\SDK\Exceptions\CoreAPIException;
use CloudObjects\SDK\AccountGateway\AccountContext;
/**
* The ObjectRetriever provides access to objects on CloudObjects.
*/
class ObjectRetriever {
use LoggerAwareTrait;
private $client;
private $prefix;
private $options;
private $cache;
private $objects;
const CO_API_URL = 'https://api.cloudobjects.net/';
const REVISION_PROPERTY = 'coid://cloudobjects.io/isAtRevision';
public function __construct($options = []) {
// Merge options with defaults
$this->options = array_merge([
'cache_provider' => 'none',
'cache_prefix' => 'clobj:',
'cache_ttl' => 60,
'static_config_path' => null,
'auth_ns' => null,
'auth_secret' => null,
'api_base_url' => null,
'logger' => null,
'timeout' => 20,
'connect_timeout' => 5
], $options);
// Set up object cache
switch ($this->options['cache_provider']) {
case 'none':
// no caching
$this->cache = null;
break;
case 'redis':
// caching with Redis
$redis = new \Redis();
$redis->pconnect(
isset($this->options['cache_provider.redis.host']) ? $this->options['cache_provider.redis.host'] : '127.0.0.1',
isset($this->options['cache_provider.redis.port']) ? $this->options['cache_provider.redis.port'] : 6379);
$this->cache = new RedisCache();
$this->cache->setRedis($redis);
break;
case 'file':
// caching on the filesystem
$this->cache = new \Doctrine\Common\Cache\FilesystemCache(
isset($this->options['cache_provider.file.directory']) ? $this->options['cache_provider.file.directory'] : sys_get_temp_dir()
);
break;
default:
throw new Exception('Valid values for cache_provider are: none, redis, file');
}
// Set up logger
if (is_a($this->options['logger'], LoggerInterface::class))
$this->setLogger($this->options['logger']);
// Set up handler stack
$stack = HandlerStack::create();
// Add HTTP cache if specified
if (isset($this->cache)) {
$stack->push(
new CacheMiddleware(
new PrivateCacheStrategy(
new DoctrineCacheStorage($this->cache)
)
)
);
}
// Initialize client
$options = [
'base_uri' => isset($options['api_base_url']) ? $options['api_base_url'] : self::CO_API_URL,
'handler' => $stack,
'connect_timeout' => $this->options['connect_timeout'],
'timeout' => $this->options['timeout']
];
if (isset($this->options['auth_ns']) && isset($this->options['auth_secret']))
$options['auth'] = [$this->options['auth_ns'], $this->options['auth_secret']];
$this->client = new Client($options);
}
private function logInfoWithTime($message, $ts) {
if (isset($this->logger))
$this->logger->info($message, [ 'elapsed_ms' => round((microtime(true) - $ts) * 1000) ]);
}
private function getFromCache($id) {
return (isset($this->cache) && $this->cache->contains($this->options['cache_prefix'].$id))
? $this->cache->fetch($this->options['cache_prefix'].$id) : null;
}
private function putIntoCache($id, $data, $ttl) {
if (isset($this->cache))
$this->cache->save($this->options['cache_prefix'].$id, $data, $ttl);
}
/**
* Get the HTTP client that is used to access the API.
*/
public function getClient() {
return $this->client;
}
/**
* Set the HTTP client that is used to access the API.
*
* @param ClientInterface $client The HTTP client.
* @param string $prefix An optional prefix (e.g. an AccountGateway mountpoint)
*/
public function setClient(ClientInterface $client, $prefix = null) {
$this->client = $client;
$this->prefix = $prefix;
}
/**
* Creates a clone of this object retriever that uses the given account
* context to access the API as a developer API. Cache settings are inherited
* but the prefix is extended to keep cache content specific to account.
*
* @param AccountContext $accountContext
* @param string $mountpointName The name for the API mountpoint.
*/
public function withAccountContext(AccountContext $accountContext, string $mountpointName) {
$newRetriever = new self($this->options);
$newRetriever->options['cache_prefix'] .= (string)$accountContext->getAAUID();
$newRetriever->client = $accountContext->getClient();
$newRetriever->prefix = '/'.$mountpointName.'/';
return $newRetriever;
}
/**
* Get an object description from CloudObjects. Attempts to get object
* from in-memory cache first, stored static configurations next,
* configured external cache third, and finally calls the Object API
* on CloudObjects Core. Returns null if the object was not found.
*
* @param IRI $coid COID of the object
* @return Node|null
*/
public function getObject(IRI $coid) {
if (!COIDParser::isValidCOID($coid))
throw new Exception("Not a valid COID.");
$uriString = (string)$coid;
if (isset($this->objects[$uriString]))
// Return from in-memory cache if it exists
return $this->objects[$uriString];
$ts = microtime(true);
if (isset($this->options['static_config_path'])) {
$location = realpath($this->options['static_config_path'].DIRECTORY_SEPARATOR.
$coid->getHost().str_replace('/', DIRECTORY_SEPARATOR, $coid->getPath())
.DIRECTORY_SEPARATOR.'object.jsonld');
if ($location && file_exists($location)) {
$object = $location;
$this->logInfoWithTime('Fetched <'.$uriString.'> from static configuration.', $ts);
}
}
if (!isset($object)) {
$object = $this->getFromCache($uriString);
if (isset($object))
$this->logInfoWithTime('Fetched <'.$uriString.'> from object cache.', $ts);
}
if (!isset($object)) {
try {
$response = $this->client
->get((isset($this->prefix) ? $this->prefix : '').$coid->getHost().$coid->getPath().'/object',
['headers' => ['Accept' => 'application/ld+json']]);
$object = (string)$response->getBody();
$this->putIntoCache($uriString, $object, $this->options['cache_ttl']);
$this->logInfoWithTime('Fetched <'.$uriString.'> from Core API ['.$response->getStatusCode().'].', $ts);
} catch (RequestException $e) {
if ($e->hasResponse())
$this->logInfoWithTime('Object <'.$uriString.'> not found in Core API ['.$e->getResponse()->getStatusCode().'].', $ts);
else
$this->logInfoWithTime('Object <'.$uriString.'> could not be retrieved from Core API.', $ts);
return null;
}
}
$document = JsonLD::getDocument($object);
$this->objects[$uriString] = $document->getGraph()->getNode($uriString);
return $this->objects[$uriString];
}
/**
* Fetch all object descriptions for objects in a specific namespace
* and with a certain type from CloudObjects. Adds individual objects
* to cache and returns a list of COIDs (as IRI) for them. The list
* itself is not cached, which means that every call of this function
* goes to the Object API.
*
* @param IRI $namespaceCoid COID of the namespace
* @param $type RDF type that objects should have
* @return array<IRI>
*/
public function fetchObjectsInNamespaceWithType(IRI $namespaceCoid, $type) {
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
throw new Exception("Not a valid namespace COID.");
$ts = microtime(true);
$type = (string)$type;
try {
$response = $this->client
->get((isset($this->prefix) ? $this->prefix : '').$namespaceCoid->getHost().'/all',
[
'headers' => [ 'Accept' => 'application/ld+json' ],
'query' => [ 'type' => $type ]
]);
$document = JsonLD::getDocument((string)$response->getBody());
$allObjects = $document->getGraph()->getNodesByType($type);
$allIris = [];
foreach ($allObjects as $object) {
$iri = new IRI($object->getId());
if (!COIDParser::isValidCOID($iri)) continue;
if ($iri->getHost() != $namespaceCoid->getHost()) continue;
$this->objects[$object->getId()] = $object;
$this->putIntoCache($object->getId(), $object, $this->options['cache_ttl']);
$allIris[] = $iri;
}
$this->logInfoWithTime('Fetched all objects with <'.$type.'> for <'.$namespaceCoid->getHost().'> from Core API ['.$response->getStatusCode().'].', $ts);
} catch (Exception $e) {
throw new CoreAPIException;
}
return $allIris;
}
/**
* Fetch all object descriptions for objects in a specific namespace
* from CloudObjects. Adds individual objects to cache and returns a
* list of COIDs (as IRI) for them. The list itself is not cached,
* which means that every call of this function goes to the Object API.
*
* @param IRI $namespaceCoid COID of the namespace
* @return array<IRI>
*/
public function fetchAllObjectsInNamespace(IRI $namespaceCoid) {
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
throw new Exception("Not a valid namespace COID.");
$ts = microtime(true);
try {
$response = $this->client
->get((isset($this->prefix) ? $this->prefix : '').$namespaceCoid->getHost().'/all',
[ 'headers' => [ 'Accept' => 'application/ld+json' ] ]);
$document = JsonLD::getDocument((string)$response->getBody());
$allObjects = $document->getGraph()->getNodes();
$allIris = [];
foreach ($allObjects as $object) {
$iri = new IRI($object->getId());
if (!COIDParser::isValidCOID($iri)) continue;
if ($iri->getHost() != $namespaceCoid->getHost()) continue;
$this->objects[$object->getId()] = $object;
$this->putIntoCache($object->getId(), $object, $this->options['cache_ttl']);
$allIris[] = $iri;
}
$this->logInfoWithTime('Fetched all objects for <'.$namespaceCoid->getHost().'> from Core API ['.$response->getStatusCode().'].', $ts);
} catch (Exception $e) {
throw new CoreAPIException;
}
return $allIris;
}
/**
* Fetch a list of COIDs for all objects in a specific namespace
* from CloudObjects, but not the objects itself. The list is not cached,
* which means that every call of this function goes to the Object API.
*
* @param IRI $namespaceCoid COID of the namespace
* @return array<IRI>
*/
public function getCOIDListForNamespace(IRI $namespaceCoid) {
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
throw new Exception("Not a valid namespace COID.");
$ts = microtime(true);
try {
$response = $this->client
->get((isset($this->prefix) ? $this->prefix : '').$namespaceCoid->getHost().'/coids',
[ 'headers' => [ 'Accept' => 'application/ld+json' ] ]);
$document = JsonLD::getDocument((string)$response->getBody());
$containerNode = $document->getGraph()->getNode('co-namespace-members://'.$namespaceCoid->getHost());
$reader = new NodeReader([ 'prefixes' => [ 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#' ]]);
$allIris = $reader->getAllValuesIRI($containerNode, 'rdfs:member');
$this->logInfoWithTime('Fetched object list for <'.$namespaceCoid->getHost().'> from Core API ['.$response->getStatusCode().'].', $ts);
} catch (Exception $e) {
throw new CoreAPIException;
}
return $allIris;
}
/**
* Fetch a list of COIDs for all objects in a specific namespace
* from CloudObjects, but not the objects itself. The list is not cached,
* which means that every call of this function goes to the Object API.
*
* @param IRI $namespaceCoid COID of the namespace
* @param $type RDF type that objects should have
* @return array<IRI>
*/
public function getCOIDListForNamespaceWithType(IRI $namespaceCoid, $type) {
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
throw new Exception("Not a valid namespace COID.");
$ts = microtime(true);
$type = (string)$type;
try {
$response = $this->client
->get((isset($this->prefix) ? $this->prefix : '').$namespaceCoid->getHost().'/coids',
[
'headers' => [ 'Accept' => 'application/ld+json' ],
'query' => [ 'type' => $type ]
]);
$document = JsonLD::getDocument((string)$response->getBody());
$containerNode = $document->getGraph()->getNode('co-namespace-members://'.$namespaceCoid->getHost());
$reader = new NodeReader([ 'prefixes' => [ 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#' ]]);
$allIris = $reader->getAllValuesIRI($containerNode, 'rdfs:member');
$this->logInfoWithTime('Fetched object list with <'.$type.'> for <'.$namespaceCoid->getHost().'> from Core API ['.$response->getStatusCode().'].', $ts);
} catch (Exception $e) {
throw new CoreAPIException;
}
return $allIris;
}
/**
* Get an object description from CloudObjects. Shorthand method for
* "getObject" which allows passing the COID as string instead of IRI.
*
* @param any $coid
* @return Node|null
*/
public function get($coid) {
if (is_string($coid))
return $this->getObject(new IRI($coid));
if (is_object($coid) && get_class($coid)=='ML\IRI\IRI')
return $this->getObject($coid);
throw new Exception('COID must be passed as a string or an IRI object.');
}
/**
* Get a object's attachment.
*
* @param IRI $coid
* @param string $filename
*/
public function getAttachment(IRI $coid, $filename) {
$object = $this->getObject($coid);
if (!$object)
// Cannot get attachment for non-existing object
return null;
$ts = microtime(true);
$cacheId = $object->getId().'#'.$filename;
$fileData = $this->getFromCache($cacheId);
// Parse cached data into revision and content
if (isset($fileData)) {
$this->logInfoWithTime('Fetched attachment <'.$filename.'> for <'.$object->getId().'> from object cache.', $ts);
list($fileRevision, $fileContent) = explode('#', $fileData, 2);
}
if (!isset($fileData)
|| $fileRevision!=$object->getProperty(self::REVISION_PROPERTY)->getValue()) {
// Does not exist in cache or is outdated, fetch from CloudObjects
try {
$response = $this->client->get((isset($this->prefix) ? $this->prefix : '').$coid->getHost().$coid->getPath()
.'/'.basename($filename));
$fileContent = $response->getBody()->getContents();
$fileData = $object->getProperty(self::REVISION_PROPERTY)->getValue().'#'.$fileContent;
$this->putIntoCache($cacheId, $fileData, 0);
$this->logInfoWithTime('Fetched attachment <'.$filename.'> for <'.$object->getId().'> from Core API ['.$response->getStatusCode().'].', $ts);
} catch (Exception $e) {
$this->logInfoWithTime('Attachment <'.$filename.'> for <'.$object->getId().'> not found in Core API ['.$e->getResponse()->getStatusCode().'].', $ts);
// ignore exception - treat as non-existing file
}
}
return $fileContent;
}
/**
* Retrieve the object that describes the namespace provided with the "auth_ns"
* configuration option.
*
* @return Node
*/
public function getAuthenticatingNamespaceObject() {
if (!isset($this->options['auth_ns']))
throw new Exception("Missing 'auth_ns' configuration option.");
$namespaceCoid = COIDParser::fromString($this->options['auth_ns']);
if (COIDParser::getType($namespaceCoid) != COIDParser::COID_ROOT)
throw new Exception("The 'auth_ns' configuration option is not a valid namespace/root COID.");
return $this->getObject($namespaceCoid);
}
}

View File

@@ -0,0 +1,224 @@
<?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->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'wa:APIKeyAuthentication'))
$clientConfig = $this->configureAPIKeyAuthentication($api, $clientConfig);
elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'oauth2:FixedBearerTokenAuthentication'))
$clientConfig = $this->configureBearerTokenAuthentication($api, $clientConfig);
elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'wa:HTTPBasicAuthentication'))
$clientConfig = $this->configureBasicAuthentication($api, $clientConfig);
elseif ($this->reader->hasPropertyValue($api, 'wa:supportsAuthenticationMechanism',
'wa:SharedSecretAuthenticationViaHTTPBasic'))
$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];
}
}