Imported code from old repository
This commit is contained in:
79
CloudObjects/SDK/AccountGateway/AAUIDParser.php
Normal file
79
CloudObjects/SDK/AccountGateway/AAUIDParser.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
369
CloudObjects/SDK/AccountGateway/AccountContext.php
Normal file
369
CloudObjects/SDK/AccountGateway/AccountContext.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
81
CloudObjects/SDK/AccountGateway/DataLoader.php
Normal file
81
CloudObjects/SDK/AccountGateway/DataLoader.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
152
CloudObjects/SDK/COIDParser.php
Normal file
152
CloudObjects/SDK/COIDParser.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
72
CloudObjects/SDK/Common/CryptoHelper.php
Normal file
72
CloudObjects/SDK/Common/CryptoHelper.php
Normal 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/'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
14
CloudObjects/SDK/Exceptions/CoreAPIException.php
Normal file
14
CloudObjects/SDK/Exceptions/CoreAPIException.php
Normal 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 {
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
85
CloudObjects/SDK/Helpers/SDKLoader.php
Normal file
85
CloudObjects/SDK/Helpers/SDKLoader.php
Normal 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];
|
||||
}
|
||||
|
||||
}
|
||||
110
CloudObjects/SDK/Helpers/SharedSecretAuthentication.php
Normal file
110
CloudObjects/SDK/Helpers/SharedSecretAuthentication.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
76
CloudObjects/SDK/JSON/SchemaValidator.php
Normal file
76
CloudObjects/SDK/JSON/SchemaValidator.php
Normal 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);
|
||||
}
|
||||
}
|
||||
364
CloudObjects/SDK/NodeReader.php
Normal file
364
CloudObjects/SDK/NodeReader.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
||||
464
CloudObjects/SDK/ObjectRetriever.php
Normal file
464
CloudObjects/SDK/ObjectRetriever.php
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
224
CloudObjects/SDK/WebAPI/APIClientFactory.php
Normal file
224
CloudObjects/SDK/WebAPI/APIClientFactory.php
Normal 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];
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user