Imported code from old repository

This commit is contained in:
2022-11-22 15:46:36 +01:00
parent 976fdc0199
commit 77cf9e05f1
91 changed files with 18969 additions and 2 deletions
+285
View File
@@ -0,0 +1,285 @@
<?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\PhpMAE;
use Psr\Container\ContainerInterface;
use ML\IRI\IRI, ML\JsonLD\Node;
use DI\Container, DI\FactoryInterface, Invoker\InvokerInterface;
use DI\Definition\Source\DefinitionArray, DI\Definition\Source\SourceChain;
use Psr\Http\Message\RequestInterface;
use CloudObjects\SDK\ObjectRetriever, CloudObjects\SDK\COIDParser,
CloudObjects\SDK\NodeReader;
use CloudObjects\PhpMAE\Exceptions\PhpMAEException;
class ClassRepository {
const DEFAULT_STACK = 'coid://phpmae.dev/DefaultStack';
private $options;
private $classMap = [];
private $container;
private $reader;
private function loader($classname) {
if (isset($this->classMap[$classname])) {
include $this->classMap[$classname];
}
}
public function __construct(ContainerInterface $container) {
$this->options = [
'cache_dir' => $container->get('cache_dir') . '/classes',
'uploads_dir' => $container->get('uploads_dir'),
'uploads' => $container->get('uploads')
];
$this->container = $container;
$this->reader = new NodeReader([
'prefixes' => [
'phpmae' => 'coid://phpmae.dev/'
]
]);
// Initialize autoloader
spl_autoload_register(array($this, 'loader'));
}
private function getURIVars(IRI $uri) {
$vars = array('uri_hash' => strtoupper(md5((string)$uri)));
$vars['php_namespace'] = "CloudObjects\\PhpMAE\\Class_".$vars['uri_hash'];
$vars['php_classname_local'] = (COIDParser::getType($uri)==COIDParser::COID_VERSIONED)
? substr($uri->getPath(), 1, strrpos($uri->getPath(), '/')-1)
: substr($uri->getPath(), 1);
$vars['php_classname'] = $vars['php_namespace'].'\\'.$vars['php_classname_local'];
$vars['cache_path'] = $this->options['cache_dir'].DIRECTORY_SEPARATOR.$vars['uri_hash'];
$vars['upload_filename'] = $this->options['uploads_dir'].DIRECTORY_SEPARATOR.$vars['uri_hash'].'.php';
if ($this->options['uploads'] == true && !is_dir($this->options['uploads_dir']))
mkdir($this->options['uploads_dir'], 0777, true);
return $vars;
}
public function coidToClassName(IRI $uri) {
$vars = $this->getURIVars($uri);
return $vars['php_classname'];
}
/**
* Get a path on which custom files can be cached for a object.
* @param Node $object
*/
public function getCustomFilesCachePath(Node $object) {
$path = $this->options['cache_dir'].DIRECTORY_SEPARATOR
.strtoupper(md5($object->getId())).DIRECTORY_SEPARATOR
.($object->getProperty(ObjectRetriever::REVISION_PROPERTY)
? $object->getProperty(ObjectRetriever::REVISION_PROPERTY)->getValue()
: 'LocalConfig');
if (!is_dir($path)) mkdir($path, 0777, true);
return $path;
}
public function storeUploadedFile(Node $object, $content) {
$uri = new IRI($object->getId());
$vars = $this->getURIVars($uri);
// Fetch class description
$revision = $object->getProperty(ObjectRetriever::REVISION_PROPERTY)
? $object->getProperty(ObjectRetriever::REVISION_PROPERTY)->getValue()
: 'LocalConfig';
// Clear cache if cached version exists
$cachedFilename = $vars['cache_path'].DIRECTORY_SEPARATOR.$revision.".php";
if (file_exists($cachedFilename)) unlink($cachedFilename);
// Store source
file_put_contents($vars['upload_filename'], $content);
return true;
}
private function buildContainer($className, Node $object, $additionalDefinitions = []) {
$autowiring = new DI\WhitelistReflectionBasedAutowiring;
$sources = [
new DefinitionArray($this->container->get(DI\DependencyInjector::class)
->getDependencies($object, $additionalDefinitions), $autowiring),
new DefinitionArray([
Engine::SKEY => \DI\autowire($className)
], $autowiring),
$autowiring
];
$source = new SourceChain($sources);
$source->setMutableDefinitionSource(new DefinitionArray([], $autowiring));
// TODO: add compilation
$container = new Container($source);
$sandboxedContainer = new DI\SandboxedContainer($container);
$container->set(ContainerInterface::class, $sandboxedContainer);
$container->set(Container::class, $sandboxedContainer);
$container->set(FactoryInterface::class, $sandboxedContainer);
$container->set(InvokerInterface::class, $sandboxedContainer);
return $sandboxedContainer;
}
/**
* Create an instance of a class. Returns a container that includes the class itself as Engine::SKEY
* as well as all dependencies specified by the class.
*
* @param Node $object The object describing the class.
* @param RequestInterface $request The optional request, if it should be made available
* @param array $additionalDefinitions Definitions to add to the DI container for this class
* @param string $sourceCode Optional source code to use for the class implementation instead of retrieving.
*/
public function createInstance(Node $object, RequestInterface $request = null,
array $additionalDefinitions = [], string $sourceCode = null) {
// Check type
if (!TypeChecker::isClass($object))
throw new PhpMAEException("<".$object->getId()."> must have a valid type.");
$uri = new IRI($object->getId());
$vars = $this->getURIVars($uri);
if (!isset($this->classMap[$vars['php_classname']])) {
$objectRetriever = $this->container->get(ObjectRetriever::class);
// Get revision
$revision = $object->getProperty(ObjectRetriever::REVISION_PROPERTY)
? $object->getProperty(ObjectRetriever::REVISION_PROPERTY)->getValue()
: 'LocalConfig';
// Build filename where cached version should exist
$filename = $vars['cache_path'].DIRECTORY_SEPARATOR.$revision.".php";
$this->container->get(ErrorHandler::class)
->addMapping($filename, $object);
// Check for required implemented interfaces
$interfaces = [];
foreach (TypeChecker::getAdditionalTypes($object) as $i) {
$interfaceObject = $objectRetriever->get($i);
if (!TypeChecker::isInterface($interfaceObject))
continue;
$this->createInterfaceInstance($interfaceObject);
$interfaces[] = $i;
}
// Load the stack that the class requires
$stack = $this->reader->getFirstValueString($object, 'phpmae:usesStack',
self::DEFAULT_STACK);
$stackDir = __DIR__.'/../stacks/'.md5($stack);
if (!is_dir($stackDir))
throw new PhpMAEException("The stack <".$stack."> is not installed on this phpMAE instance.");
require_once $stackDir.'/vendor/autoload.php';
if (!file_exists($filename) || $sourceCode != null) {
if (!isset($sourceCode)) {
// File does not exist -> check in uploads first
if (file_exists($vars['upload_filename'])) {
$sourceCode = file_get_contents($vars['upload_filename']);
} else {
// Not found in local uploads -> download source from CloudObjects
$sourceUrl = $object->getProperty('coid://phpmae.dev/hasSourceFile');
if (!$sourceUrl) throw new PhpMAEException("<".$object->getId()."> does not have an implementation source file.");
if (get_class($sourceUrl)=='ML\JsonLD\Node')
$sourceCode = $objectRetriever->getAttachment($uri, $sourceUrl->getId());
else
$sourceCode = $objectRetriever->getAttachment($uri, $sourceUrl->getValue());
}
}
// Run source code through validator to ensure sanity
// and convert to sandbox
$validator = new ClassValidator;
$sourceCode = $validator->validate($sourceCode, $uri, $interfaces);
// Add namespaces for dependencies and interfaces
$use = '';
$classList = array_merge(
$this->container->get(DI\DependencyInjector::class)->getClassDependencyList($object),
$interfaces
);
foreach ($classList as $cl)
$use .= " use ".$this->coidToClassName($cl).";";
// Add namespace declaration
$sourceCode = "<?php namespace ".$vars['php_namespace'].";".$use.$sourceCode;
$sourceCode = str_replace($vars['php_classname_local'].'::', '\\'.$vars['php_classname'].'::', $sourceCode);
// Store
if (!file_exists($vars['cache_path'])) mkdir($vars['cache_path'], 0777, true);
file_put_contents($filename, $sourceCode);
}
$this->classMap[$vars['php_classname']] = $filename;
}
if (isset($request)) {
$additionalDefinitions['request'] = $request;
$additionalDefinitions[RequestInterface::class] = $request;
}
return $this->buildContainer($vars['php_classname'], $object, $additionalDefinitions);
}
/**
* Creates an instance of an interface.
*
* @param Node $object The object describing the interface.
*/
public function createInterfaceInstance(Node $object) {
// Check type
if (!TypeChecker::isInterface($object))
throw new PhpMAEException("<".$object->getId()."> must have a valid type.");
$uri = new IRI($object->getId());
$vars = $this->getURIVars($uri);
if (!isset($this->classMap[$vars['php_classname']])) {
$objectRetriever = $this->container->get(ObjectRetriever::class);
// Get revision
$revision = $object->getProperty(ObjectRetriever::REVISION_PROPERTY)
? $object->getProperty(ObjectRetriever::REVISION_PROPERTY)->getValue()
: 'LocalConfig';
// Build filename where cached version should exist
$filename = $vars['cache_path'].DIRECTORY_SEPARATOR.$revision.".php";
$this->container->get(ErrorHandler::class)
->addMapping($filename, $object);
if (!file_exists($filename)) {
// File does not exist -> check in uploads first
if (file_exists($vars['upload_filename'])) {
$sourceCode = file_get_contents($vars['upload_filename']);
} else {
// Not found in local uploads -> download source from CloudObjects
$sourceUrl = $object->getProperty('coid://phpmae.dev/hasDefinitionFile');
if (!$sourceUrl) throw new PhpMAEException("<".$object->getId()."> does not have a definition file.");
if (get_class($sourceUrl)=='ML\JsonLD\Node')
$sourceCode = $objectRetriever->getAttachment($uri, $sourceUrl->getId());
else
$sourceCode = $objectRetriever->getAttachment($uri, $sourceUrl->getValue());
}
// Run source code through validator to ensure sanity
$validator = new ClassValidator;
$validator->validateInterface($sourceCode, $uri);
// Add namespace declaration
$sourceCode = str_replace("<?php", "<?php namespace ".$vars['php_namespace'].";", $sourceCode);
$sourceCode = str_replace($vars['php_classname_local'].'::', '\\'.$vars['php_classname'].'::', $sourceCode);
// Store
if (!file_exists($vars['cache_path'])) mkdir($vars['cache_path'], 0777, true);
file_put_contents($filename, $sourceCode);
}
$this->classMap[$vars['php_classname']] = $filename;
}
}
}
+212
View File
@@ -0,0 +1,212 @@
<?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\PhpMAE;
use CloudObjects\PhpMAE\Sandbox\CustomizedSandbox;
use PHPSandbox\SandboxWhitelistVisitor, PHPSandbox\ValidatorVisitor;
use PhpParser\ParserFactory, PhpParser\NodeTraverser;
use ML\IRI\IRI;
use CloudObjects\SDK\COIDParser;
use CloudObjects\PhpMAE\Exceptions\PhpMAEException;
/**
* Validates that classes fit the sandbox criteria.
*/
class ClassValidator {
private $sandbox;
private $whitelisted_interfaces;
private $whitelisted_types;
private $aliases;
public function __construct() {
$this->sandbox = new CustomizedSandbox;
$this->whitelisted_interfaces = [
'Symfony\Component\EventDispatcher\EventSubscriberInterface',
'Psr\Http\Message\RequestInterface',
'Psr\Container\ContainerInterface',
'Psr\SimpleCache\CacheInterface',
'Psr\Http\Message\ResponseInterface',
'Symfony\Component\Mailer\MailerInterface'
];
$this->whitelisted_types = [
'ArrayObject', 'DateInterval', 'DateTime', 'DateTimeImmutable', 'DateTimeZone',
'DOMElement', 'Exception',
'SimpleXMLElement',
'ML\IRI\IRI',
'ML\JsonLD\JsonLD', 'ML\JsonLD\Node', 'ML\JsonLD\Graph', 'ML\JsonLD\TypedValue',
'GuzzleHttp\Client',
'GuzzleHttp\HandlerStack', 'GuzzleHttp\Middleware',
'GuzzleHttp\Handler\CurlHandler',
'GuzzleHttp\Subscriber\Oauth\Oauth1',
'GuzzleHttp\Promise',
'Dflydev\FigCookies\SetCookie',
'Symfony\Component\Mime\Email',
'CloudObjects\PhpMAE\ConfigLoader',
'CloudObjects\PhpMAE\TwigTemplateFactory',
'CloudObjects\PhpMAE\DI\DynamicLoader',
'CloudObjects\SDK\ObjectRetriever',
'CloudObjects\SDK\NodeReader',
'CloudObjects\SDK\AccountGateway\AccountContext',
'CloudObjects\SDK\AccountGateway\AAUIDParser',
'CloudObjects\SDK\COIDParser',
'CloudObjects\SDK\Common\CryptoHelper',
'Webmozart\Assert\Assert'
];
}
public function isWhitelisted($name) {
return in_array($name, $this->whitelisted_types)
|| in_array($name, $this->whitelisted_interfaces);
}
private function initializeWhitelist($stack = ClassRepository::DEFAULT_STACK) {
// Generate whitelist based on alias names
$interfaces = [];
$types = [];
foreach ($this->whitelisted_interfaces as $i) {
$interfaces[] = (isset($this->aliases[$i]))
? strtolower($this->aliases[$i]) : strtolower($i);
// Validation of some elements relies on interfaces also being added as types
$types[] = (isset($this->aliases[$i]))
? strtolower($this->aliases[$i]) : strtolower($i);
}
foreach ($this->whitelisted_types as $t) {
$types[] = (isset($this->aliases[$t]))
? strtolower($this->aliases[$t]) : strtolower($t);
}
// Load and apply stack
$filename = __DIR__.'/../stacks/'.md5($stack).'/meta.json';
if (!file_exists($filename))
throw new PhpMAEException("The specified stack <".$stack."> is not installed.");
$stackMeta = json_decode(file_get_contents($filename), true);
if (isset($stackMeta['whitelisted_classes'])) {
foreach ($stackMeta['whitelisted_classes'] as $t) {
$types[] = (isset($this->aliases[$t]))
? strtolower($this->aliases[$t]) : strtolower($t);
}
}
// Apply to sandbox
$this->sandbox->whitelist([
'interfaces' => $interfaces,
'types' => $types,
'classes' => $types
]);
}
public function validate($sourceCode, IRI $coid, array $interfaceCoids = [],
$stack = ClassRepository::DEFAULT_STACK) {
// Initialize parser and parse source code
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($sourceCode);
// Parse and dump use statements
$aliasMap = array();
while (get_class($ast[0])=='PhpParser\Node\Stmt\Use_') {
foreach ($ast[0]->uses as $use) {
$name = (string)$use->name;
$aliasMap[$use->alias] = $name;
$this->aliases[$name] = $use->alias;
}
array_shift($ast);
}
// Check for class definition and implemented interfaces
if (count($ast)==1 && get_class($ast[0])=='PhpParser\Node\Stmt\Class_'
&& isset($ast[0]->implements)) {
if ($ast[0]->name != COIDParser::getName($coid))
throw new PhpMAEException("The PHP classname (".$ast[0]->name.") doesn't match the name segment of the COID (".COIDParser::getName($coid).").");
$interfaces = [];
foreach ($ast[0]->implements as $i) {
$name = (string)$i;
$interfaces[] = (isset($aliasMap[$name])) ? $aliasMap[$name] : $name;
}
// Check for any interfaces and whitelist them for validation
foreach ($interfaceCoids as $coid) {
if (!COIDParser::isValidCOID($coid)) continue;
$name = COIDParser::getName($coid);
$found = false;
foreach ($interfaces as $i) {
if ($i == $name) {
$this->whitelisted_interfaces[] = $i;
$found = true;
break;
}
}
if (!$found)
throw new PhpMAEException("Source code file must declare a class that implements <".$name.">.");
}
// Allow self-references
$this->whitelisted_types[] = strtolower($ast[0]->name);
// Initialize whitelist
$this->initializeWhitelist($stack);
// Validate and prepare code in sandbox
return $this->sandbox->prepare($sourceCode);
} else {
// Throw exeption if conditions are not met
throw new PhpMAEException("Source code file must include exactly one class declaration and must not contain further side effects.");
}
}
public function validateInterface($sourceCode, IRI $coid) {
// Initialize parser and parse source code
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($sourceCode);
// Parse and dump use statements
$aliasMap = array();
while (get_class($ast[0])=='PhpParser\Node\Stmt\Use_') {
foreach ($ast[0]->uses as $use) {
$name = (string)$use->name;
$aliasMap[$use->alias] = $name;
$this->aliases[$name] = $use->alias;
}
array_shift($ast);
}
// Check for class definition and implemented interfaces
if (count($ast)==1 && get_class($ast[0])=='PhpParser\Node\Stmt\Interface_') {
if ($ast[0]->name != COIDParser::getName($coid))
throw new PhpMAEException("The PHP interface name (".$ast[0]->name.") doesn't match the name segment of the COID (".COIDParser::getName($coid).").");
// Allow self-references
$this->whitelisted_types[] = strtolower($ast[0]->name);
// Initialize whitelist
$this->initializeWhitelist();
// Apply whitelist visitor
$traverser = new NodeTraverser;
$traverser->addVisitor(new SandboxWhitelistVisitor($this->sandbox));
$traverser->addVisitor(new ValidatorVisitor($this->sandbox));
$traverser->traverse($ast);
return;
} else {
// Throw exeption if conditions are not met
throw new PhpMAEException("Source code file must include exactly one interface declaration and must not contain further side effects.");
}
}
public static function isInvokableClass($class) {
return method_exists($class, '__invoke');
}
}
@@ -0,0 +1,90 @@
<?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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class AbstractAddDependenciesCommand extends AbstractObjectCommand {
protected function configure() {
$this->addOption('confjob', null, InputOption::VALUE_NONE, 'Calls "cloudobjects" to create a configuration job for the updated class.');
}
protected function dependencyPrecheck() {
$this->assertRDF();
$this->assertPHPExists();
}
protected function getObjectAndAssertType(string $coid, string $type) {
// Retrieve configuration
$config = shell_exec("cloudobjects get ".$coid);
if (!isset($config))
throw new \Exception("Could not retrieve <".$coid.">.");
// Parse and validate configuration
$parser = \ARC2::getRDFXMLParser();
$parser->parse('', $config);
$index = $parser->getSimpleIndex(false);
if (!isset($index) || !isset($index[$coid])
|| !isset($index[$coid]['http://www.w3.org/1999/02/22-rdf-syntax-ns#type']))
throw new \Exception("<".$coid."> is not a valid CloudObjects object.");
$hasType = false;
foreach ($index[$coid]['http://www.w3.org/1999/02/22-rdf-syntax-ns#type'] as $property => $values) {
if ($values['value'] == $type) {
$hasType = true;
break;
}
}
if (!$hasType)
throw new \Exception("<".$coid."> must have the type <".$type.">.");
}
protected function addDependency(string $key, string $type, array $valuesToMerge,
InputInterface $input, OutputInterface $output) {
$this->index['_:dep-'.$key] = array_merge([
'http://www.w3.org/1999/02/22-rdf-syntax-ns#type' => [
[ 'type' => 'uri', 'value' => $type ]
],
'coid://phpmae.dev/hasKey' => [
[ 'type' => 'literal', 'value' => $key ]
]
], $valuesToMerge);
// Edit configuration
$object = $this->index[(string)$this->coid];
if (isset($object['coid://phpmae.dev/hasDependency'])) {
foreach ($object['coid://phpmae.dev/hasDependency'] as $o) {
if ($o['type'] != 'bnode') continue;
$bnode = $this->index[$o['value']];
if (isset($bnode['coid://phpmae.dev/hasKey']) && $bnode['coid://phpmae.dev/hasKey'][0]['value'] == $key)
throw new \Exception("A dependency with the key '".$key."' already exists.");
foreach ($valuesToMerge as $k => $values)
if (isset($bnode[$k]))
foreach ($bnode[$k] as $a)
foreach ($valuesToMerge[$k] as $b)
if ($a['type'] == $b['type'] && $a['value'] == $b['value'])
throw new \Exception("This dependency was already added.");
}
$object['coid://phpmae.dev/hasDependency'][] = [ 'type' => 'bnode', 'value' => '_:dep-'.$key ];
} else
$object['coid://phpmae.dev/hasDependency'] = [['type' => 'bnode', 'value' => '_:dep-'.$key ]];
// Persist configuration
$this->index[(string)$this->coid] = $object;
$this->updateRDFLocally($output);
if ($input->getOption('confjob'))
$this->createConfigurationJob($output);
}
}
+133
View File
@@ -0,0 +1,133 @@
<?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\PhpMAE\Commands;
use Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Output\OutputInterface;
use ML\IRI\IRI;
use CloudObjects\SDK\COIDParser;
use CloudObjects\PhpMAE\CredentialManager;
abstract class AbstractObjectCommand extends Command {
protected $coid;
protected $fullName;
protected $phpFileName;
protected $xmlFileName;
protected $rdfTypes;
protected $index;
protected function requireCloudObjectsCLI() {
if (!CredentialManager::isConfigured())
throw new Exception("The 'cloudobjects' CLI tool must be installed and authorized.");
}
protected function parse($coid) {
$this->coid = COIDParser::fromString($coid);
if (COIDParser::getType($this->coid)!=COIDParser::COID_VERSIONED
&& COIDParser::getType($this->coid)!=COIDParser::COID_UNVERSIONED)
throw new Exception("Invalid COID: ".(string)$this->coid);
$name = COIDParser::getName($this->coid);
$version = COIDParser::getVersion($this->coid);
$this->fullName = isset($version) ? $name.".".$version : $name;
$this->phpFileName = $this->fullName.'.php';
$this->xmlFileName = $this->fullName.'.xml';
}
protected function assertRDF() {
if (!file_exists($this->xmlFileName))
throw new Exception("File not found: ".$this->xmlFileName);
$parser = \ARC2::getRDFXMLParser();
$parser->parse('', file_get_contents($this->xmlFileName));
$index = $parser->getSimpleIndex(false);
$id = (string)$this->coid;
if (!isset($index) || !isset($index[$id])
|| !isset($index[$id]['http://www.w3.org/1999/02/22-rdf-syntax-ns#type']))
throw new Exception($this->xmlFileName." does not contain a valid RDF description of the object.");
$this->rdfTypes = [];
foreach ($index[$id]['http://www.w3.org/1999/02/22-rdf-syntax-ns#type'] as $o)
if ($o['type'] == 'uri') $this->rdfTypes[] = $o['value'];
$this->index = $index;
}
protected function updateRDFLocally(OutputInterface $output) {
file_put_contents($this->xmlFileName,
$this->getSerializer()->getSerializedIndex($this->index));
$output->writeln("Updated ".$this->xmlFileName);
}
protected function createConfigurationJob(OutputInterface $output) {
$output->writeln("Calling cloudobjects configuration-job:create ...");
passthru("cloudobjects configuration-job:create ".$this->xmlFileName);
}
protected function ensureFilenameInConfig(OutputInterface $output, $interface = false) {
$property = ($interface ? 'coid://phpmae.dev/hasDefinitionFile'
: 'coid://phpmae.dev/hasSourceFile');
$object = $this->index[(string)$this->coid];
if (!isset($object[$property])) {
$object[$property] = [[
'value' => 'file:///'.$this->fullName.'.php',
'type' => 'uri'
]];
$this->index[(string)$this->coid] = $object;
$this->updateRDFLocally($output);
return true;
} else
return false;
}
protected function assertPHPExists() {
if (!file_exists($this->phpFileName))
throw new Exception("File not found: ".$this->phpFileName);
}
protected function getSerializer() {
return \ARC2::getRDFXMLSerializer(array(
'serializer_type_nodes' => true,
'ns' => array(
'co' => 'coid://cloudobjects.io/',
'phpmae' => 'coid://phpmae.dev/'
)
));
}
protected function watchPHPFile(OutputInterface $output, callable $callable) {
$fileTime = filemtime($this->phpFileName);
$output->writeln('Watching for changes ...');
set_time_limit(0);
while (true) {
clearstatcache();
if (filemtime($this->phpFileName) != $fileTime) {
// File has changed ...
sleep(1);
$fileTime = filemtime($this->phpFileName);
$callable();
$output->writeln('Watching for changes ...');
}
sleep(0.5);
}
}
protected function getAdditionalTypes() {
$coids = [];
foreach ($this->rdfTypes as $t) {
if (in_array($t, [ 'coid://phpmae.dev/Class',
'coid://phpmae.dev/HTTPInvokableClass',
'coid://phpmae.dev/Interface' ]))
continue;
$coids[] = new IRI($t);
}
return $coids;
}
}
+243
View File
@@ -0,0 +1,243 @@
<?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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;
use ML\IRI\IRI;
use CloudObjects\SDK\COIDParser;
use CloudObjects\PhpMAE\CredentialManager, CloudObjects\PhpMAE\ClassValidator;
use CloudObjects\PhpMAE\Exceptions\PhpMAEException;
class ClassCreateCommand extends Command {
protected function configure() {
$this->setName('class:create')
->setDescription('Create a new class for the phpMAE.')
->addArgument('coid', InputArgument::REQUIRED, 'The COID of the object.')
->addOption('http-invokable', 'hi', InputOption::VALUE_NONE, 'Makes the class HTTP-invokable.')
->addOption('force', 'f', InputOption::VALUE_NONE, 'Forces new object creation and replaces existing files.')
->addOption('public', null, InputOption::VALUE_NONE, 'Marks new object as public.')
->addOption('confjob', null, InputOption::VALUE_NONE, 'Calls "cloudobjects" to create a configuration job for the new class.')
->addOption('autowire', null, InputOption::VALUE_REQUIRED, 'Creates a constructor that autowires a PHP dependency.', null)
->addOption('implements', null, InputOption::VALUE_REQUIRED, 'Make this class an implementation of the phpMAE interface with the specified COID.', null);
}
private function getAndValidateInterfaceConfig($implements) {
// Check interface COID
$interfaceCoid = COIDParser::fromString($implements);
if (COIDParser::getType($interfaceCoid) != COIDParser::COID_VERSIONED
&& COIDParser::getType($interfaceCoid) != COIDParser::COID_UNVERSIONED)
throw new \Exception("Invalid Interface COID: ".(string)$interfaceCoid);
// Retrieve interface configuration
$implements = (string)$interfaceCoid;
$interfaceConfig = shell_exec("cloudobjects get ".$implements);
if (!isset($interfaceConfig))
throw new \Exception("Could not retrieve interface configuration.");
// Parse and validate interface configuration
$parser = \ARC2::getRDFXMLParser();
$parser->parse('', $interfaceConfig);
$index = $parser->getSimpleIndex(false);
if (!isset($index) || !isset($index[$implements])
|| !isset($index[$implements]['http://www.w3.org/1999/02/22-rdf-syntax-ns#type']))
throw new \Exception("<".$implements."> is not a valid CloudObjects object.");
$isInterface = false;
foreach ($index[$implements]['http://www.w3.org/1999/02/22-rdf-syntax-ns#type'] as $property => $values) {
if ($values['value'] == 'coid://phpmae.dev/Interface')
$isInterface = true;
}
if (!$isInterface || !isset($index[$implements]['coid://phpmae.dev/hasDefinitionFile']))
throw new \Exception("<".$implements."> is not a valid phpMAE interface.");
return [
'classname' => COIDParser::getName($interfaceCoid),
'filename' => basename($index[$implements]['coid://phpmae.dev/hasDefinitionFile'][0]['value']),
'coid' => (string)$interfaceCoid,
];
}
private function getAndParseInterfaceCode($interface, $filename) {
// Retrieve interface code
$interfaceCode = shell_exec("cloudobjects attachment:get ".$interface
." ".$filename);
// Run through validator
$validator = new ClassValidator;
$validator->validateInterface($interfaceCode, new IRI($interface));
// Find use statements
$matches = [];
preg_match_all("/use\s+(.+);/", $interfaceCode, $matches);
$use = $matches[1];
// Parse method definitions
// (this is the same algorithm as the DirectoryTemplateVariableGenerator,
// but less filtering on comment string)
$matches = [];
preg_match_all("/(?:\/\*\*((?:[\s\S](?!\/\*))*?)\*\/+\s*)?public\s+function\s+(\w+)\s*\((.+)\)/",
$interfaceCode, $matches);
// The following groups are captured through RegExes:
// 0 - complete definition block
// 1 - comment string
// 2 - method name
// 3 - method parameters
$methods = [];
for ($i = 0; $i < count($matches[0]); $i++) {
// List methods with parameters and comment
$methods[] = [
'name' => $matches[2][$i],
'params' => trim($matches[3][$i]),
'comment' => trim($matches[1][$i])
];
}
return [
'methods' => $methods,
'use' => $use
];
}
protected function execute(InputInterface $input, OutputInterface $output) {
if (!CredentialManager::isConfigured())
throw new \Exception("The 'cloudobjects' CLI tool must be installed and authorized.");
$coid = COIDParser::fromString($input->getArgument('coid'));
if (COIDParser::getType($coid) != COIDParser::COID_VERSIONED
&& COIDParser::getType($coid) != COIDParser::COID_UNVERSIONED)
throw new \Exception("Invalid COID: ".(string)$coid);
$name = COIDParser::getName($coid);
$version = COIDParser::getVersion($coid);
$fullName = isset($version) ? $name.".".$version : $name;
$invokable = $input->getOption('http-invokable');
if ($input->getOption('implements') != null) {
if ($invokable)
throw new \Exception("The 'implements' option cannot be used with 'http-invokable'.");
$implements = $input->getOption('implements');
$output->writeln("Fetching configuration for ".$implements." ...");
$interfaceData = $this->getAndValidateInterfaceConfig($implements);
}
if (!file_exists($fullName.'.xml') || $input->getOption('force')) {
// Create RDF configuration file
$content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n"
. " xmlns:co=\"coid://cloudobjects.io/\"\n"
. " xmlns:phpmae=\"coid://phpmae.dev/\">\n"
. "\n"
. " <phpmae:".($invokable ? "HTTPInvokable" : "")."Class rdf:about=\"".(string)$coid."\">\n";
// Mark as public if option was defined
if ($input->getOption('public'))
$content .= " <co:isVisibleTo rdf:resource=\"coid://cloudobjects.io/Public\" />\n"
. " <co:permitsUsageTo rdf:resource=\"coid://cloudobjects.io/Public\" />\n";
else
$content .= " <co:isVisibleTo rdf:resource=\"coid://cloudobjects.io/Vendor\" />\n";
if (isset($implements))
$content .= " <rdf:type rdf:resource=\"".$interfaceData['coid']."\" />\n";
$content .=" </phpmae:".($invokable ? "HTTPInvokable" : "")."Class>\n"
. "</rdf:RDF>";
file_put_contents($fullName.'.xml', $content);
$output->writeln("Written ".$fullName.".xml.");
} else {
// RDF configuration file already exists
$output->writeln($fullName.".xml already exists.");
}
if (!file_exists($fullName.'.php') || $input->getOption('force') !== false) {
$useStatements = [];
$classVariables = [];
$constructor = '';
if ($input->getOption('autowire') != null) {
$fullyQualifiedClassname = $input->getOption('autowire');
$validator = new ClassValidator;
if (!$validator->isWhitelisted($fullyQualifiedClassname))
throw new PhpMAEException('Cannot autowire non-whitelisted type <'.$fullyQualifiedClassname.'>.');
$useStatements[] = $fullyQualifiedClassname;
$className = substr($fullyQualifiedClassname, strrpos($fullyQualifiedClassname, '\\') + 1);
$variableName = strtolower($className[0]).substr($className, 1);
$classVariables[] = $variableName;
$constructor = " public function __construct(".$className." \$".$variableName.") {\n"
. " \$this->".$variableName." = \$".$variableName.";\n"
. " }\n\n";
}
if (isset($implements)) {
// Retrieve interface code
$output->writeln("Fetching code for ".$interfaceData['coid']." ...");
$parsedInterface = $this->getAndParseInterfaceCode($interfaceData['coid'], $interfaceData['filename']);
// Add use statements
foreach ($parsedInterface['use'] as $u)
$useStatements[] = $u;
}
// Create PHP source file
$content = "<?php\n"
. "\n";
foreach ($useStatements as $u)
$content .= "use ".$u.";\n";
if (count($useStatements) > 0)
$content .= "\n";
$content .= "/**\n"
. " * Implementation for ".(string)$coid."\n"
. (isset($implements) ? " * Using interface ".$interfaceData['coid']."\n" : "")
. " */\n"
. "class ".$name." ".(isset($implements) ? "implements ".$interfaceData['classname']." " : "")
. "{\n"
. "\n";
foreach ($classVariables as $v)
$content .= " private \$".$v.";\n";
if (count($classVariables) > 0)
$content .= "\n"
. $constructor;
if (isset($implements)) {
// Build PHP template with interface methods
foreach ($parsedInterface['methods'] as $m) {
$content .= ($m['comment'] != "" ? " /**\n ".$m['comment']."\n */\n" : "")
. " public function ".$m['name']."(".$m['params'].") {\n"
. " // TODO: Implement this\n"
. " }\n\n";
}
} else {
// Build PHP template for standard or HTTP-invokable class
$content .= ($invokable
? " public function __invoke(\$args) {\n // TODO: Add your code here\n }\n"
: " // TODO: Add methods here ...\n")
. "\n";
}
$content .= "}";
file_put_contents($fullName.'.php', $content);
$output->writeln("Written ".$fullName.".php.");
} else {
$output->writeln($fullName.".php already exists.");
}
if ($input->getOption('confjob')) {
$output->writeln("Calling cloudobjects ...");
passthru("cloudobjects configuration-job:create ".$fullName.".xml");
}
}
}
+72
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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use CloudObjects\SDK\COIDParser;
use CloudObjects\PhpMAE\ClassValidator;
class ClassDeployCommand extends AbstractObjectCommand {
protected function configure() {
$this->setName('class:deploy')
->setDescription('Validates a class for the phpMAE and uploads it into CloudObjects. Updates the configuration if necessary.')
->addArgument('coid', InputArgument::REQUIRED, 'The COID of the object.');
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->requireCloudObjectsCLI();
$this->parse($input->getArgument('coid'));
$this->assertRDF();
if (!in_array('coid://phpmae.dev/Class', $this->rdfTypes)
&& !in_array('coid://phpmae.dev/HTTPInvokableClass', $this->rdfTypes))
throw new \Exception("Object does not have a valid class type.");
$this->assertPHPExists();
// Running validator
$validator = new ClassValidator;
$validator->validate(file_get_contents($this->fullName.'.php'),
$this->coid, $this->getAdditionalTypes());
$output->writeln("Validated successfully, calling cloudobjects ...");
passthru("cloudobjects attachment:put ".(string)$this->coid." ".$this->fullName.".php");
// Updates configuration if necessary
if ($this->ensureFilenameInConfig($output, false))
$this->createConfigurationJob($output);
// Print URL so developer can easily access it
$output->writeln("");
$visibility = isset($object['coid://cloudobjects.io/isVisibleTo'])
? $object['coid://cloudobjects.io/isVisibleTo'][0]['value']
: 'coid://cloudobjects.io/Vendor';
$path = $this->coid->getHost().$this->coid->getPath();
switch ($visibility) {
case "coid://cloudobjects.io/Private":
$output->writeln("<info>Private URL for Class Execution:</info>");
$output->writeln("➡️ http://YOUR_PHPMAE_INSTANCE/".$path);
break;
case "coid://cloudobjects.io/Public":
$output->writeln("<info>Public URL for Class Execution:</info>");
$output->writeln("➡️ https://phpmae.dev/".$path);
break;
case "coid://cloudobjects.io/Vendor":
$output->writeln("<info>Authenticated URL for Class Execution:</info>");
$output->writeln("➡️ https://".$this->coid->getHost().":SECRET@phpmae.dev/".$path);
$output->writeln("");
$output->writeln("To get value for SECRET:");
$output->writeln("➡️ cloudobjects domain-providers:secret ".$this->coid->getHost()." phpmae.dev");
break;
}
$output->writeln("");
}
}
+98
View File
@@ -0,0 +1,98 @@
<?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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use ML\IRI\IRI;
use Guzzle\Http\Exception\BadResponseException;
use CloudObjects\SDK\COIDParser;
use CloudObjects\PhpMAE\ClassValidator, CloudObjects\PhpMAE\TestEnvironmentManager;
class ClassTestEnvCommand extends AbstractObjectCommand {
private $container;
protected function getContainer() {
if (!isset($this->container)) {
$this->container = TestEnvironmentManager::getContainer();
}
return $this->container;
}
protected function configure() {
$this->setName('class:testenv')
->setDescription('Uploads a class into the current test environment.')
->addArgument('coid', InputArgument::REQUIRED, 'The COID of the object.')
->addOption('config', null, InputOption::VALUE_NONE, 'Upload the local configuration of the class to the test environment instead of retrieving it from CloudObjects.')
->addOption('watch', null, InputOption::VALUE_NONE, 'Keep watching for changes of the file and reupload automatically.');
}
private function upload(OutputInterface $output) {
try {
$this->validator->validate(file_get_contents($this->phpFileName),
$this->coid, $this->getAdditionalTypes());
$this->getContainer()->get('testenv.client')->put('/uploadTestenv?type=source&coid='.urlencode((string)$this->coid), [
'body' => file_get_contents($this->phpFileName)
]);
$output->writeln('File uploaded successfully!');
} catch (BadResponseException $e) {
if ($e->getResponse()->getContentType()=='application/json') {
$errorMessage = $e->getResponse()->json();
$output->writeln('<error>'.$errorMessage['error_code'].':</error> '
.$errorMessage['error_message']);
} else
$output->writeln('<error>'.get_class($e).'</error> '.(string)$e->getResponse());
} catch (\Exception $e) {
$output->writeln('<error>'.get_class($e).'</error> '.$e->getMessage());
}
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->requireCloudObjectsCLI();
$container = $this->getContainer();
if (!$container->has('testenv.client'))
throw new \Exception("No test environment configured.");
$this->parse($input->getArgument('coid'));
$this->assertRDF();
if (!in_array('coid://phpmae.dev/Class', $this->rdfTypes)
&& !in_array('coid://phpmae.dev/HTTPInvokableClass', $this->rdfTypes))
throw new \Exception("Object does not have a valid class type.");
$this->assertPHPExists();
// Print URL so developer can easily access it
$output->writeln("<info>Test Environment Base URL for Class Execution:</info>");
$output->writeln("➡️ ".$container->get('testenv.url').$this->coid->getHost()
.$this->coid->getPath());
$output->writeln("");
if ($input->getOption('config')) {
// Upload configuration before uploading implementation
$this->ensureFilenameInConfig($output);
$container->get('testenv.client')->put('/uploadTestenv?type=config&coid='.urlencode((string)$this->coid), [
'body' => file_get_contents($this->xmlFileName)
]);
$output->writeln('Configuration uploaded successfully!');
}
$this->validator = new ClassValidator;
$this->upload($output);
if ($input->getOption('watch')) {
$cmd = $this;
$this->watchPHPFile($output, function() use ($cmd, $output) {
$cmd->upload($output);
});
}
}
}
@@ -0,0 +1,73 @@
<?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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use JsonRpc\Client;
use CloudObjects\PhpMAE\TestEnvironmentManager;
class ClassTestEnvRPCCommand extends AbstractObjectCommand {
private $container;
protected function getContainer() {
if (!isset($this->container)) {
$this->container = TestEnvironmentManager::getContainer();
}
return $this->container;
}
protected function configure() {
$this->setName('class:testenv-rpc')
->setDescription('Executes a JSON-RPC method call on a class in the current test environment.')
->addArgument('coid', InputArgument::REQUIRED, 'The COID of the object.')
->addArgument('method', InputArgument::REQUIRED, 'The method name.')
->addArgument('parameters', InputArgument::IS_ARRAY, 'The parameters for the method.');
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->requireCloudObjectsCLI();
$container = $this->getContainer();
if (!$container->has('testenv.client'))
throw new \Exception("No test environment configured.");
$this->parse($input->getArgument('coid'));
$this->assertRDF();
if (!in_array('coid://phpmae.dev/Class', $this->rdfTypes)
&& !in_array('coid://phpmae.dev/HTTPInvokableClass', $this->rdfTypes))
throw new \Exception("Object does not have a valid class type.");
$this->assertPHPExists();
// Create JSON-RPC Client
$client = new Client($container->get('testenv.url')
.$this->coid->getHost().$this->coid->getPath());
// Check parameters and load files
$parameters = [];
foreach ($input->getArgument('parameters') as $p) {
if ($p[0] == '@')
$parameters[] = @file_get_contents(substr($p, 1));
else
$parameters[] = $p;
}
// Make RPC Call
if ($client->call($input->getArgument('method'), $parameters)
&& isset($client->result))
$output->writeln(is_string($client->result)
? $client->result
: json_encode($client->result, JSON_PRETTY_PRINT));
else
$output->writeln("<error>RPC has failed!</error>");
}
}
+57
View File
@@ -0,0 +1,57 @@
<?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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use CloudObjects\PhpMAE\ClassValidator;
class ClassValidateCommand extends AbstractObjectCommand {
protected function configure() {
$this->setName('class:validate')
->setAliases([ 'validate', 'v' ])
->setDescription('Validates a class for the phpMAE.')
->addArgument('coid', InputArgument::REQUIRED, 'The COID of the object.')
->addOption('watch', null, InputOption::VALUE_NONE, 'Keep watching for changes of the file and revalidate automatically.');
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->parse($input->getArgument('coid'));
$this->assertRDF();
if (!in_array('coid://phpmae.dev/Class', $this->rdfTypes)
&& !in_array('coid://phpmae.dev/HTTPInvokableClass', $this->rdfTypes))
throw new \Exception("Object does not have a valid class type.");
$this->assertPHPExists();
// Running validator
$validator = new ClassValidator;
try {
$validator->validate(file_get_contents($this->phpFileName),
$this->coid, $this->getAdditionalTypes());
$output->writeln("Validated successfully.");
} catch (\Exception $e) {
$output->writeln('<error>'.get_class($e).'</error> '.$e->getMessage());
}
if ($input->getOption('watch')) {
$cmd = $this;
$this->watchPHPFile($output, function() use ($validator, $cmd, $output) {
try {
$validator->validate(file_get_contents($cmd->phpFileName),
$this->coid, $this->getAdditionalTypes());
$output->writeln("Validated successfully.");
} catch (\Exception $e) {
$output->writeln('<error>'.get_class($e).'</error> '.$e->getMessage());
}
});
}
}
}
@@ -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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use CloudObjects\SDK\COIDParser;
class DependenciesAddAttachmentCommand extends AbstractObjectCommand {
protected function configure() {
$this->setName('dependencies:add-attachment')
->setDescription('Adds an attachment to the specification of a class.')
->addArgument('coid-target', InputArgument::REQUIRED, 'The COID of the target class.')
->addArgument('filename', InputArgument::REQUIRED, 'The name of the file that should be attached.')
->addOption('upload', null, InputOption::VALUE_NONE, 'Calls "cloudobjects" to actually upload the file to CloudObjects.');
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->requireCloudObjectsCLI();
$this->parse($input->getArgument('coid-target'));
$this->assertRDF();
$this->assertPHPExists();
$filename = $input->getArgument('filename');
if (!file_exists($filename))
throw new \Exception("File not found: ".$filename);
$fileUri = "file:///".basename($filename);
// Edit configuration
$found = false;
$object = $this->index[(string)$this->coid];
if (isset($object['coid://phpmae.dev/usesAttachedFile'])) {
foreach ($object['coid://phpmae.dev/usesAttachedFile'] as $o) {
if ($o['type'] != 'uri') continue;
if ($o['value'] == $fileUri) $found = true;
}
if (!$found)
$object['coid://phpmae.dev/usesAttachedFile'][] = [ 'type' => 'uri', 'value' => $fileUri ];
} else
$object['coid://phpmae.dev/usesAttachedFile'] = [[ 'type' => 'uri', 'value' => $fileUri ]];
// Persist configuration
if (!$found) {
$this->index[(string)$this->coid] = $object;
$this->updateRDFLocally($output);
}
if ($input->getOption('upload') !== null) {
$output->writeln("Calling cloudobjects ...");
passthru("cloudobjects attachment:put ".(string)$this->coid." ".$filename);
}
}
}
@@ -0,0 +1,78 @@
<?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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use CloudObjects\SDK\COIDParser;
class DependenciesAddClassCommand extends AbstractAddDependenciesCommand {
protected function configure() {
$this->setName('dependencies:add-class')
->setDescription('Adds a class dependency to the specification of a class.')
->addArgument('coid-target', InputArgument::REQUIRED, 'The COID of the target class.')
->addOption('key', null, InputArgument::OPTIONAL, 'The key for dependency injection. Randomly generated if not provided.', null)
->addArgument('coid-class', InputArgument::REQUIRED, 'The COID of the dependency class.');
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->requireCloudObjectsCLI();
$this->parse($input->getArgument('coid-target'));
$this->dependencyPrecheck();
// Check Class
$coidClass = COIDParser::fromString($input->getArgument('coid-class'));
if (COIDParser::getType($coidClass) != COIDParser::COID_VERSIONED
&& COIDParser::getType($coidClass) != COIDParser::COID_UNVERSIONED)
throw new \Exception("Invalid COID: ".$coid);
// Checking type
$output->writeln("Fetching configuration for ".(string)$coidClass." ...");
$this->getObjectAndAssertType((string)$coidClass, 'coid://phpmae.dev/Class');
// Add dependency
$this->addDependency(
$input->getOption('key') ? $input->getOption('key') : uniqid('class-'),
'coid://phpmae.dev/ClassDependency',
[
'coid://phpmae.dev/hasClass' => [
[ 'type' => 'uri', 'value' => (string)$coidClass ]
]
],
$input, $output
);
// Print documentation
$className = COIDParser::getName($coidClass);
$key = $input->getOption('key') ? $input->getOption('key')
: strtolower($className[0]).substr($className, 1);
$output->writeln("");
$output->writeln("<info>Use your class dependency:</info>");
$output->writeln("");
$output->writeln("1) Add the class ".$className." to your constructor parameters.");
$output->writeln("2) Assign the class to \$this->".$key.".");
$output->writeln("");
$output->writeln(" private \$".$key.";");
$output->writeln("");
$output->writeln(" public function __construct(".$className." \$".$key.") {");
$output->writeln(" \$this->".$key." = \$".$key.";");
$output->writeln(" }");
$output->writeln("");
$output->writeln("3) Use \$this->".$key." wherever required.");
$output->writeln("");
$output->writeln("You can find the documentation for the available class methods on this page:");
$output->writeln("➡️ https://cloudobjects.io/".$coidClass->getHost().$coidClass->getPath());
$output->writeln("");
}
}
@@ -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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use CloudObjects\SDK\COIDParser;
class DependenciesAddStaticTextCommand extends AbstractAddDependenciesCommand {
protected function configure() {
$this->setName('dependencies:add-text')
->setDescription('Adds a static text dependency to the specification of a class.')
->addArgument('coid-target', InputArgument::REQUIRED, 'The COID of the target class.')
->addArgument('key', InputArgument::REQUIRED, 'The key for dependency injection.')
->addArgument('value', InputArgument::REQUIRED, 'The text value.');
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->requireCloudObjectsCLI();
$this->parse($input->getArgument('coid-target'));
$this->assertRDF();
$this->dependencyPrecheck();
// Add dependency
$this->addDependency(
$input->getArgument('key'),
'coid://phpmae.dev/StaticTextDependency',
[
'coid://phpmae.dev/hasValue' => [
[ 'type' => 'literal', 'value' => $input->getArgument('value') ]
]
],
$input, $output
);
// Print documentation
$key = $input->getArgument('key');
$output->writeln("");
$output->writeln("<info>Use your static text dependency:</info>");
$output->writeln("");
$output->writeln("1) Make sure you have access to the dependency injection container by adding the container to your class constructor.");
$output->writeln("2) Request the value string from the container using the key \"".$key."\".");
$output->writeln("");
$output->writeln(" private \$".$key.";");
$output->writeln("");
$output->writeln(" public function __construct(\Psr\Container\ContainerInterface \$container) {");
$output->writeln(" \$this->".$key." = \$container->get('".$key."');");
$output->writeln(" }");
$output->writeln("");
$output->writeln("3) Use \$this->".$key." wherever required.");
$output->writeln("");
}
}
@@ -0,0 +1,75 @@
<?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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use CloudObjects\SDK\COIDParser;
class DependenciesAddTemplateCommand extends AbstractAddDependenciesCommand {
protected function configure() {
$this->setName('dependencies:add-template')
->setDescription('Adds a Twig template dependency to the specification of a class.')
->addArgument('coid-target', InputArgument::REQUIRED, 'The COID of the target class.')
->addArgument('key', InputArgument::REQUIRED, 'The key for dependency injection.')
->addArgument('filename', InputArgument::REQUIRED, 'The filename for the Twig template.')
->addOption('upload', null, InputOption::VALUE_NONE, 'Calls "cloudobjects" to actually upload the template file to CloudObjects.');
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->requireCloudObjectsCLI();
$this->parse($input->getArgument('coid-target'));
$this->dependencyPrecheck();
// Check filename
$filename = $input->getArgument('filename');
if (!file_exists($filename))
throw new \Exception("File not found: ".$filename);
// Upload file if requested
if ($input->getOption('upload')) {
$output->writeln("Calling cloudobjects for file upload ...");
passthru("cloudobjects attachment:put ".(string)$this->coid." ".$filename);
}
// Add dependency
$this->addDependency(
$input->getArgument('key'),
'coid://phpmae.dev/TwigTemplateDependency',
[
'coid://phpmae.dev/usesAttachedTwigFile' => [
[ 'type' => 'uri', 'value' => "file:///".basename($filename) ]
]
],
$input, $output
);
// Print documentation
$key = $input->getArgument('key');
$output->writeln("");
$output->writeln("<info>Use your Twig template dependency:</info>");
$output->writeln("");
$output->writeln("1) Make sure you have access to the dependency injection container by adding the container to your class constructor.");
$output->writeln("2) Request the template from the container using the key \"".$key."\".");
$output->writeln("");
$output->writeln(" private \$".$key."Template;");
$output->writeln("");
$output->writeln(" public function __construct(\Psr\Container\ContainerInterface \$container) {");
$output->writeln(" \$this->".$key."Template = \$container->get('".$key."');");
$output->writeln(" }");
$output->writeln("");
$output->writeln("3) Render your template by calling \$this->".$key."Template->render(\$context).");
$output->writeln("");
}
}
@@ -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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use CloudObjects\SDK\COIDParser;
class DependenciesAddWebAPICommand extends AbstractAddDependenciesCommand {
protected function configure() {
$this->setName('dependencies:add-webapi')
->setDescription('Adds a dependency to the specification of a class.')
->addArgument('coid-target', InputArgument::REQUIRED, 'The COID of the target class.')
->addArgument('key', InputArgument::REQUIRED, 'The key for dependency injection.')
->addArgument('coid-webapi', InputArgument::REQUIRED, 'The COID of the Web API.');
parent::configure();
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->requireCloudObjectsCLI();
$this->parse($input->getArgument('coid-target'));
$this->dependencyPrecheck();
// Check Web API
$coidWebAPI = COIDParser::fromString($input->getArgument('coid-webapi'));
if (COIDParser::getType($coidWebAPI) != COIDParser::COID_VERSIONED
&& COIDParser::getType($coidWebAPI) != COIDParser::COID_UNVERSIONED)
throw new \Exception("Invalid COID: ".$coid);
// Checking type
$output->writeln("Fetching configuration for ".(string)$coidWebAPI." ...");
$this->getObjectAndAssertType((string)$coidWebAPI, 'coid://webapis.co-n.net/HTTPEndpoint');
// Add dependency
$this->addDependency(
$input->getArgument('key'),
'coid://phpmae.dev/WebAPIDependency',
[
'coid://phpmae.dev/hasAPI' => [
[ 'type' => 'uri', 'value' => (string)$coidWebAPI ]
]
],
$input, $output
);
// Print documentation
$key = $input->getArgument('key');
$output->writeln("");
$output->writeln("<info>Use your WebAPI dependency:</info>");
$output->writeln("");
$output->writeln("1) Make sure you have access to the dependency injection container by adding the container to your class constructor.");
$output->writeln("2) Request an API client from the container using the key \"".$key."\".");
$output->writeln("");
$output->writeln(" private \$".$key."Api;");
$output->writeln("");
$output->writeln(" public function __construct(\Psr\Container\ContainerInterface \$container) {");
$output->writeln(" \$this->".$key."Api = \$container->get('".$key."');");
$output->writeln(" }");
$output->writeln("");
$output->writeln("3) Make API requests in your class by calling methods on \$this->".$key."Api.");
$output->writeln("");
}
}
@@ -0,0 +1,84 @@
<?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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;
use CloudObjects\SDK\COIDParser;
use CloudObjects\PhpMAE\CredentialManager, CloudObjects\PhpMAE\ClassValidator;
use CloudObjects\PhpMAE\Exceptions\PhpMAEException;
class InterfaceCreateCommand extends Command {
protected function configure() {
$this->setName('interface:create')
->setDescription('Create a new interface for the phpMAE.')
->addArgument('coid', InputArgument::REQUIRED, 'The COID of the object.')
->addOption('force', 'f', InputOption::VALUE_OPTIONAL, 'Forces new object creation and replaces existing files.', false)
->addOption('confjob', null, InputOption::VALUE_OPTIONAL, 'Calls "cloudobjects" to create a configuration job for the new interface.', false);
}
protected function execute(InputInterface $input, OutputInterface $output) {
if (!CredentialManager::isConfigured())
throw new \Exception("The 'cloudobjects' CLI tool must be installed and authorized.");
$coid = COIDParser::fromString($input->getArgument('coid'));
if (COIDParser::getType($coid)!=COIDParser::COID_VERSIONED
&& COIDParser::getType($coid)!=COIDParser::COID_UNVERSIONED)
throw new \Exception("Invalid COID: ".(string)$coid);
$name = COIDParser::getName($coid);
$version = COIDParser::getVersion($coid);
$fullName = isset($version) ? $name.".".$version : $name;
if (!file_exists($fullName.'.xml') || $input->getOption('force') !== false) {
// Create RDF configuration file
$content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
. "<rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n"
. " xmlns:co=\"coid://cloudobjects.io/\"\n"
. " xmlns:phpmae=\"coid://phpmae.dev/\">\n"
. "\n"
. " <phpmae:Interface rdf:about=\"".(string)$coid."\">\n"
. " <co:isVisibleTo rdf:resource=\"coid://cloudobjects.io/Vendor\" />\n"
. " </phpmae:Interface>\n"
. "</rdf:RDF>";
file_put_contents($fullName.'.xml', $content);
$output->writeln("Written ".$fullName.".xml.");
} else {
// RDF configuration file already exists
$output->writeln($fullName.".xml already exists.");
}
if (!file_exists($fullName.'.php') || $input->getOption('force') !== false) {
// Create PHP source file
$content = "<?php\n"
. "\n"
. "/**\n"
. " * Definition for ".(string)$coid."\n"
. " */\n"
. "interface ".$name." {\n"
. "\n"
. " // Add methods here ...\n"
. "\n"
. "}";
file_put_contents($fullName.'.php', $content);
$output->writeln("Written ".$fullName.".php.");
} else {
$output->writeln($fullName.".php already exists.");
}
if ($input->getOption('confjob') !== false) {
$output->writeln("Calling cloudobjects ...");
passthru("cloudobjects configuration-job:create ".$fullName.".xml");
}
}
}
@@ -0,0 +1,50 @@
<?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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use CloudObjects\SDK\COIDParser;
use CloudObjects\PhpMAE\ClassValidator;
class InterfaceDeployCommand extends AbstractObjectCommand {
protected function configure() {
$this->setName('interface:deploy')
->setDescription('Validates an interface for the phpMAE and uploads it into CloudObjects. Updates the configuration if necessary.')
->addArgument('coid', InputArgument::REQUIRED, 'The COID of the object.');
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->parse($input->getArgument('coid'));
$this->assertRDF();
if (!in_array('coid://phpmae.dev/Interface', $this->rdfTypes))
throw new \Exception("Object does not have athevalid interface type.");
$this->assertPHPExists();
// Running validator
$validator = new ClassValidator();
$validator->validateInterface(file_get_contents($this->fullName.'.php'), $this->coid);
$output->writeln("Validated successfully, calling cloudobjects ...");
passthru("cloudobjects attachment:put ".(string)$this->coid." ".$this->fullName.".php");
// Updates configuration if necessary
if ($this->ensureFilenameInConfig($output, true)) {
$this->createConfigurationJob($output);
}
// Print URL so developer can easily access it
$output->writeln("");
$visibility = isset($object['coid://cloudobjects.io/isVisibleTo'])
? $object['coid://cloudobjects.io/isVisibleTo'][0]['value']
: 'coid://cloudobjects.io/Vendor';
$path = $this->coid->getHost().$this->coid->getPath();
}
}
@@ -0,0 +1,53 @@
<?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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use CloudObjects\PhpMAE\ClassValidator;
class InterfaceValidateCommand extends AbstractObjectCommand {
protected function configure() {
$this->setName('interface:validate')
->setDescription('Validates an interface for the phpMAE.')
->addArgument('coid', InputArgument::REQUIRED, 'The COID of the object.')
->addOption('watch', null, InputOption::VALUE_OPTIONAL, 'Keep watching for changes of the file and revalidate automatically.', null);
}
protected function execute(InputInterface $input, OutputInterface $output) {
$this->parse($input->getArgument('coid'));
$this->assertRDF();
if (!in_array('coid://phpmae.dev/Interface', $this->rdfTypes))
throw new \Exception("Object does not have the valid interface type.");
$this->assertPHPExists();
// Running validator
$validator = new ClassValidator;
try {
$validator->validateInterface(file_get_contents($this->phpFileName), $this->coid);
$output->writeln("Validated successfully.");
} catch (\Exception $e) {
$output->writeln('<error>'.get_class($e).'</error> '.$e->getMessage());
}
if ($input->getOption('watch') !== null) {
$cmd = $this;
$this->watchPHPFile($output, function() use ($validator, $cmd, $output) {
try {
$validator->validateInterface(file_get_contents($cmd->phpFileName), $this->coid);
$output->writeln("Validated successfully.");
} catch (\Exception $e) {
$output->writeln('<error>'.get_class($e).'</error> '.$e->getMessage());
}
});
}
}
}
@@ -0,0 +1,54 @@
<?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\PhpMAE\Commands;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Command\Command;
use CloudObjects\PhpMAE\TestEnvironmentManager;
use CloudObjects\PhpMAE\CredentialManager;
class TestEnvironmentStartCommand extends Command {
protected function configure() {
$this->setName('testenv:start')
->setDescription('Starts a local test environment.')
->addOption('port', null, InputOption::VALUE_OPTIONAL, 'Use this port for test environment.', 9000)
->addOption('host', null, InputOption::VALUE_OPTIONAL, 'Bind test environment to this host.', '127.0.0.1');
}
protected function execute(InputInterface $input, OutputInterface $output) {
if (!CredentialManager::isConfigured())
throw new \Exception("The 'cloudobjects' CLI tool must be installed and authorized.");
$port = $input->getOption('port');
$host = $input->getOption('host');
TestEnvironmentManager::setTestEnvironment('http://localhost:'.$port.'/');
$output->writeln('Local webserver started on port '.$port.'.');
$phar = \Phar::running();
if (!$phar || $phar == "") {
// Run extracted version
$dir = realpath(__DIR__."/../../web/");
passthru('cd '.$dir.'; php -S '.$host.':'.$port.' index.php');
} else {
// Run from phar
if (strpos($phar, '.phar') !== false) {
// Run directly
passthru('php -S '.$host.':'.$port.' '.$phar.'/web/index.php');
} else {
// Create copy with .phar extension because PHP might not recognize the file otherwise
$pharCp = sys_get_temp_dir().DIRECTORY_SEPARATOR.'phpmae.phar';
copy(substr($phar, 7), $pharCp);
passthru('php -S '.$host.':'.$port.' phar://'.$pharCp.'/web/index.php');
die("THRU!");
}
}
}
}
+59
View File
@@ -0,0 +1,59 @@
<?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\PhpMAE;
use ML\IRI\IRI;
use CloudObjects\SDK\ObjectRetriever, CloudObjects\SDK\COIDParser,
CloudObjects\SDK\NodeReader;
/**
* The Config Loader helps phpMAE classes to load configuration
* data from different CloudObjects objects.
*/
class ConfigLoader {
private $validSources;
private $retriever;
private $reader;
public function __construct(array $validSources, ObjectRetriever $retriever) {
foreach ($validSources as $v) {
if (!is_a($v, IRI::class))
throw new \Exception("Invalid sources!");
}
$this->validSources = $validSources;
$this->retriever = $retriever;
$this->reader = new NodeReader([]);
}
/**
* Get the value for a key, trying the sources in the order given by priorities.
*/
public function get($key, $priorities, $default = null) {
foreach ($priorities as $p) {
if (isset($this->validSources[$p])) {
$object = $this->retriever->getObject($this->validSources[$p]);
if (!isset($object))
continue;
} else
if (substr($p, -10) == '.namespace' && isset($this->validSources[substr($p, 0, -10)])) {
$object = $this->retriever->getObject(
COIDParser::getNamespaceCOID($this->validSources[substr($p, 0, -10)]));
if (!isset($object))
continue;
}
if (!isset($object))
continue;
$value = $this->reader->getFirstValueString($object, $key);
if (isset($value))
return $value;
}
return $default; // not found
}
}
+73
View File
@@ -0,0 +1,73 @@
<?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\PhpMAE;
use DI;
use Monolog\Logger;
use Psr\Container\ContainerInterface;
use CloudObjects\SDK\ObjectRetriever;
class Configurator {
public static function getContainer(array $config) {
// Object Retriever Definition
$objectRetrieverDefinition = DI\autowire();
$orConstructorParameters = [
'cache_provider' => 'file',
'cache_provider.file.directory' => $config['cache_dir'] . '/config',
'static_config_path' => $config['uploads_dir'] . '/config',
'logger' => DI\get(Logger::class)
];
if (!isset($config['co.auth_ns']) && CredentialManager::isConfigured()) {
// Access Object API via Account Gateway using local developer account
$objectRetrieverDefinition->constructor($orConstructorParameters)
->method('setClient', function() {
return CredentialManager::getAccountContext()->getClient();
}, '/ws/');
} else {
// Access Object API with specified namespace credentials
$objectRetrieverDefinition->constructor(array_merge([
'auth_ns' => $config['co.auth_ns'],
'auth_secret' => $config['co.auth_secret'],
], $orConstructorParameters));
}
// Create Dependency Injection Container
$builder = new DI\ContainerBuilder();
$builder->addDefinitions($config);
$builder->addDefinitions([
Logger::class => function(ContainerInterface $config) {
$logger = new Logger('CO');
switch ($config->get('log.target')) {
case "none":
$logger->pushHandler(new \Monolog\Handler\NullHandler);
break;
case "errorlog":
$logger->pushHandler(new \Monolog\Handler\ErrorLogHandler(0, $config->get('log.level')));
break;
default:
throw new \Exception("Invalid log.target!");
}
return $logger;
},
ObjectRetriever::class => $objectRetrieverDefinition,
ObjectRetrieverPool::class => DI\autowire()
->constructorParameter('baseHostname', @$config['co.auth_ns'])
->constructorParameter('options', [
'cache_provider' => 'file',
'cache_provider.file.directory' => $config['cache_dir'] . '/config',
'static_config_path' => $config['uploads_dir'] . '/config',
'logger' => DI\get(Logger::class)
])
]);
$builder->enableCompilation($config['cache_dir']);
return $builder->build();
}
}
+36
View File
@@ -0,0 +1,36 @@
<?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\PhpMAE;
use Cilex\Application;
use ML\IRI\IRI;
use CloudObjects\SDK\AccountGateway\AccountContext;
class CredentialManager {
private static $context;
private static function getFilename() {
return getenv('HOME').DIRECTORY_SEPARATOR.'.cloudobjects';
}
public static function isConfigured() {
return file_exists(self::getFilename());
}
public static function getAccountContext() {
if (!self::$context) {
if (file_exists(self::getFilename())) {
$data = json_decode(file_get_contents(self::getFilename()), true);
if (isset($data['aauid']) && isset($data['access_token']))
self::$context = new AccountContext(new IRI('aauid:'.$data['aauid']), $data['access_token']);
}
}
return self::$context;
}
}
+32
View File
@@ -0,0 +1,32 @@
<?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\PhpMAE;
use PHPSandbox\PHPSandbox;
use PhpParser\Node, PhpParser\NodeVisitorAbstract;
/**
* Performs additional sandbox validation steps which are specific to phpMAE and
* not implemented in a compatible way in the PHPSandbox library.
*/
class CustomValidationVisitor extends NodeVisitorAbstract {
protected $sandbox;
public function __construct(PHPSandbox $sandbox) {
$this->sandbox = $sandbox;
}
public function leaveNode(Node $node) {
if ($node instanceof Node\Expr\FuncCall) {
if ($node->name instanceof Node\Expr\Variable) {
$this->sandbox->validationError("Sandboxed code attempted to call a variable method!", 0, $node);
}
}
}
}
+316
View File
@@ -0,0 +1,316 @@
<?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\PhpMAE\DI;
use ML\JsonLD\Node;
use ML\IRI\IRI;
use Psr\Container\ContainerInterface;
use Psr\SimpleCache\CacheInterface;
use Psr\Http\Message\RequestInterface, Psr\Http\Message\ResponseInterface;
use DI\ContainerBuilder;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Cache\FilesystemCache;
use Slim\Http\Response;
use Symfony\Component\Mailer\MailerInterface,
Symfony\Component\Mailer\Transport, Symfony\Component\Mailer\Mailer;
use CloudObjects\SDK\ObjectRetriever, CloudObjects\SDK\NodeReader, CloudObjects\SDK\COIDParser;
use CloudObjects\SDK\AccountGateway\AAUIDParser,
CloudObjects\SDK\AccountGateway\AccountContext;
use CloudObjects\SDK\WebAPI\APIClientFactory;
use CloudObjects\SDK\Common\CryptoHelper;
use CloudObjects\PhpMAE\ObjectRetrieverPool, CloudObjects\PhpMAE\ClassRepository,
CloudObjects\PhpMAE\ErrorHandler, CloudObjects\PhpMAE\Engine,
CloudObjects\PhpMAE\ConfigLoader, CloudObjects\PhpMAE\TwigTemplate,
CloudObjects\PhpMAE\TwigTemplateFactory, CloudObjects\PhpMAE\InteractiveRunController;
use CloudObjects\PhpMAE\Exceptions\PhpMAEException;
use CloudObjects\PhpMAE\Injectables\CacheBridge;
/**
* The DependencyInjector returns all the dependencies specified for a PHP class.
*/
class DependencyInjector {
private $retrieverPool;
private $classRepository;
private $container;
public function __construct(ObjectRetrieverPool $retrieverPool, ClassRepository $classRepository,
ContainerInterface $container) {
$this->retrieverPool = $retrieverPool;
$this->classRepository = $classRepository;
$this->container = $container;
}
/**
* Checks whether the request comes from a CloudObjects Account Gateway and,
* if so, configures an AccountContext for the class to use.
*/
private function configureAccountGateway(RequestInterface $request) {
$definitions = [];
if ($request->hasHeader('C-AAUID')
&& $request->hasHeader('C-Access-Token')) {
$definitions[AccountContext::class] = function() use ($request) {
$context = AccountContext::fromPsrRequest($request);
if ($this->container->get('agws.data_cache') == 'file') {
// Enable filesystem cache for account data
$cache = new FilesystemCache($this->container->get('cache_dir') . '/acct');
$context->getDataLoader()->setCache($cache);
}
return $context;
};
}
return $definitions;
}
/**
* Get all dependencies for injection.
*
* @param Node $object The object representing the PHP class.
* @param array $additionalDefinitions Additional definitions to add to the container
*/
public function getDependencies(Node $object, array $additionalDefinitions = []) {
$reader = new NodeReader([
'prefixes' => [ 'phpmae' => 'coid://phpmae.dev/' ]
]);
$dependencies = $reader->getAllValuesNode($object, 'phpmae:hasDependency');
$objectCoid = new IRI($object->getId());
$namespaceCoid = COIDParser::getNamespaceCOID($objectCoid);
if (substr($namespaceCoid->getHost(), -7) == '.phpmae') {
$originalHostname = $this->container->get(InteractiveRunController::class)
->getOriginalHostname($namespaceCoid->getHost());
$realNamespaceCoid = isset($originalHostname) ? new IRI('coid://'.$originalHostname) : $namespaceCoid;
} else
$realNamespaceCoid = $namespaceCoid;
$definitions = [
'cookies' => \DI\create(ArrayCollection::class),
ObjectRetriever::class => function() use ($realNamespaceCoid) {
// Get or create an object retriever with the identity of the namespace of this object
return $this->retrieverPool->getObjectRetriever($realNamespaceCoid->getHost());
},
APIClientFactory::class => function(ContainerInterface $c) use ($realNamespaceCoid) {
// Get an API client factory
return new APIClientFactory($c->get(ObjectRetriever::class), $realNamespaceCoid);
},
DynamicLoader::class => function() {
return new DynamicLoader($this->retrieverPool->getBaseObjectRetriever(),
$this->classRepository);
},
ConfigLoader::class => function(ContainerInterface $c) use ($objectCoid, $additionalDefinitions) {
// Get a ConfigLoader that allows the class to read the configuration
// of various objects, including itself and additional definitions
$configDefinitions = [
'self' => $objectCoid
];
foreach ($additionalDefinitions as $key => $value)
if (is_a($value, IRI::class))
$configDefinitions[$key] = $value;
if (isset($additionalDefinitions[RequestInterface::class])) {
$request = $c->get(RequestInterface::class);
if ($request->hasHeader('C-Accessor'))
$configDefinitions['accessor'] =
new IRI($request->getHeaderLine('C-Accessor'));
}
return new ConfigLoader($configDefinitions, $c->get(ObjectRetriever::class));
},
TwigTemplateFactory::class => function() use ($object) {
return new TwigTemplateFactory($this->classRepository
->getCustomFilesCachePath($object));
},
CryptoHelper::class => function(ContainerInterface $c) use ($realNamespaceCoid) {
return new CryptoHelper($c->get(ObjectRetriever::class), $realNamespaceCoid);
},
CacheInterface::class => function(ContainerInterface $c) use ($namespaceCoid) {
// Enable filesystem cache for custom data
return new CacheBridge(
new FilesystemCache($this->container->get('cache_dir') . '/custom'),
$namespaceCoid->getHost()
);
},
ResponseInterface::class => \DI\autowire(Response::class),
MailerInterface::class => function() use ($object, $realNamespaceCoid, $reader) {
// Find mailer DSN
$mailerDsn = $reader->getFirstValueString($object, 'phpmae:hasClassMailerDSN');
if (!isset($mailerDsn)) {
// Get DSN from namespace
$namespaceObject = $this->retrieverPool->getBaseObjectRetriever()
->getObject($realNamespaceCoid);
$mailerDsn = $reader->getFirstValueString($namespaceObject, 'phpmae:hasGlobalMailerDSN');
}
if (!isset($mailerDsn))
throw new PhpMAEException("Requires class or global mailer DSN.");
if (substr($mailerDsn, 0, 5) != 'smtp:')
throw new PhpMAEException("Mailer DSN must start with smtp:.");
$transport = Transport::fromDsn($mailerDsn);
return new Mailer($transport);
}
];
$definitions = array_merge($definitions, $additionalDefinitions);
if (isset($definitions[RequestInterface::class])) {
$definitions = array_merge($definitions, $this->configureAccountGateway($definitions[RequestInterface::class]));
}
foreach ($dependencies as $d) {
$keyedDependency = null;
if (!$d->getProperty('coid://phpmae.dev/hasKey'))
throw new PhpMAEException("<".$object->getId()."> has an invalid dependency: no key!");
if ($reader->hasType($d, 'phpmae:StaticTextDependency')) {
// Static Text Dependency
$value = $reader->getFirstValueString($d, 'phpmae:hasValue');
if (!isset($value))
throw new PhpMAEException("<".$object->getId()."> has an invalid dependency: StaticTextDependency without value!");
$keyedDependency = $value;
} else
if ($reader->hasType($d, 'phpmae:WebAPIDependency')) {
// Web API Dependency
$apiCoid = $reader->getFirstValueString($d, 'phpmae:hasAPI');
if (isset($apiCoid)) {
// API is available to phpMAE, set up dependency
$keyedDependency = function(ContainerInterface $c) use ($apiCoid, $object) {
return $c->get(APIClientFactory::class)
->getClientWithCOID(new IRI($apiCoid), true);
};
} else {
// API might be a private API that wasn't shared with phpMAE,
// we accept the dependency for now and resolve it with a
// namespace-scoped retriever later
$lookupKey = $reader->getFirstValueString($d, 'phpmae:hasKey');
$keyedDependency = function(ContainerInterface $c) use ($objectCoid,
$reader, $lookupKey, $realNamespaceCoid) {
$object = $c->get(ObjectRetriever::class)->get($objectCoid);
$dependencies = $reader->getAllValuesNode($object, 'phpmae:hasDependency');
foreach ($dependencies as $d) {
if ($reader->getFirstValueString($d, 'phpmae:hasKey') == $lookupKey) {
// Found dependency
$apiCoid = $reader->getFirstValueString($d, 'phpmae:hasAPI');
if (!isset($apiCoid))
throw new PhpMAEException("<".$object->getId()."> has an invalid dependency: WebAPIDependency without API!");
return $c->get(APIClientFactory::class)
->getClientWithCOID(new IRI($apiCoid), true);
}
}
throw new PhpMAEException("<".$object->getId()."> had problems during dependency resolution!");
};
}
} else
if ($reader->hasType($d, 'phpmae:ClassDependency')) {
// Class Dependency
$classCoid = $reader->getFirstValueIRI($d, 'phpmae:hasClass');
if (!isset($classCoid))
throw new PhpMAEException("<".$object->getId()."> has an invalid dependency: ClassDependency without class!");
// TODO: can we optimize this? only create dependency container in inject function
$dependencyContainer = $this->classRepository
->createInstance($this->retrieverPool->getBaseObjectRetriever()->getObject($classCoid), null, [ 'callerClass' => $objectCoid ]);
$keyedDependency = function() use ($dependencyContainer) {
return $dependencyContainer->get(Engine::SKEY);
};
// Also add with classname to allow constructor autowiring
$definitions[$this->classRepository->coidToClassName($classCoid)] = $keyedDependency;
} else
if ($reader->hasType($d, 'phpmae:TwigTemplateDependency')) {
// Twig Template Dependency
$filename = $reader->getFirstValueString($d, 'phpmae:usesAttachedTwigFile');
if (!isset($filename))
throw new PhpMAEException("<".$object->getId()."> has an invalid dependency: TwigTemplateDependency without attachment!");
$keyedDependency = function() use ($object, $objectCoid, $filename) {
$content = $this->retrieverPool->getBaseObjectRetriever()
->getAttachment($objectCoid, $filename);
$cachePath = $this->classRepository->getCustomFilesCachePath($object);
return new TwigTemplate($filename, $content, $cachePath);
};
} else
throw new PhpMAEException("<".$object->getId()."> has an invalid dependency: unknown type!");
$definitions[$reader->getFirstValueString($d, 'phpmae:hasKey')] = $keyedDependency;
}
$attachments = $reader->getAllValuesString($object, 'phpmae:usesAttachedFile');
foreach ($attachments as $a) {
$filename = basename($a);
$definitions[$filename] = function() use ($objectCoid, $filename) {
$content = $this->retrieverPool->getBaseObjectRetriever()
->getAttachment($objectCoid, $filename);
if (substr($filename, -5) == '.json')
$content = json_decode($content, true);
return $content;
};
}
if ($reader->hasProperty($object, 'phpmae:usesStaticAccountContext')) {
$accountDefinition = $reader->getFirstValueNode($object, 'phpmae:usesStaticAccountContext');
if (!$reader->hasProperty($accountDefinition, 'phpmae:hasAAUID')
|| !$reader->hasProperty($accountDefinition, 'phpmae:hasAccessToken'))
throw new PhpMAEException("Incomplete AccountContext definition.");
$definitions[AccountContext::class] = function() use ($accountDefinition, $reader) {
return new AccountContext(
AAUIDParser::fromString($reader->getFirstValueString($accountDefinition,
'phpmae:hasAAUID')),
$reader->getFirstValueString($accountDefinition, 'phpmae:hasAccessToken')
);
};
}
return $definitions;
}
/**
* Get a list of COIDs for class dependencies.
*
* @param Node $object The object representing the PHP class.
*/
public function getClassDependencyList(Node $object) {
$reader = new NodeReader([
'prefixes' => [ 'phpmae' => 'coid://phpmae.dev/' ]
]);
$dependencies = $reader->getAllValuesNode($object, 'phpmae:hasDependency');
$list = [];
foreach ($dependencies as $d) {
if ($reader->hasType($d, 'phpmae:ClassDependency')) {
// Class Dependency
$classCoid = $reader->getFirstValueIRI($d, 'phpmae:hasClass');
if (!isset($classCoid))
throw new PhpMAEException("<".$object->getId()."> has an invalid dependency: ClassDependency without class!");
$list[] = $classCoid;
}
}
return $list;
}
}
+34
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\PhpMAE\DI;
use Psr\Http\Message\RequestInterface;
use CloudObjects\SDK\ObjectRetriever;
use CloudObjects\PhpMAE\Engine, CloudObjects\PhpMAE\ClassRepository;
/**
* The DynamicLoader allows accessing other classes that are not
* statically defined as dependencies.
*/
class DynamicLoader {
private $retriever;
private $repository;
public function __construct(ObjectRetriever $retriever, ClassRepository $repository) {
$this->retriever = $retriever;
$this->repository = $repository;
}
public function get($coid, RequestInterface $request = null) {
$object = $this->retriever->get($coid);
return isset($object)
? $this->repository->createInstance($object, $request)->get(Engine::SKEY)
: null;
}
}
+31
View File
@@ -0,0 +1,31 @@
<?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\PhpMAE\DI;
use Psr\Container\ContainerInterface;
/**
* The SandboxedContainer is a proxy class for a PSR-11 container that
* ensures that only interface methods are available.
*/
class SandboxedContainer implements ContainerInterface {
private $container;
public function __construct(ContainerInterface $container) {
$this->container = $container;
}
public function has($id) {
return $this->container->has($id);
}
public function get($id) {
return $this->container->get($id);
}
}
@@ -0,0 +1,23 @@
<?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\PhpMAE\DI;
use DI\Definition\ObjectDefinition;
use DI\Definition\Source\DefinitionSource, DI\Definition\Source\Autowiring,
DI\Definition\Source\ReflectionBasedAutowiring;
/**
* This is an extension of ReflectionBasedAutowiring that only allows autowiring
* for classes that have a definition. Used for sandboxing containers.
*/
class WhitelistReflectionBasedAutowiring extends ReflectionBasedAutowiring implements DefinitionSource, Autowiring {
public function autowire(string $name, ObjectDefinition $definition = null) {
return isset($definition) ? parent::autowire($name, $definition) : null;
}
}
+323
View File
@@ -0,0 +1,323 @@
<?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\PhpMAE;
use Psr\Http\Message\RequestInterface, Psr\Http\Message\ResponseInterface,
Psr\Http\Message\ServerRequestInterface;
use Psr\Container\ContainerInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Slim\App;
use Slim\Http\Headers, Slim\Http\Request, Slim\Http\Response, Slim\Http\Environment;
use ML\IRI\IRI;
use ML\JsonLD\Node;
use Tuupola\Middleware\HttpBasicAuthentication,
Tuupola\Middleware\CorsMiddleware;
use Relay\Relay;
use Dflydev\FigCookies\SetCookie, Dflydev\FigCookies\FigResponseCookies;
use CloudObjects\SDK\COIDParser, CloudObjects\SDK\NodeReader,
CloudObjects\SDK\ObjectRetriever;
use CloudObjects\SDK\Helpers\SharedSecretAuthentication;
use CloudObjects\PhpMAE\DI\SandboxedContainer;
use CloudObjects\PhpMAE\Exceptions\PhpMAEException;
class Engine implements RequestHandlerInterface {
const SKEY = '__self';
const PREPARE_TIME_LIMIT = 2;
const CLASS_TIME_LIMIT = 5;
const CO_PUBLIC = 'coid://cloudobjects.io/Public';
private $objectRetriever;
private $classRepository;
private $slim;
private $container;
private $reader;
private $object;
private $runClass;
public function __construct(ObjectRetriever $objectRetriever,
ClassRepository $classRepository, App $slim,
ErrorHandler $errorHandler, ContainerInterface $container) {
$this->objectRetriever = $objectRetriever;
$this->classRepository = $classRepository;
$this->slim = $slim;
$this->container = $container;
$this->reader = new NodeReader([
'prefixes' => [
'co' => 'coid://cloudobjects.io/',
'phpmae' => 'coid://phpmae.dev/'
]
]);
register_shutdown_function(function(ErrorHandler $handler) {
$handler->getErrorResponse();
}, $errorHandler); // see: http://stackoverflow.com/questions/4410632/handle-fatal-errors-in-php-using-register-shutdown-function
}
private function isObjectPublic() {
return ($this->reader->hasProperty($this->object, 'co:isVisibleTo')
&& $this->reader->getFirstValueIRI($this->object, 'co:isVisibleTo')
->equals(self::CO_PUBLIC)
&& $this->reader->hasProperty($this->object, 'co:permitsUsageTo')
&& $this->reader->getFirstValueIRI($this->object, 'co:permitsUsageTo')
->equals(self::CO_PUBLIC));
}
private function getCORSMiddleware() {
$defaultConfig = [
'headers.allow' => [ 'Content-Type' ],
'cache' => 600
];
if ($this->container->has('global_cors_origins')
&& $this->container->get('global_cors_origins') == '*')
return new CorsMiddleware($defaultConfig); // every origin is allowed, globally
if ($this->reader->getFirstValueString($this->object, 'phpmae:allowsCORSOrigin') == '*')
return new CorsMiddleware($defaultConfig); // every origin is allowed, by class
$origins = $this->reader->getAllValuesString($this->object, 'phpmae:allowsCORSOrigin');
if ($this->container->has('global_cors_origins'))
$origins = array_merge($origins, explode('|', $this->container->get('global_cors_origins')));
if (count($origins) == 0 || trim($origins[0]) == '')
return null; // no CORS enabled
return new CorsMiddleware(array_merge($defaultConfig, [
'origin' => $origins
])); // configured CORS middleware
}
private function getAuthenticationMiddleware() {
$authSchemes = explode('|', $this->container->get('client_authentication'));
if (in_array('none', $authSchemes))
return null; // no authentication required
if (in_array('none:public_only', $authSchemes)
&& $this->isObjectPublic())
return null; // no authentication required
$object = $this->object;
return new HttpBasicAuthentication([
'secure' => !$this->container->has('client_authentication_must_be_secure')
|| $this->container->get('client_authentication_must_be_secure'),
'realm' => 'phpMAE',
'authenticator' => function($args) use ($object, $authSchemes) {
$authenticated = false;
foreach ($authSchemes as $as) {
if (substr($as, 0, 14) == 'shared_secret:') {
$authResult = (SharedSecretAuthentication::verifyCredentials(
$this->objectRetriever, $args['user'], $args['password'])
== SharedSecretAuthentication::RESULT_OK);
if ($authResult != true)
continue;
if (substr($as, 14) == 'runclass')
$authenticated = (substr($object->getId(), 7, strlen($args['user']) +1) == $args['user'].'/');
else
$authenticated = (substr($as, 14) == 'coid://'.$args['user']);
} elseif (substr($as, 0, 4) != 'none')
throw new PhpMAEException("Unsupported authentication scheme!");
if ($authenticated == true)
break;
}
return $authenticated;
}
]);
}
/**
* Executes an invokable class.
*/
private function executeInvokableClass(RequestInterface $request, $args = null) {
if (is_array($args) && count($args) > 0) {
$input = $args;
} else {
$input = $request->getParsedBody();
if (!is_array($input))
$input = [];
}
set_time_limit($this->container->has('execution_time_limit')
? $this->container->get('execution_time_limit') : self::CLASS_TIME_LIMIT);
$result = $this->runClass->get(self::SKEY)->__invoke($input);
return $this->generateResponse($result);
}
/**
* Generates an response with adequate Content Type based on the format of the content.
* @param mixed $content Content for the body of the response.
*/
public function generateResponse($content) {
$lowercaseContent = is_string($content) ? strtolower($content) : '';
if (!isset($content))
// Empty response
$response = new Response(204);
elseif (is_string($content) && (substr($lowercaseContent, 0, 5) == '<html' || substr($lowercaseContent, 0, 14) == '<!doctype html'))
// HTML response
$response = (new Response)->write($content);
elseif (is_string($content) && (substr($lowercaseContent, 0, 7) == 'http://' || substr($lowercaseContent, 0, 8) == 'https://'))
// Redirect response
$response = (new Response)->withRedirect($content);
elseif (is_string($content) || is_numeric($content))
// Plain text response
$response = (new Response)->withHeader('Content-Type', 'text/plain')->write($content);
elseif (is_object($content) && in_array(ResponseInterface::class, class_implements($content)))
// Existing response to pass through
$response = $content;
else
// JSON response (default)
$response = (new Response)->withJson($content);
// TODO: add support for XML
// Add cookies if any
if (isset($this->runClass) && $this->runClass->has('cookies')) {
foreach ($this->runClass->get('cookies') as $cookie) {
if (is_a($cookie, SetCookie::class))
$response = FigResponseCookies::set($response, $cookie);
}
}
return $response;
}
/**
* Executes a standard class using JSON-RPC.
*/
private function executeJsonRPC(RequestInterface $request) {
$transport = new JsonRPCTransport;
$server = new JsonRPCServer($this->runClass->get(self::SKEY), $transport);
set_time_limit($this->container->has('execution_time_limit')
? $this->container->get('execution_time_limit') : self::CLASS_TIME_LIMIT);
$server->receive((string)$request->getBody());
return $this->generateResponse($transport->getResponse());
}
/**
* Start execution of a request.
*/
public function execute(RequestInterface $request) {
$path = rtrim($request->getUri()->getBasePath().$request->getUri()->getPath(), '/');
switch ($path) {
case "":
case "/":
// Display homepage
$file = ($this->container
->get(InteractiveRunController::class)
->isEnabled()) ? 'app.html' : 'app_disabled.html';
return $this->generateResponse(file_get_contents(__DIR__.'/../static/'.$file));
case "/run":
// Run interactive code request
return $this->container
->get(InteractiveRunController::class)
->handle($request, $this);
case "/uploadTestenv":
// Upload into test environment (if enabled)
return $this->container
->get(UploadController::class)
->handle($request);
default:
if (file_exists(__DIR__.'/../static'.$path)) {
// Proxy for static files
$filename = realpath(__DIR__.'/../static'.$path);
$response = (new Response)->write(file_get_contents($filename));
switch (pathinfo($filename, PATHINFO_EXTENSION)) {
case "css":
return $response->withHeader('Content-Type', 'text/css');
case "js":
return $response->withHeader('Content-Type', 'application/javascript');
default:
return $response;
}
}
$coid = COIDParser::fromString(substr($path, 1));
$this->loadRunClass($coid, $request);
// Process a standard request for a phpMAE class
$queue = [
$this->getCORSMiddleware(),
$this->getAuthenticationMiddleware(),
$this
];
$relay = new Relay(
array_filter($queue, function ($q) {
return $q !== null;
})
);
return $relay->handle($request);
}
}
/**
* Load a class to execute.
*/
public function loadRunClass(IRI $coid, RequestInterface $request = null) {
if (COIDParser::isValidCOID($coid) && COIDParser::getType($coid) != COIDParser::COID_ROOT) {
$this->object = $this->objectRetriever->get($coid);
if (!isset($this->object))
throw new PhpMAEException("The object <" . (string)$coid . "> does not exist or this phpMAE instance is not allowed to access it.");
$this->runClass = $this->classRepository->createInstance($this->object, $request);
} else {
throw new PhpMAEException("You must provide a valid, non-root COID to specify the class for execution.");
}
}
/**
* Specifies the class for execution in the engine.
*/
public function setRunClass(DI\SandboxedContainer $runClass) {
$this->runClass = $runClass;
}
public function handle(ServerRequestInterface $request, $args = null): ResponseInterface {
try {
if (ClassValidator::isInvokableClass($this->runClass->get(self::SKEY))) {
// Run as invokable class
return $this->executeInvokableClass($request, $args);
} else {
// Run as RPC
// JsonRPC
return $this->executeJsonRPC($request);
}
} catch (\Exception $e) {
throw new PhpMAEException(get_class($e).": ".$e->getMessage());
}
}
/**
* Create main request and execute.
*/
public function run() {
set_time_limit(self::PREPARE_TIME_LIMIT);
$env = new Environment($_SERVER);
$request = Request::createFromEnvironment($env);
try {
$response = $this->execute($request);
} catch (PhpMAEException $e) {
// Create plain-text error response
$response = (new Response(500))
->withHeader('Content-Type', 'text/plain')
->write($e->getMessage());
}
$this->slim->respond($response);
}
}
+51
View File
@@ -0,0 +1,51 @@
<?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\PhpMAE;
use Slim\App;
use Slim\Http\Response;
use ML\JsonLD\Node;
use CloudObjects\SDK\ObjectRetriever;
class ErrorHandler {
private $classMap = [];
private $slim;
private $responseGenerator;
public function __construct(App $slim) {
$this->slim = $slim;
ini_set("display_errors", 0);
$this->setResponseGenerator(function($error, $object) {
$message = substr($error['message'], 0, strpos($error['message'], ' in /'));
return (new Response(500))
->withHeader('Content-Type', 'text/plain')
->write("Error in implementation of <".$object->getId()."> at revision "
. $object->getProperty(ObjectRetriever::REVISION_PROPERTY)->getValue()
. ":\n".$message);
});
}
public function addMapping($filename, Node $object) {
$this->classMap[realpath($filename)] = $object;
}
public function setResponseGenerator(callable $responseGenerator) {
$this->responseGenerator = $responseGenerator;
}
public function getErrorResponse() {
$error = error_get_last();
if ($error !== null && in_array($error['type'], [ E_ERROR, E_COMPILE_ERROR ])
&& isset($this->classMap[$error['file']])) {
$response = call_user_func($this->responseGenerator, $error, $this->classMap[$error['file']]);
$this->slim->respond($response);
}
}
}
+13
View File
@@ -0,0 +1,13 @@
<?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\PhpMAE\Exceptions;
class PhpMAEException extends \Exception {
// provides no further implementation except for what was inherited
}
+74
View File
@@ -0,0 +1,74 @@
<?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\PhpMAE\Injectables;
use Psr\SimpleCache\CacheInterface, Psr\SimpleCache\CacheException;
use Psr\Http\Message\MessageInterface;
use Doctrine\Common\Cache\Cache;
use GuzzleHttp\Psr7;
class CacheBridge implements CacheInterface {
private $cache;
private $prefix;
public function __construct(Cache $cache, string $prefix) {
$this->cache = $cache;
$this->prefix = $prefix;
}
public function get($key, $default = null) {
$value = $this->cache->fetch($this->prefix.$key);
if (is_array($value) && isset($value['container']) && isset($value['body'])
&& is_object($value['container']) && is_a($value['container'], MessageInterface::class)
&& is_string($value['body'])) {
// Convert message interface back
$stream = Psr7\stream_for($value['body']);
$value = $value['container']
->withBody($stream);
}
return ($value === false ? $default : $value);
}
public function set($key, $value, $ttl = null) {
if (is_object($value) && is_a($value, MessageInterface::class)) {
// Read full stream for caching
$stream = $value->getBody();
$value = [
'container' => $value,
'body' => $stream->getContents()
];
$stream->rewind();
}
return $this->cache->save($this->prefix.$key, $value, $ttl);
}
public function delete($key) {
throw new CacheException("This method is not implemented.");
}
public function clear() {
throw new CacheException("This method is not implemented.");
}
public function getMultiple($keys, $default = null) {
throw new CacheException("This method is not implemented.");
}
public function setMultiple($values, $ttl = null) {
throw new CacheException("This method is not implemented.");
}
public function deleteMultiple($keys) {
throw new CacheException("This method is not implemented.");
}
public function has($key) {
return $this->cache->contains($this->prefix.$key);
}
}
+172
View File
@@ -0,0 +1,172 @@
<?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\PhpMAE;
use Exception;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\RequestInterface;
use ML\JsonLD\JsonLD;
use CloudObjects\SDK\COIDParser;
use CloudObjects\SDK\AccountGateway\AccountContext, CloudObjects\SDK\AccountGateway\AAUIDParser;
use CloudObjects\Utilities\RDF\Arc2JsonLdConverter;
use GuzzleHttp\Psr7\ServerRequest;
use CloudObjects\PhpMAE\Exceptions\PhpMAEException;
class InteractiveRunController {
private $classRepository;
private $session;
private $mapBack = [];
private $accountContext;
private $domains;
public function __construct(ClassRepository $classRepository,
ContainerInterface $container) {
$this->classRepository = $classRepository;
$this->container = $container;
}
public function isEnabled() {
return ($this->container->has('interactive_run')
&& $this->container->get('interactive_run') === true);
}
public function getOriginalHostname($sessionHostname) {
if (!isset($this->accountContext)) {
// We cannot access original hostnames for anonymous users
return null;
}
if (!isset($this->domains)) {
// Get domains that the user has access to
$apiResponse = json_decode($this->accountContext->getClient()->get('dr/')
->getBody(), true);
$this->domains = isset($apiResponse['domains']) ? $apiResponse['domains'] : [];
}
$hostname = @$this->mapBack[$sessionHostname];
return (isset($hostname) && in_array($hostname, $this->domains))
? $hostname : null;
}
private function createTemporaryClassObject($className, $config) {
// Parse RDF/XML config
$parser = \ARC2::getRDFXMLParser();
$parser->parse('', $config);
$index = $parser->getSimpleIndex(false);
$rewrittenIndex = [];
$targetName = '';
foreach ($index as $subject => $data) {
$coid = COIDParser::fromString($subject);
if (COIDParser::getName($coid) == $className) {
// Rewrite namespace to not interfere with production classes
$targetName = 'coid://'.$this->session.'.phpmae'.$coid->getPath();
$this->mapBack[$this->session.'.phpmae'] = $coid->getHost();
$rewrittenIndex[$targetName] = $data;
} else
$rewrittenIndex[$subject] = $data;
}
if (empty($targetName))
throw new Exception("Could not find configuration for <".$className.">.");
// Convert to JsonLD as required by the CloudObjects SDK
$document = JsonLD::getDocument(
JsonLD::fromRdf(Arc2JsonLdConverter::indexToQuads($rewrittenIndex))
);
// Find object
$node = $document->getGraph()->getNode($targetName);
if (!isset($node))
throw new Exception("Invalid object configuration.");
return $node;
}
public function handle(RequestInterface $request, Engine $engine) {
if (!$this->isEnabled())
throw new PhpMAEException("This is not an environment with interactive run enabled.");
if ($request->getMethod() != 'POST')
throw new PhpMAEException("Must use POST for run requests.");
$runData = $request->getParsedBody();
$this->session = isset($runData['session'])
? $runData['session']
: uniqid();
if (isset($runData['aauid']) && isset($runData['access_token'])) {
// We have user credentials
$this->accountContext = new AccountContext(
AAUIDParser::fromString($runData['aauid']), $runData['access_token']
);
}
try {
$object = $this->createTemporaryClassObject($runData['class'], $runData['config']);
$engine->setRunClass($this->classRepository
->createInstance($object, $request, [], $runData['sourceCode'])
);
} catch (Exception $e) {
// Catch validation errors
return $engine->generateResponse([
'status' => 'error',
'content' => $e->getMessage(),
'session' => $this->session
]);
}
$rpcBody = json_encode([
'jsonrpc' => '2.0',
'method' => $runData['method'],
'params' => $runData['params'],
'id' => 'PG'
]);
// Update error handler for response format required by client
$this->container->get(ErrorHandler::class)
->setResponseGenerator(function($error, $object) use ($engine) {
$message = substr($error['message'], 0, strpos($error['message'], ' in /'));
return $engine->generateResponse([
'status' => 'error',
'content' => $message,
'session' => $this->session
]);
});
try {
$innerRequest = new ServerRequest('POST', $request->getUri(),
$request->getHeaders(), $rpcBody);
$innerResponse = $engine->handle($innerRequest);
$rpcResponse = json_decode($innerResponse->getBody(), true);
if (isset($rpcResponse['result']))
return $engine->generateResponse([
'status' => 'success',
'content' => $rpcResponse['result'],
'session' => $this->session
]);
else
return $engine->generateResponse([
'status' => 'error',
'content' => $rpcResponse['error']['message'],
'session' => $this->session
]);
} catch (PhpMAEException $e) {
return $engine->generateResponse([
'status' => 'error',
'content' => $e->getMessage(),
'session' => $this->session
]);
}
}
}
+459
View File
@@ -0,0 +1,459 @@
<?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\PhpMAE;
use Psr\Http\Message\ResponseInterface;
use JsonRpc\Base\Rpc;
use JsonRpc\Base\Request;
use JsonRpc\Base\Response;
/**
* This is a modified version of JsonRPC\Server with custom enhancements for phpMAE.
*/
class JsonRPCServer
{
private $handler;
private $transport = null;
private $logger = null;
private $assoc = false;
private $requests = array();
private $responses = array();
private $error = null;
private $handlerError = null;
private $refClass = null;
public function __construct($methodHandler, $transport = null)
{
ini_set('display_errors', '0');
$this->handler = $methodHandler;
$this->transport = $transport;
if (!$this->transport)
{
$this->transport = new JsonRPCTransport;
}
}
public function receive($input = '')
{
$this->init();
try
{
$input = $input ?: $this->transport->receive();
$json = $this->process($input);
$this->transport->reply($json);
}
catch (\Exception $e)
{
$this->logException($e);
exit;
}
}
public function setTransport($transport)
{
$this->transport = $transport;
}
public function setLogger($logger)
{
$this->logger = $logger;
}
public function setObjectsAsArrays()
{
$this->assoc = true;
}
private function process($json)
{
if (!$struct = Rpc::decode($json, $batch))
{
$code = is_null($struct) ? Rpc::ERR_PARSE : Rpc::ERR_REQUEST;
$data = new Response();
$data->createStdError($code);
return $data->toJson();
}
$this->getRequests($struct);
$this->processRequests();
if (count($this->responses) == 1 && is_object($this->responses[0]))
return $this->responses[0];
$data = implode(',', $this->responses);
return $batch && $data ? '[' . $data . ']' : $data;
}
private function getRequests($struct)
{
if (is_array($struct))
{
foreach ($struct as $item)
{
$this->requests[] = new Request($item);
}
}
else
{
$this->requests[] = new Request($struct);
}
}
private function processRequests()
{
foreach ($this->requests as $request)
{
# check if we got an error parsing the request, otherwise process it
if ($request->fault)
{
$this->error = array(
'code' => Rpc::ERR_REQUEST,
'data' => $request->fault
);
# we always response to request errors
$this->addResponse($request, null);
continue;
}
$result = $this->processRequest($request->method, $request->params);
if (!$request->notification)
{
$this->addResponse($request, $result);
}
}
}
private function processRequest($method, $params)
{
$this->error = null;
if (!$callback = $this->getCallback($method))
{
$this->error = Rpc::ERR_METHOD;
return;
}
if (!$this->checkMethod($method, $params))
{
$this->error = Rpc::ERR_PARAMS;
return;
}
if ($this->assoc)
{
$this->castObjectsToArrays($params);
}
try
{
$result = call_user_func_array($callback, $params);
}
catch (\Exception $e)
{
$this->logException($e);
$this->error = Rpc::ERR_INTERNAL;
return;
}
if ($this->error = $this->getHandlerError())
{
$this->clearHandlerError();
}
return $result;
}
private function addResponse($request, $result)
{
if (is_object($result) && is_a($result, ResponseInterface::class)) {
$this->responses[] = $result;
return;
}
$ar = array(
'id' => $request->id
);
if ($this->error)
{
$ar['error'] = $this->error;
}
else
{
$ar['result'] = $result;
}
$response = new Response();
if (!$response->create($ar))
{
$this->logError($response->fault);
$response->createStdError(Rpc::ERR_INTERNAL, $request->id);
}
$this->responses[] = $response->toJson();
}
private function getCallback($method)
{
$callback = array($this->handler, $method);
if (is_callable($callback))
{
return $callback;
}
}
private function checkMethod($method, &$params)
{
try
{
if (!$this->refClass)
{
# we have already checked that handler is callable
$this->refClass = new \ReflectionClass($this->handler);
try
{
$prop = $this->refClass->getProperty('error');
if ($prop->isPublic())
{
$this->handlerError = $prop;
}
}
catch (\Exception $e){}
}
try
{
$refMethod = $this->refClass->getMethod($method);
}
catch (\Exception $e)
{
# we know we are callable, so the class must be implementing __call or __callStatic
$params = $this->getParams($params);
return true;
}
$res = true;
if (is_object($params))
{
$named = (array) $params;
$params = array();
$refParams = $refMethod->getParameters();
foreach ($refParams as $arg)
{
$argName = $arg->getName();
if (array_key_exists($argName, $named))
{
$params[] = $named[$argName];
unset($named[$argName]);
}
elseif (!$arg->isOptional())
{
$res = false;
break;
}
}
if ($extra = array_values($named))
{
$params = array_merge($params, $extra);
}
}
else
{
$params = $this->getParams($params);
$reqArgs = $refMethod->getNumberOfRequiredParameters();
$res = count($params) >= $reqArgs;
}
return $res;
}
catch (\Exception $e) {}
}
private function getParams($params)
{
if (is_object($params))
{
$params = array_values((array) $params);
}
elseif (is_null($params))
{
$params = array();
}
return $params;
}
private function castObjectsToArrays(&$params)
{
foreach ($params as &$param)
{
if (is_object($param))
{
$param = (array) $param;
}
}
}
private function getHandlerError()
{
if ($this->handlerError)
{
if ($this->handlerError->isStatic())
{
return $this->handlerError->getValue();
}
else
{
return $this->handlerError->getValue($this->handler);
}
}
}
private function clearHandlerError()
{
if ($this->handlerError)
{
if ($this->handlerError->isStatic())
{
return $this->handlerError->setValue(null);
}
else
{
return $this->handlerError->setValue($this->handler, null);
}
}
}
private function logException(\Exception $e)
{
$message = 'Exception: '. $e->getMessage();
$message .= ' in ' . $e->getFile() . ' on line ' . $e->getLine();
$this->logError($message);
}
private function logError($message)
{
try
{
if ($this->logger)
{
$callback = array($this->logger, 'addRecord');
$params = array(
500,
$message
);
$result = call_user_func_array($callback, $params);
}
else
{
error_log($message);
}
}
catch (\Exception $e)
{
error_log($e->__toString());
}
}
private function init()
{
$this->requests = array();
$this->responses = array();
$this->error = null;
$this->handlerError = null;
$this->refClass = null;
}
}
+38
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\PhpMAE;
use Psr\Http\Message\ResponseInterface;
use Slim\Http\Response;
/**
* The JsonRPCTransport writes JsonRPC output into a Slim response.
*/
class JsonRPCTransport {
private $response;
public function reply($data) {
if (is_a($data, ResponseInterface::class))
$this->response = $data->withHeader('C-PhpMae-Passthru', '1');
else
$this->response = (new Response(200))
->withHeader('Content-Type', 'application/json')
->write($data);
}
public function receive() {
return "";
}
/**
* Get the response
*/
public function getResponse() {
return $this->response;
}
}
+61
View File
@@ -0,0 +1,61 @@
<?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\PhpMAE;
use GuzzleHttp\Client;
use CloudObjects\SDK\ObjectRetriever;
class ObjectRetrieverPool {
private $baseObjectRetriever;
private $baseHostname;
private $options;
private $objectRetrievers = [];
public function __construct(ObjectRetriever $baseObjectRetriever, $baseHostname, $options) {
$this->baseObjectRetriever = $baseObjectRetriever;
$this->baseHostname = $baseHostname;
$this->objectRetrievers[$baseHostname] = $baseObjectRetriever;
$this->options = $options;
}
public function getBaseObjectRetriever() {
return $this->baseObjectRetriever;
}
public function getObjectRetriever($hostname) {
if (!isset($this->baseHostname))
// Never create a new retriever when using developer credentials
return $this->baseObjectRetriever;
if (!isset($this->objectRetrievers[$hostname])) {
if (substr($hostname, -7) == '.phpmae') {
// Get an anonymous retriever
$retriever = new ObjectRetriever(array_merge($this->options, [
'auth_ns' => '',
'auth_secret' => '',
'cache_prefix' => 'clobj:'.$hostname.':',
'logger' => $this->options['logger']->withName('CO-'.$hostname)
]));
} else {
// Get a retriever with namespace impersonation
$config = $this->baseObjectRetriever->getClient()->getConfig();
$config['headers']['C-Act-As'] = $hostname;
$retriever = new ObjectRetriever(array_merge($this->options, [
'cache_prefix' => 'clobj:'.$hostname.':',
'logger' => $this->options['logger']->withName('CO-'.$hostname)
]));
$retriever->setClient(new Client($config));
}
$this->objectRetrievers[$hostname] = $retriever;
}
return $this->objectRetrievers[$hostname];
}
}
+302
View File
@@ -0,0 +1,302 @@
<?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\PhpMAE;
use InvalidArgumentException;
use Psr\Http\Message\RequestInterface, Psr\Http\Message\ResponseInterface;
use Psr\Container\ContainerInterface;
use Slim\App;
use Tuupola\Middleware\CorsMiddleware;;
use Slim\Http\Environment, Slim\Http\Uri, Slim\Http\Response,
Slim\Http\Request, Slim\Http\Headers;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\BadResponseException;
use GuzzleHttp\Psr7\ServerRequest;
use ML\IRI\IRI;
use ML\JsonLD\Node, ML\JsonLD\JsonLD;
use CloudObjects\Utilities\RDF\Arc2JsonLdConverter;
use CloudObjects\SDK\NodeReader, CloudObjects\SDK\ObjectRetriever;
use CloudObjects\SDK\JSON\SchemaValidator;
use CloudObjects\PhpMAE\Exceptions\PhpMAEException;
use CloudObjects\PhpMAE\ObjectRetrieverPool;
class Router {
const STATIC_CACHE_TTL = 60;
private $engine;
private $objectRetriever;
private $container;
public function __construct(Engine $engine, ObjectRetriever $objectRetriever,
ContainerInterface $container) {
$this->engine = $engine;
$this->objectRetriever = $objectRetriever;
$this->container = $container;
}
private function getRequestHeaders(Request $request) {
$headers = new Headers; // this instance is required to access the "normalizeKey" method
$output = [];
foreach (array_keys($request->getHeaders()) as $key) {
$key = $headers->normalizeKey($key);
if (substr($key, 0, 9) == 'c-phpmae-' || $key == 'host') {
// do not forward "Host" header or custom phpMAE headers
continue;
}
$output[$key] = $request->getHeaderLine($key);
}
return $output;
}
private function configure(App $app, Node $object) {
$reader = new NodeReader([
'prefixes' => [
'co' => 'coid://cloudobjects.io/',
'phpmae' => 'coid://phpmae.dev/',
'wa' => 'coid://webapis.co-n.net/',
'agws' => 'coid://aauid.net/',
]
]);
if ($reader->hasProperty($object, 'phpmae:enableCORSWithOrigin')) {
// Add CORS support to router
$origins = $reader->getAllValuesString($object, 'phpmae:enableCORSWithOrigin');
$app->add(new CorsMiddleware([
'origin' => $origins,
'headers.allow' => [ 'Content-Type' ],
'credentials' => true,
'cache' => 600
]));
}
$routes = array_merge(
$reader->getAllValuesNode($object, 'phpmae:hasRoute'),
$reader->getAllValuesNode($object, 'agws:hasMethod')
);
$router = $this;
$engine = $this->engine;
$retriever = $this->getObjectRetriever(new IRI($object->getId()));
foreach ($routes as $r) {
if (!$reader->hasProperty($r, 'wa:hasVerb') || !$reader->hasProperty($r, 'wa:hasPath'))
throw new PhpMAEException("Incomplete route configuration! Routes must have wa:hasVerb and wa:hasPath.");
$app->map([ $reader->getFirstValueString($r, 'wa:hasVerb') ],
$reader->getFirstValueString($r, 'wa:hasPath'),
function(RequestInterface $request, ResponseInterface $response, $args) use ($r, $reader, $router, $engine, $retriever, $object) {
if ($reader->hasProperty($r, 'phpmae:runsClass')) {
try {
$params = [];
if ($reader->hasProperty($r, 'wa:hasJSONBodyWithSchema')) {
// Validate request against schema
$schemaValidator = new SchemaValidator($retriever);
$schemaValidator->validateAgainstCOID($request->getParsedBody(),
$reader->getFirstValueIRI($r, 'wa:hasJSONBodyWithSchema'));
// Then, map to single argument
$params = [ $request->getParsedBody() ];
} elseif (is_array($args) && count($args) > 0) {
// Use path arguments as params
$params = $args;
} elseif (is_array($request->getParsedBody())) {
// Use body as params
$params = $request->getParsedBody();
} elseif (is_array($request->getQueryParams())) {
// Use query as params
$params = $request->getQueryParams();
}
} catch (InvalidArgumentException $e) {
return (new Response(400))->withJson([
'error' => 'InputValidationFailed',
'message' => $e->getMessage()
]);
}
// Add static parameters from Router
foreach ($reader->getAllValuesNode($r, 'phpmae:includeParameterWithDefault') as $p) {
if (!$reader->hasProperty($p, 'wa:hasKey') || !$reader->hasProperty($p, 'wa:hasDefaultValue')) {
// Ignore incomplete parameter
continue;
}
if ($reader->hasType($p, 'wa:HeaderParameter')) {
// Add custom header
$request = $request->withHeader(
'HTTP_'.strtoupper($reader->getFirstValueString($p, 'wa:hasKey')),
$reader->getFirstValueString($p, 'wa:hasDefaultValue')
);
}
}
// Route is mapped to a class
$engine->loadRunClass($reader->getFirstValueIRI($r, 'phpmae:runsClass'), $request);
if ($reader->hasProperty($r, 'phpmae:runsMethod')) {
// Calls to specific methods must be rewritten
// Path, request and query parameters are merged and used as method input
$rpcBody = json_encode([
'jsonrpc' => '2.0',
'method' => $reader->getFirstValueString($r, 'phpmae:runsMethod'),
'params' => $params,
'id' => 'R'
]);
$innerRequest = new ServerRequest('POST', $request->getUri(),
$request->getHeaders(), $rpcBody);
$innerResponse = $engine->handle($innerRequest);
if ($innerResponse->hasHeader('C-PhpMae-Passthru'))
return $innerResponse->withoutHeader('C-PhpMae-Passthru');
$rpcResponse = json_decode($innerResponse->getBody(), true);
if (isset($rpcResponse['result'])) {
return $engine->generateResponse($rpcResponse['result']);
} else {
return (new Response(500))->withJson($rpcResponse);
}
} else {
// Generic class execution (invokable)
return $engine->handle($request, $params);
}
} elseif ($reader->hasProperty($r, 'phpmae:redirectsToURL')) {
// Route to redirect to a specific external URL
return (new Response)->withRedirect($reader->getFirstValueString($r, 'phpmae:redirectsToURL'));
} elseif ($reader->hasProperty($r, 'phpmae:redirectsToBaseURL')) {
// Route to redirect to an external base URL
$baseUrl = $reader->getFirstValueIRI($r, 'phpmae:redirectsToBaseURL');
$uri = $request->getUri();
$targetUrl = (string)$baseUrl->resolve(substr($uri->getPath(), 1) . ($uri->getQuery() != '' ? '?'.$uri->getQuery() : ''));
return (new Response)->withRedirect($targetUrl);
} elseif ($reader->hasProperty($r, 'phpmae:proxiesRequestsToBaseURL')) {
// Route to proxy requests to an external URL
$client = new Client([
'base_uri' => $reader->getFirstValueString($r, 'phpmae:proxiesRequestsToBaseURL')
]);
$uri = $request->getUri();
try {
return $client->request($request->getMethod(),
$uri->getPath() . ($uri->getQuery() != '' ? '?'.$uri->getQuery() : ''), [
'headers' => $router->getRequestHeaders($request),
'body' => $request->getBody()
]);
} catch (BadResponseException $e) {
return $e->getResponse();
}
} elseif ($reader->hasProperty($r, 'phpmae:servesStaticFileAttachment')) {
// Rule to serve an attached file
$etag = '"'.md5($reader->getFirstValueString($object, 'co:isAtRevision')
.'+'.$reader->getFirstValueString($r, 'phpmae:servesStaticFileAttachment')).'"';
if ($request->hasHeader('If-None-Match') && strpos($request->getHeaderLine('If-None-Match'), $etag) > -1) {
// Can use cached version
return (new Response(304))->withHeader('ETag', $etag)
->withHeader('Cache-Control', 'max-age='.self::STATIC_CACHE_TTL.', public');
}
$attachmentContent = $retriever->getAttachment(new IRI($object->getId()),
$reader->getFirstValueString($r, 'phpmae:servesStaticFileAttachment'));
if (!isset($attachmentContent))
return (new Response(501))->write("Requested static file attachment cannot be found.");
return $engine->generateResponse($attachmentContent)
->withHeader('ETag', $etag)
->withHeader('Cache-Control', 'max-age='.self::STATIC_CACHE_TTL.', public');
} else {
// Route has no implementation
return (new Response(501))->write("Route implementation not available or no access granted.");
}
});
}
}
private function getObjectRetriever(IRI $coid) {
return $this->container->get(ObjectRetrieverPool::class)
->getObjectRetriever($coid->getHost());
}
private function getRouter(IRI $coid) {
return $this->getObjectRetriever($coid)
->get($coid);
}
/**
* Setup and run router.
*/
public function run() {
try {
$modes = explode('|', $this->container->get('mode'));
$fallback = false;
$configuration = [
'settings' => [
'displayErrorDetails' => true,
],
];
$c = new \Slim\Container($configuration);
$app = new App($c);
$router = null;
$env = new Environment($_SERVER);
foreach ($modes as $m) {
switch ($m) {
case 'router:vhost':
if ($router == null) {
$uri = Uri::createFromEnvironment($env);
if ($uri->getHost() == 'localhost' || filter_var($uri->getHost(), FILTER_VALIDATE_IP) !== false) continue;
$namespace = $this->objectRetriever->get('coid://'.$uri->getHost());
if (isset($namespace) && $routerCoid = $namespace->getProperty('coid://phpmae.dev/hasRouter'))
$router = $this->getRouter(new IRI($routerCoid->getId()));
}
break;
case 'router:header':
if ($router == null) {
$request = Request::createFromEnvironment($env);
if ($request->hasHeader('C-PhpMae-Router-COID'))
$router = $this->getRouter(new IRI($request->getHeaderLine('C-PhpMae-Router-COID')));
}
break;
case 'default':
$fallback = true;
break;
default:
if (substr($m, 0, 14) == 'router:coid://')
$router = $this->getRouter(new IRI(substr($m, 7)));
else
throw new PhpMAEException("Unsupported mode!");
}
}
if (isset($router)) {
$this->configure($app, $router);
$response = $app->run(true);
} elseif ($fallback) {
$this->container->get(Engine::class)
->run();
exit;
} else {
throw new PhpMAEException("No router found!");
}
} catch (PhpMAEException $e) {
// Create plain-text error response
$response = (new Response(500))
->withHeader('Content-Type', 'text/plain')
->write($e->getMessage());
}
$app->respond($response);
}
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,403 @@
<?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\PhpMAE\Sandbox;
use PhpParser\Node, PhpParser\NodeVisitorAbstract;
use PHPSandbox\PHPSandbox, PHPSandbox\ValidatorVisitor,
PHPSandbox\Error;
class CustomizedValidatorVisitor extends ValidatorVisitor {
public function leaveNode(Node $node){
if($node instanceof Node\Arg){
return new Node\Expr\FuncCall(new Node\Name\FullyQualified(($node->value instanceof Node\Expr\Variable) ? 'PHPSandbox\\wrapByRef' : 'PHPSandbox\\wrap'), [$node, new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox')], $node->getAttributes());
} else if($node instanceof Node\Stmt\InlineHTML){
if(!$this->sandbox->allow_escaping){
$this->sandbox->validationError("Sandboxed code attempted to escape to HTML!", Error::ESCAPE_ERROR, $node);
}
}else if($node instanceof Node\Expr\Cast){
if(!$this->sandbox->allow_casting){
$this->sandbox->validationError("Sandboxed code attempted to cast!", Error::CAST_ERROR, $node);
}
if($node instanceof Node\Expr\Cast\Int_){
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_intval', [new Node\Arg($node->expr)], $node->getAttributes());
} else if($node instanceof Node\Expr\Cast\Double){
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_floatval', [new Node\Arg($node->expr)], $node->getAttributes());
} else if($node instanceof Node\Expr\Cast\Bool_){
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_boolval', [new Node\Arg($node->expr)], $node->getAttributes());
} else if($node instanceof Node\Expr\Cast\Array_){
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_arrayval', [new Node\Arg($node->expr)], $node->getAttributes());
} else if($node instanceof Node\Expr\Cast\Object_){
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_objectval', [new Node\Arg($node->expr)], $node->getAttributes());
}
} else if($node instanceof Node\Expr\FuncCall){
if($node->name instanceof Node\Name){
$name = strtolower($node->name->toString());
if(!$this->sandbox->checkFunc($name)){
$this->sandbox->validationError("Function failed custom validation!", Error::VALID_FUNC_ERROR, $node);
}
if($this->sandbox->isDefinedFunc($name)){
$args = $node->args;
array_unshift($args, new Node\Arg(new Node\Scalar\String_($name)));
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), 'call_func', $args, $node->getAttributes());
}
if($this->sandbox->overwrite_defined_funcs && in_array($name, PHPSandbox::$defined_funcs)){
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_' . $name, [new Node\Arg(new Node\Expr\FuncCall(new Node\Name([$name])))], $node->getAttributes());
}
if($this->sandbox->overwrite_sandboxed_string_funcs && in_array($name, PHPSandbox::$sandboxed_string_funcs)){
$args = $node->args;
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_' . $name, $args, $node->getAttributes());
}
if($this->sandbox->overwrite_func_get_args && in_array($name, PHPSandbox::$arg_funcs)){
if($name == 'func_get_arg'){
$index = new Node\Arg(new Node\Scalar\LNumber(0));
if(isset($node->args[0]) && $node->args[0] instanceof Node\Arg){
$index = $node->args[0];
}
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_' . $name, [new Node\Arg(new Node\Expr\FuncCall(new Node\Name(['func_get_args']))), $index], $node->getAttributes());
}
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_' . $name, [new Node\Arg(new Node\Expr\FuncCall(new Node\Name(['func_get_args'])))], $node->getAttributes());
}
} else {
return new Node\Expr\Ternary(
new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), 'check_func', [new Node\Arg($node->name)], $node->getAttributes()),
$node,
new Node\Expr\ConstFetch(new Node\Name('null'))
);
}
} else if($node instanceof Node\Stmt\Function_){
if(!$this->sandbox->allow_functions){
$this->sandbox->validationError("Sandboxed code attempted to define function!", Error::DEFINE_FUNC_ERROR, $node);
}
if(!$this->sandbox->checkKeyword('function')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'function');
}
if(!$node->name){
$this->sandbox->validationError("Sandboxed code attempted to define unnamed function!", Error::DEFINE_FUNC_ERROR, $node, '');
}
if($this->sandbox->isDefinedFunc($node->name)){
$this->sandbox->validationError("Sandboxed code attempted to redefine function!", Error::DEFINE_FUNC_ERROR, $node, $node->name);
}
if($node->byRef && !$this->sandbox->allow_references){
$this->sandbox->validationError("Sandboxed code attempted to define function return by reference!", Error::BYREF_ERROR, $node);
}
} else if($node instanceof Node\Expr\Closure){
if(!$this->sandbox->allow_closures){
$this->sandbox->validationError("Sandboxed code attempted to create a closure!", Error::CLOSURE_ERROR, $node);
}
} else if($node instanceof Node\Stmt\Class_){
if(!$this->sandbox->allow_classes){
$this->sandbox->validationError("Sandboxed code attempted to define class!", Error::DEFINE_CLASS_ERROR, $node);
}
if(!$this->sandbox->checkKeyword('class')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'class');
}
if(!$node->name){
$this->sandbox->validationError("Sandboxed code attempted to define unnamed class!", Error::DEFINE_CLASS_ERROR, $node, '');
}
if(!$this->sandbox->checkClass($node->name)){
$this->sandbox->validationError("Class failed custom validation!", Error::VALID_CLASS_ERROR, $node, $node->name);
}
if($node->extends instanceof Node\Name){
if(!$this->sandbox->checkKeyword('extends')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'extends');
}
if(!$node->extends->toString()){
$this->sandbox->validationError("Sandboxed code attempted to extend unnamed class!", Error::DEFINE_CLASS_ERROR, $node, '');
}
if(!$this->sandbox->checkClass($node->extends->toString(), true)){
$this->sandbox->validationError("Class extension failed custom validation!", Error::VALID_CLASS_ERROR, $node, $node->extends->toString());
}
}
if(is_array($node->implements)){
if(!$this->sandbox->checkKeyword('implements')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'implements');
}
foreach($node->implements as $implement){
/**
* @var Node\Name $implement
*/
if(!$implement->toString()){
$this->sandbox->validationError("Sandboxed code attempted to implement unnamed interface!", Error::DEFINE_INTERFACE_ERROR, $node, '');
}
if(!$this->sandbox->checkInterface($implement->toString())){
$this->sandbox->validationError("Interface failed custom validation!", Error::VALID_INTERFACE_ERROR, $node, $implement->toString());
}
}
}
} else if($node instanceof Node\Stmt\Interface_){
if(!$this->sandbox->allow_interfaces){
$this->sandbox->validationError("Sandboxed code attempted to define interface!", Error::DEFINE_INTERFACE_ERROR, $node);
}
if(!$this->sandbox->checkKeyword('interface')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'interface');
}
if(!$node->name){
$this->sandbox->validationError("Sandboxed code attempted to define unnamed interface!", Error::DEFINE_INTERFACE_ERROR, $node, '');
}
if(!$this->sandbox->checkInterface($node->name)){
$this->sandbox->validationError("Interface failed custom validation!", Error::VALID_INTERFACE_ERROR, $node, $node->name);
}
} else if($node instanceof Node\Stmt\Trait_){
if(!$this->sandbox->allow_traits){
$this->sandbox->validationError("Sandboxed code attempted to define trait!", Error::DEFINE_TRAIT_ERROR, $node);
}
if(!$this->sandbox->checkKeyword('trait')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'trait');
}
if(!$node->name){
$this->sandbox->validationError("Sandboxed code attempted to define unnamed trait!", Error::DEFINE_TRAIT_ERROR, $node, '');
}
if(!$this->sandbox->checkTrait($node->name)){
$this->sandbox->validationError("Trait failed custom validation!", Error::VALID_TRAIT_ERROR, $node, $node->name);
}
} else if($node instanceof Node\Stmt\TraitUse){
if(!$this->sandbox->checkKeyword('use')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'use');
}
if(is_array($node->traits)){
foreach($node->traits as $trait){
/**
* @var Node\Name $trait
*/
if(!$trait->toString()){
$this->sandbox->validationError("Sandboxed code attempted to use unnamed trait!", Error::DEFINE_TRAIT_ERROR, $node, '');
}
if(!$this->sandbox->checkTrait($trait->toString())){
$this->sandbox->validationError("Trait failed custom validation!", Error::VALID_TRAIT_ERROR, $node, $trait->toString());
}
}
}
} else if($node instanceof Node\Expr\Yield_){
if(!$this->sandbox->allow_generators){
$this->sandbox->validationError("Sandboxed code attempted to create a generator!", Error::GENERATOR_ERROR, $node);
}
if(!$this->sandbox->checkKeyword('yield')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'yield');
}
} else if($node instanceof Node\Stmt\Global_){
if(!$this->sandbox->allow_globals){
$this->sandbox->validationError("Sandboxed code attempted to use global keyword!", Error::GLOBALS_ERROR, $node);
}
if(!$this->sandbox->checkKeyword('global')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'global');
}
foreach($node->vars as $var){
/**
* @var Node\Expr\Variable $var
*/
if($var instanceof Node\Expr\Variable){
if(!$this->sandbox->checkGlobal($var->name)){
$this->sandbox->validationError("Global failed custom validation!", Error::VALID_GLOBAL_ERROR, $node, $var->name);
}
} else {
$this->sandbox->validationError("Sandboxed code attempted to pass non-variable to global keyword!", Error::DEFINE_GLOBAL_ERROR, $node);
}
}
} else if($node instanceof Node\Expr\Variable){
if(!is_string($node->name)){
$this->sandbox->validationError("Sandboxed code attempted dynamically-named variable call!", Error::DYNAMIC_VAR_ERROR, $node);
}
if($node->name == $this->sandbox->name){
$this->sandbox->validationError("Sandboxed code attempted to access the PHPSandbox instance!", Error::SANDBOX_ACCESS_ERROR, $node);
}
if(in_array($node->name, PHPSandbox::$superglobals)){
if(!$this->sandbox->checkSuperglobal($node->name)){
$this->sandbox->validationError("Superglobal failed custom validation!", Error::VALID_SUPERGLOBAL_ERROR, $node, $node->name);
}
if($this->sandbox->overwrite_superglobals){
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_get_superglobal', [new Node\Arg(new Node\Scalar\String_($node->name))], $node->getAttributes());
}
} else {
if(!$this->sandbox->checkVar($node->name)){
$this->sandbox->validationError("Variable failed custom validation!", Error::VALID_VAR_ERROR, $node, $node->name);
}
}
} else if($node instanceof Node\Stmt\StaticVar){
if(!$this->sandbox->allow_static_variables){
$this->sandbox->validationError("Sandboxed code attempted to create static variable!", Error::STATIC_VAR_ERROR, $node);
}
if(!is_string($node->name)){
$this->sandbox->validationError("Sandboxed code attempted dynamically-named static variable call!", Error::DYNAMIC_STATIC_VAR_ERROR, $node);
}
if(!$this->sandbox->checkVar($node->name)){
$this->sandbox->validationError("Variable failed custom validation!", Error::VALID_VAR_ERROR, $node, $node->name);
}
if($node->default instanceof Node\Expr\New_){
$node->default = $node->default->args[0];
}
} else if($node instanceof Node\Stmt\Const_){
$this->sandbox->validationError("Sandboxed code cannot use const keyword in the global scope!", Error::GLOBAL_CONST_ERROR, $node);
} else if($node instanceof Node\Expr\ConstFetch){
if(!$node->name instanceof Node\Name){
$this->sandbox->validationError("Sandboxed code attempted dynamically-named constant call!", Error::DYNAMIC_CONST_ERROR, $node);
}
if(!$this->sandbox->checkConst($node->name->toString())){
$this->sandbox->validationError("Constant failed custom validation!", Error::VALID_CONST_ERROR, $node, $node->name->toString());
}
} else if($node instanceof Node\Expr\ClassConstFetch || $node instanceof Node\Expr\StaticCall || $node instanceof Node\Expr\StaticPropertyFetch){
$class = $node->class;
if(!$class instanceof Node\Name){
$this->sandbox->validationError("Sandboxed code attempted dynamically-named class call!", Error::DYNAMIC_CLASS_ERROR, $node);
}
if($this->sandbox->isDefinedClass($class)){
$node->class = new Node\Name($this->sandbox->getDefinedClass($class));
}
/**
* @var Node\Name $class
*/
if(!$this->sandbox->checkClass($class->toString())){
$this->sandbox->validationError("Class constant failed custom validation!", Error::VALID_CLASS_ERROR, $node, $class->toString());
}
return $node;
} else if($node instanceof Node\Param && $node->type instanceof Node\Name){
$class = $node->type->toString();
if($this->sandbox->isDefinedClass($class)){
$node->type = new Node\Name($this->sandbox->getDefinedClass($class));
}
return $node;
} else if($node instanceof Node\Expr\New_){
if(!$this->sandbox->allow_objects){
$this->sandbox->validationError("Sandboxed code attempted to create object!", Error::CREATE_OBJECT_ERROR, $node);
}
if(!$this->sandbox->checkKeyword('new')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'new');
}
if(!$node->class instanceof Node\Name){
$this->sandbox->validationError("Sandboxed code attempted dynamically-named class call!", Error::DYNAMIC_CLASS_ERROR, $node);
}
$class = $node->class->toString();
if($this->sandbox->isDefinedClass($class)){
$node->class = new Node\Name($this->sandbox->getDefinedClass($class));
}
$this->sandbox->checkType($class);
return $node;
} else if($node instanceof Node\Expr\ErrorSuppress){
if(!$this->sandbox->allow_error_suppressing){
$this->sandbox->validationError("Sandboxed code attempted to suppress error!", Error::ERROR_SUPPRESS_ERROR, $node);
}
} else if($node instanceof Node\Expr\AssignRef){
if(!$this->sandbox->allow_references){
$this->sandbox->validationError("Sandboxed code attempted to assign by reference!", Error::BYREF_ERROR, $node);
}
} else if($node instanceof Node\Stmt\HaltCompiler){
if(!$this->sandbox->allow_halting){
$this->sandbox->validationError("Sandboxed code attempted to halt compiler!", Error::HALT_ERROR, $node);
}
if(!$this->sandbox->checkKeyword('halt')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'halt');
}
} else if($node instanceof Node\Stmt\Namespace_){
if(!$this->sandbox->allow_namespaces){
$this->sandbox->validationError("Sandboxed code attempted to define namespace!", Error::DEFINE_NAMESPACE_ERROR, $node);
}
if(!$this->sandbox->checkKeyword('namespace')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'namespace');
}
if($node->name instanceof Node\Name){
$namespace = $node->name->toString();
$this->sandbox->checkNamespace($namespace);
if(!$this->sandbox->isDefinedNamespace($namespace)){
$this->sandbox->defineNamespace($namespace);
}
} else {
$this->sandbox->validationError("Sandboxed code attempted use invalid namespace!", Error::DEFINE_NAMESPACE_ERROR, $node);
}
return $node->stmts;
} else if($node instanceof Node\Stmt\Use_){
if(!$this->sandbox->allow_aliases){
$this->sandbox->validationError("Sandboxed code attempted to use namespace and/or alias!", Error::DEFINE_ALIAS_ERROR, $node);
}
if(!$this->sandbox->checkKeyword('use')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'use');
}
foreach($node->uses as $use){
/**
* @var Node\Stmt\UseUse $use
*/
if($use instanceof Node\Stmt\UseUse && $use->name instanceof Node\Name && (is_string($use->alias) || is_null($use->alias))){
$this->sandbox->checkAlias($use->name->toString());
if($use->alias){
if(!$this->sandbox->checkKeyword('as')){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, 'as');
}
}
$this->sandbox->defineAlias($use->name->toString(), $use->alias);
} else {
$this->sandbox->validationError("Sandboxed code attempted use invalid namespace or alias!", Error::DEFINE_ALIAS_ERROR, $node);
}
}
return false;
} else if($node instanceof Node\Expr\ShellExec){
if($this->sandbox->isDefinedFunc('shell_exec')){
$args = [
new Node\Arg(new Node\Scalar\String_('shell_exec')),
new Node\Arg(new Node\Scalar\String_(implode('', $node->parts)))
];
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), 'call_func', $args, $node->getAttributes());
}
if($this->sandbox->hasWhitelistedFuncs()){
if(!$this->sandbox->isWhitelistedFunc('shell_exec')){
$this->sandbox->validationError("Sandboxed code attempted to use shell execution backticks when the shell_exec function is not whitelisted!", Error::BACKTICKS_ERROR, $node);
}
} else if($this->sandbox->hasBlacklistedFuncs() && $this->sandbox->isBlacklistedFunc('shell_exec')){
$this->sandbox->validationError("Sandboxed code attempted to use shell execution backticks when the shell_exec function is blacklisted!", Error::BACKTICKS_ERROR, $node);
}
if(!$this->sandbox->allow_backticks){
$this->sandbox->validationError("Sandboxed code attempted to use shell execution backticks!", Error::BACKTICKS_ERROR, $node);
}
} else if($name = $this->isMagicConst($node)){
if(!$this->sandbox->checkMagicConst($name)){
$this->sandbox->validationError("Magic constant failed custom validation!", Error::VALID_MAGIC_CONST_ERROR, $node, $name);
}
if($this->sandbox->isDefinedMagicConst($name)){
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_get_magic_const', [new Node\Arg(new Node\Scalar\String_($name))], $node->getAttributes());
}
} else if($name = $this->isKeyword($node)){
if(!$this->sandbox->checkKeyword($name)){
$this->sandbox->validationError("Keyword failed custom validation!", Error::VALID_KEYWORD_ERROR, $node, $name);
}
if($node instanceof Node\Expr\Include_ && !$this->sandbox->allow_includes){
$this->sandbox->validationError("Sandboxed code attempted to include files!", Error::INCLUDE_ERROR, $node, $name);
} else if($node instanceof Node\Expr\Include_ &&
(
($node->type == Node\Expr\Include_::TYPE_INCLUDE && $this->sandbox->isDefinedFunc('include'))
|| ($node->type == Node\Expr\Include_::TYPE_INCLUDE_ONCE && $this->sandbox->isDefinedFunc('include_once'))
|| ($node->type == Node\Expr\Include_::TYPE_REQUIRE && $this->sandbox->isDefinedFunc('require'))
|| ($node->type == Node\Expr\Include_::TYPE_REQUIRE_ONCE && $this->sandbox->isDefinedFunc('require_once'))
)){
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), 'call_func', [new Node\Arg(new Node\Scalar\String_($name)), new Node\Arg($node->expr)], $node->getAttributes());
} else if($node instanceof Node\Expr\Include_ && $this->sandbox->sandbox_includes){
switch($node->type){
case Node\Expr\Include_::TYPE_INCLUDE_ONCE:
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_include_once', [new Node\Arg($node->expr)], $node->getAttributes());
break;
case Node\Expr\Include_::TYPE_REQUIRE:
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_require', [new Node\Arg($node->expr)], $node->getAttributes());
break;
case Node\Expr\Include_::TYPE_REQUIRE_ONCE:
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_require_once', [new Node\Arg($node->expr)], $node->getAttributes());
break;
case Node\Expr\Include_::TYPE_INCLUDE:
default:
return new Node\Expr\MethodCall(new Node\Expr\StaticCall(new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\CustomizedSandbox"), 'getGlobalSandbox'), '_include', [new Node\Arg($node->expr)], $node->getAttributes());
break;
}
}
} else if($name = $this->isOperator($node)){
if(!$this->sandbox->checkOperator($name)){
$this->sandbox->validationError("Operator failed custom validation!", Error::VALID_OPERATOR_ERROR, $node, $name);
}
} else if($name = $this->isPrimitive($node)){
if(!$this->sandbox->checkPrimitive($name)){
$this->sandbox->validationError("Primitive failed custom validation!", Error::VALID_PRIMITIVE_ERROR, $node, $name);
}
}
return null;
}
}
+34
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\PhpMAE\Sandbox;
use CloudObjects\PhpMAE\Exceptions\PhpMAEException;
class FunctionExecutor {
private static $trusted = false;
private static $packageWhitelist = [ 'function_exists', 'extension_loaded',
'defined', 'ini_get', 'is_callable', 'class_exists', 'get_called_class',
'stream_for' ];
public static function setTrusted(bool $trusted) {
self::$trusted = $trusted;
}
public static function execute() {
$call = func_get_args();
if (self::$trusted == true || in_array($call[0], self::$packageWhitelist))
return call_user_func_array($call[0], array_slice($call, 1));
else
throw new PhpMAEException("A package attempted to call the non-whitelisted PHP function ".$call[0]."()!");
}
public static function returnNull() {
return null;
}
}
@@ -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\PhpMAE\Sandbox;
use PhpParser\Node, PhpParser\NodeVisitorAbstract;
use PHPSandbox\PHPSandbox, PHPSandbox\Error;
class FunctionExecutorWrapperVisitor extends NodeVisitorAbstract {
private $sandbox;
public function __construct(PHPSandbox $sandbox) {
$this->sandbox = $sandbox;
}
public function leaveNode(Node $node){
if ($node instanceof Node\Expr\FuncCall) {
if($node->name instanceof Node\Name) {
$name = strtolower($node->name->toString());
try {
$this->sandbox->checkFunc($name);
} catch (\Exception $e) {
return new Node\Expr\StaticCall(
new Node\Name\FullyQualified("CloudObjects\\PhpMAE\\Sandbox\\FunctionExecutor"), 'execute',
array_merge([ new Node\Scalar\String_($name) ], $node->args),
$node->getAttributes());
}
}
}
return null;
}
}
+81
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\PhpMAE;
use Defuse\Crypto\Key, Defuse\Crypto\Crypto;
use Symfony\Component\HttpFoundation\Request, Symfony\Component\HttpFoundation\Response,
Symfony\Component\HttpFoundation\Cookie;
/**
* The session identifies an individual over multiple requests and allows
* storing encrypted information in a cookie.
*/
class Session implements \ArrayAccess {
private $id;
private $key;
private $data;
public static function createFromRequestWithKey(Request $request, $keyString) {
$session = new Session;
if ($request->cookies->has('session')) {
try {
// attempt to restore session from encrypted data
$key = Key::loadFromAsciiSafeString($keyString);
$sessionContent = json_decode(Crypto::decrypt($request->cookies->get('session'), $key), true);
if (!isset($sessionContent['id']) || !isset($sessionContent['data']))
throw new \Exception;
$session->id = $sessionContent['id'];
$session->data = $sessionContent['data'];
} catch (\Exception $e) {
// session could not be restored, create a new one anonymously
$session->id = uniqid();
$session->data = [];
}
} else {
// no session found, create one
$session->id = uniqid();
$session->data = [];
}
$session->key = $keyString;
return $session;
}
public function persistToResponse(Response $response) {
$key = Key::loadFromAsciiSafeString($this->key);
$sessionContent = json_encode([
'id' => $this->id,
'data' => $this->data
]);
$response->headers->setCookie(new Cookie('session',
Crypto::encrypt($sessionContent, $key),
new \DateTime('1 hour')));
}
public function getId() {
return $this->id;
}
public function offsetExists($offset) {
return isset($this->data[$offset]);
}
public function offsetGet($offset) {
return $this->data[$offset];
}
public function offsetSet($offset, $value) {
$this->data[$offset] = $value;
}
public function offsetUnset($offset) {
unset($this->data[$offset]);
}
}
+48
View File
@@ -0,0 +1,48 @@
<?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\PhpMAE;
use DI\ContainerBuilder;
use Psr\Container\ContainerInterface;
use GuzzleHttp\Client;
class TestEnvironmentManager {
private static function getFilename() {
return getenv('HOME').DIRECTORY_SEPARATOR.'.phpmae';
}
public static function getContainer() {
$definitions = [];
if (file_exists(self::getFilename())) {
$data = json_decode(file_get_contents(self::getFilename()), true);
if (isset($data['testenv_url'])) {
$definitions['testenv.url'] = $data['testenv_url'];
$definitions['testenv.client'] = \DI\factory(function (ContainerInterface $c) {
return new Client([ 'base_uri' => $c->get('testenv.url') ]);
});
}
}
$containerBuilder = new ContainerBuilder;
$containerBuilder->addDefinitions($definitions);
return $containerBuilder->build();
}
public static function setTestEnvironment($url) {
file_put_contents(self::getFileName(), json_encode(array(
'testenv_url' => $url
)));
}
public static function unsetTestEnvironment() {
file_put_contents(self::getFileName(), json_encode(array()));
}
}
+25
View File
@@ -0,0 +1,25 @@
<?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\PhpMAE;
class TwigTemplate {
private $key;
private $environment;
public function __construct($key, $content, $cachePath) {
$this->key = $key;
$this->environment = new \Twig_Environment(
new \Twig_Loader_Array([ $key => $content ]),
[ 'cache' => $cachePath ]
);
}
public function render($context) {
return $this->environment->render($this->key, $context);
}
}
+28
View File
@@ -0,0 +1,28 @@
<?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\PhpMAE;
class TwigTemplateFactory {
private $cachePath;
public function __construct($cachePath) {
$this->cachePath = $cachePath;
}
/**
* Creates a Twig template.
*
* @param string $string The template content.
* @param string $id An identifier that is used to cache the compiled template.
* If not provided, a hash of the template content is used instead.
*/
public function fromString(string $string, string $id = null) {
if ($id == null) $id = md5($string);
return new TwigTemplate($id, $string, $this->cachePath);
}
}
+52
View File
@@ -0,0 +1,52 @@
<?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\PhpMAE;
use ML\IRI\IRI;
use ML\JsonLD\Node;
use CloudObjects\SDK\NodeReader;
class TypeChecker {
private static $reader;
public static function isType(Node $object, $typeString) {
if (!isset(self::$reader))
self::$reader = new NodeReader([ 'prefixes' => [
'phpmae' => 'coid://phpmae.dev/'
]]);
return self::$reader->hasType($object, $typeString);
}
public static function isClass(Node $object) {
return self::isType($object, 'phpmae:Class')
|| self::isType($object, 'phpmae:HTTPInvokableClass');
}
public static function isInterface(Node $object) {
return self::isType($object, 'phpmae:Interface');
}
public static function getAdditionalTypes(Node $object) {
$types = $object->getType();
if (!is_array($types))
return [];
$coids = [];
foreach ($types as $t) {
if (in_array($t->getId(), [ 'coid://phpmae.dev/Class',
'coid://phpmae.dev/HTTPInvokableClass',
'coid://phpmae.dev/Interface',
])) continue;
$coids[] = new IRI($t->getId());
}
return $coids;
}
}
+106
View File
@@ -0,0 +1,106 @@
<?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\PhpMAE;
use Psr\Container\ContainerInterface;
use Psr\Http\Message\RequestInterface;
use Slim\Http\Response;
use ML\IRI\IRI;
use ML\JsonLD\JsonLD;
use CloudObjects\Utilities\RDF\Arc2JsonLdConverter;
use CloudObjects\SDK\ObjectRetriever;
use CloudObjects\PhpMAE\Exceptions\PhpMAEException;
class UploadController {
private $container;
private $objectRetriever;
private $classRepository;
public function __construct(ContainerInterface $container, ObjectRetriever $objectRetriever,
ClassRepository $classRepository) {
$this->container = $container;
$this->objectRetriever = $objectRetriever;
$this->classRepository = $classRepository;
}
private function uploadSource(RequestInterface $request) {
$object = $this->objectRetriever->get($request->getQueryParam('coid'));
if (!$object)
throw new PhpMAEException("Unable to retrieve object.");
$content = (string)$request->getBody();
$validator = new ClassValidator;
try {
$validator->validate($content,
new IRI($object->getId()),
TypeChecker::getAdditionalTypes($object));
} catch (\Exception $e) {
$response = (new Response(400))->withJson([
'error_code' => get_class($e),
'error_message' => $e->getMessage()
]);
return $response;
}
if ($this->classRepository->storeUploadedFile($object, $content))
return new Response(201);
else
return new Response(500);
}
private function uploadConfig(RequestInterface $request) {
// Parse RDF/XML config
$parser = \ARC2::getRDFXMLParser();
$parser->parse('', (string)$request->getBody());
$index = $parser->getSimpleIndex(false);
// Convert to JsonLD as required by the CloudObjects SDK
$jsonLdConfig = JsonLD::compact(
JsonLD::fromRdf(Arc2JsonLdConverter::indexToQuads($index)));
// Validate config
if (!isset($jsonLdConfig->{'@id'})
|| $jsonLdConfig->{'@id'} != $request->getQueryParam('coid')) {
throw new PhpMAEException("Uploaded configuration does not correspond to object.");
}
// Store configuration
$iri = new IRI($request->getQueryParam('coid'));
$path = $this->container->get('uploads_dir')
.DIRECTORY_SEPARATOR.'config'
.DIRECTORY_SEPARATOR.$iri->getHost()
.$iri->getPath();
if (!is_dir($path)) mkdir($path, 0777, true);
file_put_contents($path.DIRECTORY_SEPARATOR.'object.jsonld', json_encode($jsonLdConfig));
return new Response(201);
}
public function handle(RequestInterface $request) {
if (!$this->container->has('uploads') || $this->container->get('uploads') !== true)
throw new PhpMAEException("This is not a test environment where uploads are permitted.");
if ($request->getMethod() != 'PUT')
throw new PhpMAEException("Must use PUT for uploading to test environment.");
switch ($request->getQueryParam('type')) {
case "source":
return $this->uploadSource($request);
break;
case "config":
return $this->uploadConfig($request);
break;
default:
throw new PhpMAEException("Invalid test environment request.");
}
}
}