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

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
vendor
build
cache
*.phar

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

314
LICENSE
View File

@@ -1,104 +1,110 @@
Mozilla Public License Version 2.0
==================================
Mozilla Public License, version 2.0
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
means each individual or legal entity that creates, contributes to the
creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
means the combination of the Contributions of others (if any) used by a
Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
means Source Code Form to which the initial Contributor has attached the
notice in Exhibit A, the Executable Form of such Source Code Form, and
Modifications of such Source Code Form, in each case including portions
thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
a. that the initial Contributor has attached the notice described in
Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
b. that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the terms of
a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
means a work that combines Covered Software with other material, in a
separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
means having the right to grant, to the maximum extent possible, whether
at the time of the initial grant or subsequently, any and all of the
rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
a. any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
b. any new file in Source Code Form that contains any Covered Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
Contributor that would be infringed, but for the grant of the License,
by the making, using, selling, offering for sale, having made, import,
or transfer of either its Contributions or its Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
means either the GNU General Public License, Version 2.0, the GNU Lesser
General Public License, Version 2.1, the GNU Affero General Public
License, Version 3.0, or any later versions of those licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
License. For legal entities, "You" includes any entity that controls, is
controlled by, or is under common control with You. For purposes of this
definition, "control" means (a) the power, direct or indirect, to cause
the direction or management of such entity, whether by contract or
otherwise, or (b) ownership of more than fifty percent (50%) of the
outstanding shares or beneficial ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
a. under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
b. under Patent Claims of such Contributor to make, use, sell, offer for
sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
@@ -115,15 +121,14 @@ distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
a. for any code that a Contributor has removed from Covered Software; or
(b) for infringements caused by: (i) Your and any other third party's
b. for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
c. under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
@@ -140,8 +145,8 @@ permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
Contributions are its original creation(s) or it has sufficient rights to
grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
@@ -151,11 +156,11 @@ equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
@@ -171,16 +176,16 @@ Form.
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
a. such Covered Software must also be made available in Source Code Form,
as described in Section 3.1, and You must inform recipients of the
Executable Form how they can obtain a copy of such Source Code Form by
reasonable means in a timely manner, at a charge no more than the cost
of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
b. You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
license for the Executable Form does not attempt to limit or alter the
recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
@@ -198,10 +203,10 @@ License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
(including copyright notices, patent notices, disclaimers of warranty, or
limitations of liability) contained within the Source Code Form of the
Covered Software, except that You may alter any license notices to the
extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
@@ -217,34 +222,32 @@ disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
If it is impossible for You to comply with any of the terms of this License
with respect to some or all of the Covered Software due to statute,
judicial order, or regulation then You must: (a) comply with the terms of
this License to the maximum extent possible; and (b) describe the
limitations and the code they affect. Such description must be placed in a
text file included with all distributions of the Covered Software under
this License. Except to the extent prohibited by statute or regulation,
such description must be sufficiently detailed for a recipient of ordinary
skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.1. The rights granted under this License will terminate automatically if You
fail to comply with any of its terms. However, if You become compliant,
then the rights granted under this License from a particular Contributor
are reinstated (a) provisionally, unless and until such Contributor
explicitly and finally terminates Your grants, and (b) on an ongoing
basis, if such Contributor fails to notify You of the non-compliance by
some reasonable means prior to 60 days after You have come back into
compliance. Moreover, Your grants from a particular Contributor are
reinstated on an ongoing basis if such Contributor notifies You of the
non-compliance by some reasonable means, this is the first time You have
received notice of non-compliance with this License from such
Contributor, and You become compliant prior to 30 days after Your receipt
of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
@@ -253,75 +256,60 @@ directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user
license agreements (excluding distributors and resellers) which have been
validly granted by You or Your distributors under this License prior to
termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
6. Disclaimer of Warranty
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
Covered Software is provided under this License on an "as is" basis,
without warranty of any kind, either expressed, implied, or statutory,
including, without limitation, warranties that the Covered Software is free
of defects, merchantable, fit for a particular purpose or non-infringing.
The entire risk as to the quality and performance of the Covered Software
is with You. Should any Covered Software prove defective in any respect,
You (not any Contributor) assume the cost of any necessary servicing,
repair, or correction. This disclaimer of warranty constitutes an essential
part of this License. No use of any Covered Software is authorized under
this License except under this disclaimer.
7. Limitation of Liability
Under no circumstances and under no legal theory, whether tort (including
negligence), contract, or otherwise, shall any Contributor, or anyone who
distributes Covered Software as permitted above, be liable to You for any
direct, indirect, special, incidental, or consequential damages of any
character including, without limitation, damages for lost profits, loss of
goodwill, work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses, even if such party shall have been
informed of the possibility of such damages. This limitation of liability
shall not apply to liability for death or personal injury resulting from
such party's negligence to the extent applicable law prohibits such
limitation. Some jurisdictions do not allow the exclusion or limitation of
incidental or consequential damages, so this exclusion and limitation may
not apply to You.
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
Any litigation relating to this License may be brought only in the courts
of a jurisdiction where the defendant maintains its principal place of
business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions. Nothing
in this Section shall prevent a party's ability to bring cross-claims or
counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
necessary to make it enforceable. Any law or regulation which provides that
the language of a contract shall be construed against the drafter shall not
be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
@@ -346,28 +334,30 @@ any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Licenses If You choose to distribute Source Code Form that is
Incompatible With Secondary Licenses under the terms of this version of
the License, the notice described in Exhibit B of this License must be
attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
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 https://mozilla.org/MPL/2.0/.
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/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
If it is not possible or desirable to put the notice in a particular file,
then You may include the notice in a location (such as a LICENSE file in a
relevant directory) where a recipient would be likely to look for such a
notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.
This Source Code Form is "Incompatible
With Secondary Licenses", as defined by
the Mozilla Public License, v. 2.0.

View File

@@ -1,2 +1,66 @@
# CloudObjects-PHP-SDK
# CloudObjects PHP SDK
[![Latest Stable Version](https://poser.pugx.org/cloudobjects/sdk/v/stable)](https://packagist.org/packages/cloudobjects/sdk) [![Total Downloads](https://poser.pugx.org/cloudobjects/sdk/downloads)](https://packagist.org/packages/cloudobjects/sdk)
The CloudObjects PHP SDK provides simple access to [CloudObjects](https://cloudobjects.io/) from PHP-based applications. It wraps the [Object API](https://coid.link/cloudobjects.io/ObjectAPI/1.0) to fetch objects from the CloudObjects Core database and provides object-based access to their RDF description. A two-tiered caching mechanism (in-memory and Doctrine cache drivers) is included. The SDK also contains a helper class to validate COIDs.
## Installation
The SDK is [distributed through packagist](https://packagist.org/packages/cloudobjects/sdk). Add `cloudobjects/sdk` to the `require` section of your `composer.json`, like this:
````json
{
"require": {
"cloudobjects/sdk" : ">=0.7"
}
}
````
## Retrieving Objects
In order to retrieve objects from the CloudObjects Core database you need to create an instance of `CloudObjects\SDK\ObjectRetriever`. Then you can call `getObject()`. This method returns an `ML\JsonLD\Node` instance or `null` if the object is not found. You can use the object interface of the [JsonLD library](https://github.com/lanthaler/JsonLD/) to read the information from the object.
Here's a simple example:
````php
use ML\IRI\IRI;
use CloudObjects\SDK\ObjectRetriever;
/* ... */
$retriever = new ObjectRetriever();
$object = $this->retriever->getObject(new IRI('coid://cloudobjects.io'));
if (isset($object))
echo $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue();
else
echo "Object not found.";
````
### Configuration
You can pass an array of configuration options to the ObjectRetriever's constructor:
| Option | Description | Default |
|---|---|---|
| `cache_provider` | The type of cache used. Currently supports `redis`, `file` and `none`. | `none` |
| `cache_prefix` | A prefix used for cache IDs. Normally this should not be set but might be necessary on shared caches. | `clobj:` |
| `cache_ttl` | Determines how long objects can remain cached. | `60` |
| `auth_ns` | The namespace of the service that this retriever acts for. If not set the API is accessed anonymously. | `null` |
| `auth_secret` | The shared secret between the namespace in `auth_ns` and `cloudobjects.io` for authenticated. If not set the API is accessed anonymously. | `null` |
#### For `redis` cache:
| Option | Description | Default |
|---|---|---|
| `cache_provider.redis.host` | The hostname or IP of the Redis instance. | `127.0.0.1` |
| `cache_provider.redis.port` | The port number of the Redis instance. | `6379` |
#### For `file` cache:
| Option | Description | Default |
|---|---|---|
| `cache_provider.file.directory` | The directory to store cache data in. | The system's temporary directory. |
## License
The PHP SDK is licensed under Mozilla Public License (see LICENSE file).

39
composer.json Normal file
View File

@@ -0,0 +1,39 @@
{
"name": "cloudobjects/sdk",
"description": "CloudObjects SDK for PHP for working with COIDs and object descriptions from CloudObjects.",
"keywords": ["cloudobjects", "sdk"],
"homepage": "https://github.com/CloudObjects/CloudObjects-PHP-SDK",
"license": "MPL-2.0",
"require" : {
"ml/json-ld": ">=1.0.7",
"doctrine/common" : ">=2.6.1",
"doctrine/cache" : "1.*",
"guzzlehttp/guzzle" : ">=6.0",
"psr/log": "^1.1",
"kevinrob/guzzle-cache-middleware": "^3.2",
"webmozart/assert": "^1.6"
},
"authors": [
{
"name": "Lukas Rosenstock"
}
],
"autoload": {
"psr-0": {
"CloudObjects\\SDK" : ""
}
},
"require-dev" : {
"phpunit/phpunit": ">=4.8.0,<5.0",
"symfony/http-foundation" : ">=4.0",
"symfony/psr-http-message-bridge" : ">=1.1.0",
"zendframework/zend-diactoros" : "~1.8.6",
"defuse/php-encryption" : "^2.2"
},
"suggest" : {
"symfony/http-foundation" : "Required to use parseSymfonyRequest() in AccountContext.",
"symfony/psr-http-message-bridge" : "Required to use parseSymfonyRequest() in AccountContext.",
"zendframework/zend-diactoros" : "Required to use parseSymfonyRequest() in AccountContext.",
"defuse/php-encryption": "Required to use CryptoHelper"
}
}

3206
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

23
makefile Normal file
View File

@@ -0,0 +1,23 @@
.PHONY: all
all: vendor build
composer.lock: composer.json
# Updating Dependencies with Composer
composer update -o
vendor: composer.lock
# Installing Dependencies with Composer
composer install -o
sami.phar:
# Get a copy of sami (only on PHP 7)
@if [ `php -v | awk '{ if ($$1 == "PHP") { print substr($$2,0,1) }}'` = "7" ]; then\
wget http://get.sensiolabs.org/sami.phar;\
fi
build: sami.phar
# Building documentation with sami.phar (only on PHP 7)
@if [ `php -v | awk '{ if ($$1 == "PHP") { print substr($$2,0,1) }}'` = "7" ]; then\
php sami.phar update sami-config.php --force;\
fi

20
phpunit.xml Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="./tests/bootstrap.php">
<testsuites>
<testsuite name="OfflineTests">
<directory>./tests/OfflineTests/</directory>
</testsuite>
<testsuite name="OnlineTests">
<directory>./tests/OnlineTests/</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,63 @@
<?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 AAUIDParserTest extends \PHPUnit_Framework_TestCase {
public function testValidAccountAAUID() {
$aauid = new IRI('aauid:abcd1234abcd1234');
$this->assertEquals(AAUIDParser::AAUID_ACCOUNT, AAUIDParser::getType($aauid));
$this->assertEquals('abcd1234abcd1234', AAUIDParser::getAAUID($aauid));
}
public function testInvalidAccountAAUID() {
$aauid = new IRI('aauid:abcd1234abcd123');
$this->assertEquals(AAUIDParser::AAUID_INVALID, AAUIDParser::getType($aauid));
$this->assertNull(AAUIDParser::getAAUID($aauid));
}
public function testValidAccountConnectionAAUID() {
$aauid = new IRI('aauid:abcd1234abcd1234:connection:AA');
$this->assertEquals(AAUIDParser::AAUID_CONNECTION, AAUIDParser::getType($aauid));
$this->assertEquals('abcd1234abcd1234', AAUIDParser::getAAUID($aauid));
$this->assertEquals('AA', AAUIDParser::getQualifier($aauid));
}
public function testInvalidAccountConnectionAAUID() {
$aauid = new IRI('aauid:abcd1234abcd1234:connection:AAA');
$this->assertEquals(AAUIDParser::AAUID_INVALID, AAUIDParser::getType($aauid));
$this->assertNull(AAUIDParser::getAAUID($aauid));
$this->assertNull(AAUIDParser::getQualifier($aauid));
}
public function testValidConnectedAccountAAUID() {
$aauid = new IRI('aauid:abcd1234abcd1234:account:AA');
$this->assertEquals(AAUIDParser::AAUID_CONNECTED_ACCOUNT, AAUIDParser::getType($aauid));
$this->assertEquals('abcd1234abcd1234', AAUIDParser::getAAUID($aauid));
$this->assertEquals('AA', AAUIDParser::getQualifier($aauid));
}
public function testInvalidConnectedAccountAAUID() {
$aauid = new IRI('aauid:abcd1234abcd1234:account:X9');
$this->assertEquals(AAUIDParser::AAUID_INVALID, AAUIDParser::getType($aauid));
$this->assertNull(AAUIDParser::getAAUID($aauid));
$this->assertNull(AAUIDParser::getQualifier($aauid));
}
public function testFromStringValid() {
$aauid1 = new IRI('aauid:5678defg8765gfed');
$aauid2 = AAUIDParser::fromString('aauid:5678defg8765gfed');
$this->assertEquals($aauid1, $aauid2);
$aauid1 = new IRI('aauid:5678defg8765gfed');
$aauid2 = AAUIDParser::fromString('5678defg8765gfed');
$this->assertEquals($aauid1, $aauid2);
}
}

View File

@@ -0,0 +1,34 @@
<?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 GuzzleHttp\Psr7\Request as GuzzlePsrRequest;
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
class AccountContextParseTest extends \PHPUnit_Framework_TestCase {
public function testParsePsrRequest() {
$request = new GuzzlePsrRequest('GET', '/', [
'C-AAUID' => '1234123412341234', 'C-Access-Token' => 'test'
]);
$context = AccountContext::fromPsrRequest($request);
$this->assertNotNull($context);
$this->assertEquals('1234123412341234', AAUIDParser::getAAUID($context->getAAUID()));
}
public function testParseSymfonyRequest() {
$request = SymfonyRequest::create('/', 'GET', [], [], [], [
'HTTP_C_AAUID' => '1234123412341234', 'HTTP_C_ACCESS_TOKEN' => 'test'
]);
$context = AccountContext::fromSymfonyRequest($request);
$this->assertNotNull($context);
$this->assertEquals('1234123412341234', AAUIDParser::getAAUID($context->getAAUID()));
}
}

View File

@@ -0,0 +1,33 @@
<?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 AccountContextTest extends \PHPUnit_Framework_TestCase {
private $context;
protected function setUp() {
$this->context = new AccountContext(new IRI('aauid:aaaabbbbccccdddd'), 'DUMMY');
}
public function testDefaultGatewayBaseURL() {
$this->assertEquals('https://aaaabbbbccccdddd.aauid.net', $this->context->getClient()->getConfig('base_uri'));
}
public function testSetAccountGatewayBaseURLTemplateWithPlaceholder() {
$this->context->setAccountGatewayBaseURLTemplate('http://{aauid}.localhost');
$this->assertEquals('http://aaaabbbbccccdddd.localhost', $this->context->getClient()->getConfig('base_uri'));
}
public function testSetAccountGatewayBaseURLTemplateWithoutPlaceholder() {
$this->context->setAccountGatewayBaseURLTemplate('http://localhost');
$this->assertEquals('http://localhost', $this->context->getClient()->getConfig('base_uri'));
}
}

View File

@@ -0,0 +1,130 @@
<?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;
class COIDParserTest extends \PHPUnit_Framework_TestCase {
public function testRootCOID() {
$coid = new IRI('coid://example.com');
$this->assertEquals(COIDParser::COID_ROOT, COIDParser::getType($coid));
}
public function testInvalidRootCOID() {
$coid = new IRI('coid://example');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://exämple.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://ex&mple.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testInvalidCOID() {
$coid = new IRI('http://example.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('example.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('COID://example.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('Coid://example.com');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://EXAMPLE.COM');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://exAMPle.CoM');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testUnversionedCOID() {
$coid = new IRI('coid://example.com/Example');
$this->assertEquals(COIDParser::COID_UNVERSIONED, COIDParser::getType($coid));
}
public function testInvalidUnversionedCOID() {
$coid = new IRI('coid://example.com/Exümple');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Examp%e');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testVersionedCOID() {
$coid = new IRI('coid://example.com/Example/1.0');
$this->assertEquals(COIDParser::COID_VERSIONED, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/alpha');
$this->assertEquals(COIDParser::COID_VERSIONED, COIDParser::getType($coid));
}
public function testInvalidVersionedCOID() {
$coid = new IRI('coid://example.com/Example/1.$');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testVersionWildcardCOID() {
$coid = new IRI('coid://example.com/Example/^1.0');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/~1.0');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/1.*');
$this->assertEquals(COIDParser::COID_VERSION_WILDCARD, COIDParser::getType($coid));
}
public function testInvalidVersionWildcardCOID() {
$coid = new IRI('coid://example.com/Example/^1.*');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
$coid = new IRI('coid://example.com/Example/1.a.*');
$this->assertEquals(COIDParser::COID_INVALID, COIDParser::getType($coid));
}
public function testIRICaseSensitivity() {
$coid1 = new IRI('coid://example.com/example/1.0');
$coid2 = new IRI('coid://example.com/Example/1.0');
$this->assertFalse($coid1->equals($coid2));
}
public function testRootFromString() {
$coid1 = new IRI('coid://example.com');
$coid2 = COIDParser::fromString('coid://example.com');
$coid3 = COIDParser::fromString('example.com');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testUnversionedFromString() {
$coid1 = new IRI('coid://example.com/Example');
$coid2 = COIDParser::fromString('coid://example.com/Example');
$coid3 = COIDParser::fromString('example.com/Example');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testVersionedFromString() {
$coid1 = new IRI('coid://example.com/Example/1.0');
$coid2 = COIDParser::fromString('coid://example.com/Example/1.0');
$coid3 = COIDParser::fromString('example.com/Example/1.0');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testNormalizeRootFromString() {
$coid1 = new IRI('coid://example.com');
$coid2 = COIDParser::fromString('COID://example.com');
$coid3 = COIDParser::fromString('ExAmple.COM');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
}
public function testNormalizeNonRootFromString() {
$coid1 = new IRI('coid://example.com/Example');
$coid2 = COIDParser::fromString('COID://example.com/Example');
$coid3 = COIDParser::fromString('ExAmple.COM/Example');
$coid4 = COIDParser::fromString('ExAmple.COM/EXample');
$this->assertTrue($coid1->equals($coid2));
$this->assertTrue($coid1->equals($coid3));
$this->assertFalse($coid1->equals($coid4));
}
}

View File

@@ -0,0 +1,46 @@
<?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 InvalidArgumentException;
use GuzzleHttp\Client, GuzzleHttp\Handler\MockHandler,
GuzzleHttp\HandlerStack, GuzzleHttp\Psr7\Response;
use CloudObjects\SDK\ObjectRetriever;
class CryptoHelperTest extends \PHPUnit_Framework_TestCase {
private $retriever;
private $graph;
private function setMockResponse(Response $response) {
$mock = new MockHandler([$response]);
$handler = HandlerStack::create($mock);
$this->retriever->setClient(new Client(['handler' => $handler]));
}
public function setUp() {
$this->retriever = new ObjectRetriever([
'auth_ns' => 'test.cloudobjects.io',
'auth_secret' => 'TEST'
]);
}
public function testEncryptDecrypt() {
$this->setMockResponse(new Response(200,
[ 'Content-Type' => 'application/ld+json' ],
'{"@context":{"common":"coid:\/\/common.cloudobjects.io\/"},"@id":"coid:\/\/test.cloudobjects.io","common:usesSharedEncryptionKey": "def0000092c63296feb07f6b44f323351ab2e570fb04c2dff73c3119fd1103234ea03f5af094d33e8fb5122c5cf73f745957a5f8f47b4fc3c43bc86fb631969f4c591831"}'));
$cryptoHelper = new CryptoHelper($this->retriever);
$cleartext = "CLEARTEXT";
$ciphertext = $cryptoHelper->encryptWithSharedEncryptionKey($cleartext);
$encryptedDecryptedText = $cryptoHelper->decryptWithSharedEncryptionKey($ciphertext);
$this->assertEquals($cleartext, $encryptedDecryptedText);
}
}

View File

@@ -0,0 +1,175 @@
<?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 InvalidArgumentException;
use ML\JsonLD\JsonLD;
use CloudObjects\SDK\ObjectRetriever;
class SchemaValidatorTest extends \PHPUnit_Framework_TestCase {
private $schemaValidator;
private $graph;
public function setUp() {
$this->schemaValidator = new SchemaValidator(new ObjectRetriever);
$this->graph = JsonLD::getDocument('{}')->getGraph();
}
public function testString() {
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/String'));
$this->schemaValidator->validateAgainstNode("Test", $node);
}
public function testNotString() {
$this->setExpectedException(InvalidArgumentException::class);
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/String'));
$this->schemaValidator->validateAgainstNode(9, $node);
}
public function testNumber() {
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Number'));
$this->schemaValidator->validateAgainstNode(3.5, $node);
}
public function testNotNumber() {
$this->setExpectedException(InvalidArgumentException::class);
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Number'));
$this->schemaValidator->validateAgainstNode("ABC", $node);
}
public function testInteger() {
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Integer'));
$this->schemaValidator->validateAgainstNode(12, $node);
}
public function testNotInteger() {
$this->setExpectedException(InvalidArgumentException::class);
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Integer'));
$this->schemaValidator->validateAgainstNode(1.4, $node);
}
public function testArray() {
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Array'));
$this->schemaValidator->validateAgainstNode([ 1, 2, "foo" ], $node);
}
public function testNotArray() {
$this->setExpectedException(InvalidArgumentException::class);
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Array'));
$this->schemaValidator->validateAgainstNode("NANANA", $node);
}
public function testObject() {
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Object'));
$this->schemaValidator->validateAgainstNode([
'a' => 'A',
'b' => 'B'
], $node);
}
public function testNotObject() {
$this->setExpectedException(InvalidArgumentException::class);
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Object'));
$this->schemaValidator->validateAgainstNode(5, $node);
}
public function testObjectWithProperty() {
$stringNode = $this->graph->createNode();
$stringNode->setProperty('coid://json.co-n.net/hasKey', 'a');
$stringNode->setType($this->graph->createNode('coid://json.co-n.net/String'));
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Object'));
$node->setProperty('coid://json.co-n.net/hasProperty', $stringNode);
$this->schemaValidator->validateAgainstNode([
'a' => 'A',
'b' => 'B'
], $node);
}
public function testObjectWithPropertyTypeError() {
$this->setExpectedException(InvalidArgumentException::class);
$stringNode = $this->graph->createNode();
$stringNode->setProperty('coid://json.co-n.net/hasKey', 'a');
$stringNode->setType($this->graph->createNode('coid://json.co-n.net/String'));
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Object'));
$node->setProperty('coid://json.co-n.net/hasProperty', $stringNode);
$this->schemaValidator->validateAgainstNode([
'a' => 0,
'b' => 'B'
], $node);
}
public function testObjectWithRequiredProperty() {
$stringNode = $this->graph->createNode();
$stringNode->setProperty('coid://json.co-n.net/hasKey', 'a');
$stringNode->setProperty('coid://json.co-n.net/isRequired', 'true');
$stringNode->setType($this->graph->createNode('coid://json.co-n.net/String'));
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Object'));
$node->setProperty('coid://json.co-n.net/hasProperty', $stringNode);
$this->schemaValidator->validateAgainstNode([
'a' => 'A',
'b' => 'B'
], $node);
}
public function testObjectWithRequiredPropertyTypeError() {
$this->setExpectedException(InvalidArgumentException::class);
$stringNode = $this->graph->createNode();
$stringNode->setProperty('coid://json.co-n.net/hasKey', 'a');
$stringNode->setProperty('coid://json.co-n.net/isRequired', 'true');
$stringNode->setType($this->graph->createNode('coid://json.co-n.net/String'));
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Object'));
$node->setProperty('coid://json.co-n.net/hasProperty', $stringNode);
$this->schemaValidator->validateAgainstNode([
'a' => 0,
'b' => 'B'
], $node);
}
public function testObjectWithRequiredPropertyMissing() {
$this->setExpectedException(InvalidArgumentException::class);
$stringNode = $this->graph->createNode();
$stringNode->setProperty('coid://json.co-n.net/hasKey', 'a');
$stringNode->setProperty('coid://json.co-n.net/isRequired', 'true');
$stringNode->setType($this->graph->createNode('coid://json.co-n.net/String'));
$node = $this->graph->createNode();
$node->setType($this->graph->createNode('coid://json.co-n.net/Object'));
$node->setProperty('coid://json.co-n.net/hasProperty', $stringNode);
$this->schemaValidator->validateAgainstNode([
'b' => 'B',
'c' => 'C'
], $node);
}
}

View File

@@ -0,0 +1,156 @@
<?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;
use GuzzleHttp\Client, GuzzleHttp\Handler\MockHandler,
GuzzleHttp\HandlerStack, GuzzleHttp\Psr7\Response;
class NodeReaderMockTest extends \PHPUnit_Framework_TestCase {
private $retriever;
private $reader;
private function setMockResponse(Response $response) {
$mock = new MockHandler([$response]);
$handler = HandlerStack::create($mock);
$this->retriever->setClient(new Client(['handler' => $handler]));
}
private function useRootResourceMock() {
$this->setMockResponse(new Response(200,
['Content-Type' => 'application/ld+json'],
'{"@context":{"co":"coid:\/\/cloudobjects.io\/","rdf":"http:\/\/www.w3.org\/1999\/02\/22-rdf-syntax-ns#","agws":"coid:\/\/aauid.net\/","rdfs":"http:\/\/www.w3.org\/2000\/01\/rdf-schema#"},"@id":"coid:\/\/cloudobjects.io","@type":["agws:Service","co:Namespace"],"co:isAtRevision":"6-fbea0c90b2c5e5300e4039ed99be9b2d","co:isVisibleTo":{"@id":"co:Public"},"co:recommendsPrefix":"co","co:wasUpdatedAt":{"@type":"http:\/\/www.w3.org\/2001\/XMLSchema#dateTime","@value":"2017-01-16T17:29:22+00:00"},"rdfs:comment":"The CloudObjects namespace defines the essential objects.","rdfs:label":"CloudObjects"}'));
}
protected function setUp() {
$this->retriever = new ObjectRetriever;
$this->reader = new NodeReader([
'prefixes' => [
'co' => 'coid://cloudobjects.io/',
'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#'
]
]);
}
public function testHasType1() {
$coid = new IRI('coid://cloudobjects.io');
$this->useRootResourceMock();
$object = $this->retriever->getObject($coid);
$this->assertTrue($this->reader->hasType($object, 'coid://cloudobjects.io/Namespace'));
$this->assertTrue($this->reader->hasType($object, 'co:Namespace'));
$this->assertFalse($this->reader->hasType($object, 'coid://cloudobjects.io/MemberRole'));
$this->assertFalse($this->reader->hasType($object, 'co:MemberRole'));
}
public function testHasPropertyValue1() {
$coid = new IRI('coid://cloudobjects.io');
$this->useRootResourceMock();
$object = $this->retriever->getObject($coid);
$this->assertTrue($this->reader->hasPropertyValue($object, 'http://www.w3.org/2000/01/rdf-schema#label', 'CloudObjects'));
$this->assertTrue($this->reader->hasPropertyValue($object, 'rdfs:label', 'CloudObjects'));
}
public function testGetFirstValueString1() {
$coid = new IRI('coid://cloudobjects.io');
$this->useRootResourceMock();
$object = $this->retriever->getObject($coid);
$this->assertEquals('CloudObjects', $this->reader->getFirstValueString($object, 'http://www.w3.org/2000/01/rdf-schema#label'));
$this->assertEquals('CloudObjects', $this->reader->getFirstValueString($object, 'rdfs:label'));
$this->assertNull($this->reader->getFirstValueString($object, 'coid://cloudobjects.io/makesTriplesVisibleTo'));
$this->assertNull($this->reader->getFirstValueString($object, 'co:makesTriplesVisibleTo'));
$this->assertEquals('theDefaultValue', $this->reader->getFirstValueString($object, 'coid://cloudobjects.io/makesTriplesVisibleTo', 'theDefaultValue'));
$this->assertEquals('theDefaultValue', $this->reader->getFirstValueString($object, 'co:makesTriplesVisibleTo', 'theDefaultValue'));
}
public function testGetFirstValueIRI1() {
$coid = new IRI('coid://cloudobjects.io');
$this->useRootResourceMock();
$object = $this->retriever->getObject($coid);
$this->assertInstanceOf('ML\IRI\IRI', $this->reader->getFirstValueIRI($object, 'coid://cloudobjects.io/isVisibleTo'));
$this->assertInstanceOf('ML\IRI\IRI', $this->reader->getFirstValueIRI($object, 'co:isVisibleTo'));
$this->assertEquals(new IRI('coid://cloudobjects.io/Public'), $this->reader->getFirstValueIRI($object, 'coid://cloudobjects.io/isVisibleTo'));
$this->assertEquals(new IRI('coid://cloudobjects.io/Public'), $this->reader->getFirstValueIRI($object, 'co:isVisibleTo'));
}
public function testGetFirstValueNode1() {
$coid = new IRI('coid://cloudobjects.io');
$this->useRootResourceMock();
$object = $this->retriever->getObject($coid);
$this->assertInstanceOf('ML\JsonLD\Node', $this->reader->getFirstValueNode($object, 'coid://cloudobjects.io/isVisibleTo'));
$this->assertInstanceOf('ML\JsonLD\Node', $this->reader->getFirstValueNode($object, 'co:isVisibleTo'));
$this->assertEquals('coid://cloudobjects.io/Public', $this->reader->getFirstValueNode($object, 'coid://cloudobjects.io/isVisibleTo')->getId());
$this->assertEquals('coid://cloudobjects.io/Public', $this->reader->getFirstValueNode($object, 'co:isVisibleTo')->getId());
}
public function testGetAllValuesString1() {
$coid = new IRI('coid://cloudobjects.io');
$this->useRootResourceMock();
$object = $this->retriever->getObject($coid);
$this->assertCount(1, $this->reader->getAllValuesString($object, 'http://www.w3.org/2000/01/rdf-schema#label'));
$this->assertCount(1, $this->reader->getAllValuesString($object, 'rdfs:label'));
$this->assertEquals('CloudObjects', $this->reader->getAllValuesString($object, 'http://www.w3.org/2000/01/rdf-schema#label')[0]);
$this->assertEquals('CloudObjects', $this->reader->getAllValuesString($object, 'rdfs:label')[0]);
$this->assertCount(0, $this->reader->getAllValuesString($object, 'coid://cloudobjects.io/makesTriplesVisibleTo'));
$this->assertCount(0, $this->reader->getAllValuesString($object, 'co:makesTriplesVisibleTo'));
$this->assertCount(2, $this->reader->getAllValuesString($object, '@type'));
}
public function testGetAllValuesIRI1() {
$coid = new IRI('coid://cloudobjects.io');
$this->useRootResourceMock();
$object = $this->retriever->getObject($coid);
$this->assertCount(0, $this->reader->getAllValuesIRI($object, 'http://www.w3.org/2000/01/rdf-schema#label'));
$this->assertCount(0, $this->reader->getAllValuesIRI($object, 'rdfs:label'));
$this->assertCount(1, $this->reader->getAllValuesIRI($object, 'coid://cloudobjects.io/isVisibleTo'));
$this->assertCount(1, $this->reader->getAllValuesIRI($object, 'co:isVisibleTo'));
$this->assertCount(2, $this->reader->getAllValuesIRI($object, '@type'));
$this->assertInstanceOf('ML\IRI\IRI', $this->reader->getAllValuesIRI($object, 'coid://cloudobjects.io/isVisibleTo')[0]);
$this->assertInstanceOf('ML\IRI\IRI', $this->reader->getAllValuesIRI($object, 'co:isVisibleTo')[0]);
$this->assertEquals(new IRI('coid://cloudobjects.io/Public'), $this->reader->getAllValuesIRI($object, 'coid://cloudobjects.io/isVisibleTo')[0]);
$this->assertEquals(new IRI('coid://cloudobjects.io/Public'), $this->reader->getAllValuesIRI($object, 'co:isVisibleTo')[0]);
}
public function testGetAllValuesNode1() {
$coid = new IRI('coid://cloudobjects.io');
$this->useRootResourceMock();
$object = $this->retriever->getObject($coid);
$this->assertCount(0, $this->reader->getAllValuesNode($object, 'http://www.w3.org/2000/01/rdf-schema#label'));
$this->assertCount(0, $this->reader->getAllValuesNode($object, 'rdfs:label'));
$this->assertCount(1, $this->reader->getAllValuesNode($object, 'coid://cloudobjects.io/isVisibleTo'));
$this->assertCount(1, $this->reader->getAllValuesNode($object, 'co:isVisibleTo'));
$this->assertCount(2, $this->reader->getAllValuesNode($object, '@type'));
$this->assertInstanceOf('ML\JsonLD\Node', $this->reader->getAllValuesNode($object, 'coid://cloudobjects.io/isVisibleTo')[0]);
$this->assertInstanceOf('ML\JsonLD\Node', $this->reader->getAllValuesNode($object, 'co:isVisibleTo')[0]);
$this->assertEquals('coid://cloudobjects.io/Public', $this->reader->getAllValuesNode($object, 'coid://cloudobjects.io/isVisibleTo')[0]->getId());
$this->assertEquals('coid://cloudobjects.io/Public', $this->reader->getAllValuesNode($object, 'co:isVisibleTo')[0]->getId());
}
}

View File

@@ -0,0 +1,40 @@
<?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;
use GuzzleHttp\Client, GuzzleHttp\Handler\MockHandler,
GuzzleHttp\HandlerStack, GuzzleHttp\Psr7\Response;
class ObjectRetrieverMockTest extends \PHPUnit_Framework_TestCase {
private $retriever;
private function setMockResponse(Response $response) {
$mock = new MockHandler([$response]);
$handler = HandlerStack::create($mock);
$this->retriever->setClient(new Client(['handler' => $handler]));
}
protected function setUp() {
$this->retriever = new ObjectRetriever;
}
public function testGetRootResource() {
$this->setMockResponse(new Response(200,
['Content-Type' => 'application/ld+json'],
'{"@context":{"cloudobjects":"coid:\/\/cloudobjects.io\/","rdf":"http:\/\/www.w3.org\/1999\/02\/22-rdf-syntax-ns#","rdfs":"http:\/\/www.w3.org\/2000\/01\/rdf-schema#"},"@id":"coid:\/\/cloudobjects.io","@type":"cloudobjects:Namespace","cloudobjects:hasPublicListing":"true","cloudobjects:revision":"1-325baa62b76105f56dc09386f5a2ec91","rdfs:comment":"The CloudObjects namespace defines the essential objects.","rdfs:label":"CloudObjects"}'));
$coid = new IRI('coid://cloudobjects.io');
$object = $this->retriever->getObject($coid);
$this->assertNotNull($object);
$this->assertEquals((string)$coid, $object->getID());
$this->assertNotNull($object->getProperty('http://www.w3.org/2000/01/rdf-schema#label'));
$this->assertEquals('CloudObjects', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
}
}

View File

@@ -0,0 +1,38 @@
<?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 InvalidArgumentException;
use ML\IRI\IRI;
use CloudObjects\SDK\ObjectRetriever;
class SchemaValidatorPublicTest extends \PHPUnit_Framework_TestCase {
private $schemaValidator;
public function setUp() {
$this->schemaValidator = new SchemaValidator(new ObjectRetriever);
}
public function testAddress() {
$this->schemaValidator->validateAgainstCOID([
'locality' => 'Frankfurt',
'region' => 'Hessen',
'country-name' => 'Germany'
], new IRI('coid://json.co-n.net/Address'));
}
public function testNotAddress() {
$this->setExpectedException(InvalidArgumentException::class);
$this->schemaValidator->validateAgainstCOID([
'region' => 'Hessen',
'country-name' => 'Germany'
], new IRI('coid://json.co-n.net/Address'));
}
}

View File

@@ -0,0 +1,60 @@
<?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;
class ObjectRetrieverTest extends \PHPUnit_Framework_TestCase {
private $retriever;
protected function setUp() {
$this->retriever = new ObjectRetriever;
}
private function stringifyItems(array $input) {
$output = [];
foreach ($input as $i)
$output[] = (string)$i;
return $output;
}
public function testGetRootObject() {
$coid = new IRI('coid://cloudobjects.io');
$object = $this->retriever->getObject($coid);
$this->assertNotNull($object);
$this->assertEquals((string)$coid, $object->getID());
$this->assertNotNull($object->getProperty('http://www.w3.org/2000/01/rdf-schema#label'));
$this->assertEquals('CloudObjects', $object->getProperty('http://www.w3.org/2000/01/rdf-schema#label')->getValue());
}
public function testGetCOIDList() {
$coid = new IRI('coid://cloudobjects.io');
$list = $this->stringifyItems(
$this->retriever->getCOIDListForNamespace($coid)
);
$this->assertNotEmpty($list);
$this->assertContains('coid://cloudobjects.io/isVisibleTo', $list);
$this->assertContains('coid://cloudobjects.io/Public', $list);
$this->assertNotContains('coid://json.co-n.net/Element', $list);
}
public function testGetFilteredCOIDList() {
$coid = new IRI('coid://cloudobjects.io');
$list = $this->stringifyItems(
$this->retriever->getCOIDListForNamespaceWithType($coid, 'coid://cloudobjects.io/Audience')
);
$this->assertNotEmpty($list);
$this->assertNotContains('coid://cloudobjects.io/isVisibleTo', $list);
$this->assertContains('coid://cloudobjects.io/Public', $list);
$this->assertContains('coid://cloudobjects.io/Private', $list);
}
}

7
tests/bootstrap.php Normal file
View File

@@ -0,0 +1,7 @@
<?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/. */
require_once __DIR__."/../vendor/autoload.php";