Imported code from old repository
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
vendor
|
||||
build
|
||||
cache
|
||||
.cache
|
||||
.config
|
||||
.local
|
||||
*.phar
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
FROM cloudobjects/php-app-base:latest
|
||||
|
||||
# Add application code
|
||||
ADD / /var/www/app/
|
||||
|
||||
# Add an executable to the Docker container for CLI use
|
||||
RUN mv /var/www/app/phpmae.docker /usr/local/bin/phpmae && chmod +x /usr/local/bin/phpmae
|
||||
|
||||
# Have default config.php ready for CLI where init may not run
|
||||
RUN cp /var/www/app/config.php.default /var/www/app/config.php
|
||||
|
||||
# Launch init script
|
||||
CMD ["/bin/sh", "/var/www/app/init.sh"]
|
||||
@@ -35,7 +35,7 @@ Mozilla Public License Version 2.0
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
@@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
|
||||
@@ -1,2 +1,122 @@
|
||||
# phpMAE
|
||||
|
||||
phpMAE, the *PHP* *M*icro *A*PI *E*ngine, is an opinionated serverless framework for the development, execution, and deployment of small-sized stateless Web APIs, so-called "Micro APIs".
|
||||
|
||||
The framework is partially built on top of the [Slim micro-framework](https://www.slimframework.com/) and leverages [PHPSandbox](https://phpsandbox.org/) to provide a safe runtime environment.
|
||||
|
||||
The configuration and source code for Micro APIs can be stored in [CloudObjects Core](https://cloudobjects.io/) and are deployed just-in-time to a running phpMAE instance via the [CloudObjects SDK](https://packagist.org/packages/cloudobjects/sdk) when a request for a specific API is received.
|
||||
|
||||
CloudObjects currently provides a preview release of a public hosted version of phpMAE. For development or if you wish to run your own Micro API engine, you can run phpMAE as a PHAR file, using our Docker container or directly from the source.
|
||||
|
||||
## Installation
|
||||
|
||||
**Note:** The installation steps below were tested on _macOS_, which has PHP installed by default. If you are on Linux, you may have to install PHP through your distribution's package manager first. phpMAE has not yet been tested on Windows, but support is on the roadmap.
|
||||
|
||||
### PHAR
|
||||
|
||||
This is the recommended installation method for developers to create, validate and deploy their Micro APIs.
|
||||
|
||||
You can grab the latest PHAR (PHp ARchive) from the [phpMAE Releases on Codeberg](https://codeberg.org/CloudObjects/phpMAE/releases).
|
||||
|
||||
Type `php phpmae.phar` to get a list of available CLI commands. Any commands or options that interact with CloudObjects require the [CloudObjects CLI Tool](https://cloudobjects.io/clitool) to be installed as a prerequisite.
|
||||
|
||||
To make the `phpmae` CLI tool globally available on your system run the following commands from the directory in which you downloaded `phpmae.phar`:
|
||||
|
||||
cp phpmae.phar /usr/local/bin/phpmae
|
||||
chmod +x /usr/local/bin/phpmae
|
||||
|
||||
### Docker
|
||||
|
||||
This is the recommended installation method if you want to run Micro APIs for staging and production on your own servers (local or cloud).
|
||||
|
||||
A prebuilt-image is available [from the Docker Hub](https://hub.docker.com/r/cloudobjects/phpmae/). You can download it via CLI:
|
||||
|
||||
docker pull cloudobjects/phpmae
|
||||
|
||||
When running the container you need to provide the _CO_AUTH_NS_ and _CO_AUTH_SECRET_ environment variables so that the phpMAE can authenticate itself against CloudObjects. Otherwise, only Micro APIs with [co:isVisibleTo](https://coid.link/cloudobjects.io/isVisibleTo) set to [co:Public](https://coid.link/cloudobjects.io/Public) can be run:
|
||||
|
||||
docker run -e CO_AUTH_NS=example.com -e CO_AUTH_SECRET=XXXXXXXX -p 8080:80 cloudobjects/phpmae
|
||||
|
||||
For _CO_AUTH_NS_ use a domain that you have added to CloudObjects and that you want to use as the identity for this phpMAE instance. For _CO_AUTH_SECRET_ you need to retrieve the shared secret between that domain and _cloudobjects.io_. You can retrieve this secret with the [CloudObjects CLI Tool](https://cloudobjects.io/clitool) using the following command:
|
||||
|
||||
cloudobjects domain-providers:secret example.com cloudobjects.io
|
||||
|
||||
### Source
|
||||
|
||||
This installation method is only recommended if you want to "look under the hood" of phpMAE or run with particular options.
|
||||
|
||||
You need to have [Composer](https://getcomposer.org) installed globally on your system to download and install the dependencies.
|
||||
|
||||
You can download or `git clone` this repository from Codeberg, then run `composer install` (or `make`) to install dependencies.
|
||||
|
||||
It's recommended to use composer to download and install with a single command:
|
||||
|
||||
composer create-project cloudobjects/phpmae
|
||||
|
||||
You can customize your installation of phpMAE by copying `config.php.default` to `config.php` and then editing the file as per your requirements.
|
||||
|
||||
phpMAE comes with unit tests. You can run them after installing the source to validate that any changes you made did not break the tests:
|
||||
|
||||
vendor/bin/phpunit
|
||||
|
||||
## Getting Started Guide
|
||||
|
||||
### Create a class
|
||||
|
||||
Micro APIs compatible with phpMAE are implemented as PHP classes. They are represented as objects on CloudObjects with the type [phpmae:Class](https://coid.link/phpmae.dev/Class). All public methods on these classes are made accessible via RPC protocols (currently [JSON-RPC](https://www.jsonrpc.org/specification)) and other classes that add your class as a dependency. If you want your class to expose a single entry point you can add [PHP's magic __invoke()](https://secure.php.net/manual/en/language.oop5.magic.php#object.invoke) to it and make its object an instance of the type [phpmae:HTTPInvokableClass](https://coid.link/phpmae.dev/HTTPInvokableClass).
|
||||
|
||||
Like all objects, phpMAE classes are uniquely identified with COIDs (*C*loud *O*bject *ID*entifiers). COIDs are namespaced into domains, and you can create objects with COIDs only for domains that you have created or been assigned to in CloudObjects. You can see those domains in the [CloudObjects Dashboard](https://cloudobjects.io/dashboard).
|
||||
|
||||
To create a new class, choose a COID and then run the following command:
|
||||
|
||||
phpmae class:create --confjob coid://NAMESPACE/NAME/VERSION
|
||||
|
||||
If you want to create an HTTP-invokable class, add the option `--http-invokable`.
|
||||
|
||||
This command writes two files into the current directory, namely `NAME.VERSION.xml` and `NAME.VERSION.php`. The `.xml` file contains the basic object description for CloudObjects in RDF/XML format and the `.php` file contains the skeleton code for the PHP class. It also creates a configuration job to register the COID.
|
||||
|
||||
Open the `.php` file and start inserting your code.
|
||||
|
||||
### Validate and test locally
|
||||
|
||||
To check whether your code is valid PHP and also respects the constraints of phpMAE regarding whitelisted functions and classes run the following command:
|
||||
|
||||
phpmae class:validate coid://NAMESPACE/NAME/VERSION
|
||||
|
||||
You can add `--watch` to continuously watch for changes in the file and automatically revalidate.
|
||||
|
||||
To run your class, you can launch a local web server. Open a second terminal window or tab and run the following command:
|
||||
|
||||
phpmae testenv:start
|
||||
|
||||
The web server runs in the foreground and can be stopped with _Ctrl + C_ (or _Cmd + C_). Go back to the first tab and run the following command:
|
||||
|
||||
phpmae class:testenv coid://NAMESPACE/NAME/VERSION
|
||||
|
||||
Apart from deploying your code to the local web server this command also prints out the base URL of your Micro API which you can then open in a browser or query from a tool such as `curl`. The `--watch` option is supported for continuous redeployment.
|
||||
|
||||
### Deploy your class
|
||||
|
||||
Use the following command to deploy your class:
|
||||
|
||||
phpmae class:deploy coid://NAMESPACE/NAME/VERSION
|
||||
|
||||
Internally, this command first validates the class, then calls the CloudObjects CLI to upload the `.php` source file as an attachment to CloudObjects Core and, if necessary, update the `.xml` file with a configuration job. Deployed classes are available for phpMAE instances within moments.
|
||||
|
||||
The output of the deploy command shows you the base URL of your Micro API on the public phpMAE instances. These instances require you to use HTTP Basic authentication to access your class. You have to use the namespace as the username, and the CloudObjects shared secret between that domain and _phpmae.dev_ as the password. The command for retrieving this secret is shown to you as well.
|
||||
|
||||
### Use your class on custom instances
|
||||
|
||||
You can use your class on your own private instances as well. Start an instance,e.g., using Docker as described above, and replace _phpmae.dev_ in the URL with our own instance's URL.
|
||||
|
||||
## Help&Support
|
||||
|
||||
You can report bugs or suggest features through [our Codeberg Issues](https://codeberg.org/CloudObjects/phpMAE/issues). We also accept PRs with bug fixes; if you wish to contribute features, please create an issue first or discuss on chat. If you found a potential security issue, e.g., with the sandboxing feature, please do not use the public issue tracker but send an email to phpmae-security@cloudobjects.io.
|
||||
|
||||
Make sure you follow the [CloudObjects Blog](https://blog.cloudobjects.io/) and [@CloudObjectsIO](https://twitter.com/CloudObjectsIO) for the latest updates, guides, and tutorials.
|
||||
|
||||
Commercial support and hosted private instances are available from [CloudObjects Consulting](https://cloudobjects.io/consulting).
|
||||
|
||||
## License
|
||||
|
||||
phpMAE is licensed under the Mozilla Public License (see LICENSE file).
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use CloudObjects\SDK\COIDParser, CloudObjects\SDK\ObjectRetriever;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* This is project's console commands configuration for Robo task runner.
|
||||
*
|
||||
* @see http://robo.li/
|
||||
*/
|
||||
class RoboFile extends \Robo\Tasks {
|
||||
|
||||
public function phar() {
|
||||
// based on example in http://robo.li/tasks/Development/#packphar
|
||||
|
||||
$pharTask = $this->taskPackPhar('phpmae.phar')
|
||||
->compress()
|
||||
->addFile('phpmae.php', 'phpmae.php')
|
||||
->addFile('config.php', 'config.php')
|
||||
->addFile('web/index.php', 'web/index.php')
|
||||
->stub('stub.php');
|
||||
|
||||
$finder = Finder::create()
|
||||
->name('*.php')
|
||||
->in('classes');
|
||||
|
||||
foreach ($finder as $file) {
|
||||
$pharTask->addFile('classes/'.$file->getRelativePathname(), $file->getRealPath());
|
||||
}
|
||||
|
||||
$finder = Finder::create()->files()
|
||||
->name('*.php')
|
||||
->in('vendor');
|
||||
|
||||
foreach ($finder as $file) {
|
||||
$pharTask->addStripped('vendor/'.$file->getRelativePathname(), $file->getRealPath());
|
||||
}
|
||||
|
||||
$finder = Finder::create()->files()
|
||||
->name('*.php')
|
||||
->name('*.json')
|
||||
->in('stacks');
|
||||
|
||||
foreach ($finder as $file) {
|
||||
$pharTask->addStripped('stacks/'.$file->getRelativePathname(),$file->getRealPath());
|
||||
}
|
||||
|
||||
$pharTask->run();
|
||||
|
||||
// Verify Phar is packed correctly
|
||||
$code = $this->_exec('php phpmae.phar');
|
||||
}
|
||||
|
||||
public function sanitizeDependencies($directory) {
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
$finder = Finder::create()->files()
|
||||
->name('*.php')
|
||||
->in($directory)
|
||||
->notPath('composer');
|
||||
|
||||
$forbiddenFunctions = [ 'file_get_contents', 'file_put_contents', 'fopen' ];
|
||||
|
||||
foreach ($finder as $file) {
|
||||
try {
|
||||
$content = file_get_contents($file);
|
||||
foreach ($forbiddenFunctions as $func) {
|
||||
$content = preg_replace('/\b(?<!\>)'.$func.'\(/', '\CloudObjects\PhpMAE\Sandbox\FunctionExecutor::returnNull(', $content);
|
||||
$content = str_replace('\\\CloudObjects', '\CloudObjects', $content);
|
||||
}
|
||||
file_put_contents($file, $content);
|
||||
} catch (\Exception $e) {
|
||||
$this->say($file.': '.get_class($e).' '.$e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function buildFunctionWhitelist() {
|
||||
$allFunctions = get_defined_functions()['internal'];
|
||||
$whitelisted = file_exists('data/function.whitelist.json')
|
||||
? json_decode(file_get_contents('data/function.whitelist.json'), true) : [];
|
||||
$blacklisted = file_exists('data/function.blacklist.json')
|
||||
? json_decode(file_get_contents('data/function.blacklist.json'), true) : [];
|
||||
|
||||
$client = new \GuzzleHttp\Client([ 'base_uri' => 'http://php.net/manual/en/' ]);
|
||||
$autoBlacklistPrefix = '';
|
||||
|
||||
$progress = (count($whitelisted) + count($blacklisted)) / count($allFunctions) * 100;
|
||||
$this->say($progress.'% of functions handled, let\'s do the rest ...');
|
||||
|
||||
foreach ($allFunctions as $f) {
|
||||
// Ignore if already handled
|
||||
if (in_array($f, $whitelisted) || in_array($f, $blacklisted))
|
||||
continue;
|
||||
|
||||
$this->yell($f);
|
||||
|
||||
if ($autoBlacklistPrefix != '' && substr($f, 0, strlen($autoBlacklistPrefix)) == $autoBlacklistPrefix) {
|
||||
$this->say('Blacklisted automatically.');
|
||||
$blacklisted[] = $f;
|
||||
continue;
|
||||
}
|
||||
$autoBlacklistPrefix = '';
|
||||
|
||||
try {
|
||||
// Get and display documentation
|
||||
$crawler = new \Symfony\Component\DomCrawler\Crawler(
|
||||
$client->get('function.'.str_replace('_', '-', $f).'.php')->getBody()->getContents()
|
||||
);
|
||||
$this->say($crawler->filter('.refpurpose')->text());
|
||||
$this->say($crawler->filter('.refsect1.description')->text());
|
||||
} catch (\Exception $e) {
|
||||
$this->say('Error while retrieving information: '.$e->getMessage());
|
||||
}
|
||||
// Ask what to do
|
||||
$action = null;
|
||||
while (!in_array($action, ['w','b','a','s','x']))
|
||||
$action = $this->ask('(w)hitelist, (b)lacklist, (a)ll blacklist, (s)kip, e(x)it');
|
||||
|
||||
switch ($action) {
|
||||
case "w":
|
||||
$whitelisted[] = $f;
|
||||
break;
|
||||
case "b":
|
||||
$blacklisted[] = $f;
|
||||
break;
|
||||
case "a":
|
||||
$blacklisted[] = $f;
|
||||
$autoBlacklistPrefix = substr($f, 0, strpos($f, '_')+1);
|
||||
break;
|
||||
case "x":
|
||||
file_put_contents('data/function.whitelist.json', json_encode($whitelisted));
|
||||
file_put_contents('data/function.blacklist.json', json_encode($blacklisted));
|
||||
exit(0);
|
||||
}
|
||||
// Save on every step to prevent data loss from crashes
|
||||
file_put_contents('data/function.whitelist.json', json_encode($whitelisted));
|
||||
file_put_contents('data/function.blacklist.json', json_encode($blacklisted));
|
||||
}
|
||||
}
|
||||
|
||||
public function installStack($stack) {
|
||||
require_once "vendor/autoload.php";
|
||||
|
||||
// Fetch stack definition from CloudObjects Core
|
||||
$stackCoid = COIDParser::fromString($stack);
|
||||
$retriever = new ObjectRetriever([
|
||||
'auth_ns' => 'phpmae.dev',
|
||||
'auth_secret' => getenv('PHPMAE_SHARED_SECRET')
|
||||
]);
|
||||
$stackObject = $retriever->getObject($stackCoid);
|
||||
|
||||
$composerFileContent = $retriever->getAttachment($stackCoid,
|
||||
$stackObject->getProperty('coid://phpmae.dev/hasAttachedComposerFile')
|
||||
->getId());
|
||||
Assert::startsWith($composerFileContent, '{');
|
||||
|
||||
$lockFileContent = $retriever->getAttachment($stackCoid,
|
||||
$stackObject->getProperty('coid://phpmae.dev/hasAttachedLockFile')
|
||||
->getId());
|
||||
Assert::startsWith($lockFileContent, '{');
|
||||
|
||||
$stackDir = __DIR__.'/stacks/'.md5($stack);
|
||||
|
||||
// Install stack
|
||||
$this->taskFilesystemStack()
|
||||
->mkdir($stackDir)
|
||||
->run();
|
||||
$this->taskWriteToFile($stackDir.'/composer.json')
|
||||
->text($composerFileContent)
|
||||
->run();
|
||||
$this->taskWriteToFile($stackDir.'/composer.lock')
|
||||
->text($lockFileContent)
|
||||
->run();
|
||||
|
||||
// Find whitelisted classes
|
||||
$whitelistedClasses = [];
|
||||
foreach ($stackObject->getProperty('coid://phpmae.dev/whitelistsClassname')
|
||||
as $whitelistedClass)
|
||||
$whitelistedClasses[] = $whitelistedClass->getValue();
|
||||
|
||||
$this->taskWriteToFile($stackDir.'/meta.json')
|
||||
->text(json_encode([
|
||||
'rev' => $stackObject->getProperty(ObjectRetriever::REVISION_PROPERTY)->getValue(),
|
||||
'whitelisted_classes' => $whitelistedClasses
|
||||
]))
|
||||
->run();
|
||||
|
||||
$this->taskComposerInstall(__DIR__.'/composer.phar')
|
||||
->dir($stackDir)
|
||||
->run();
|
||||
|
||||
// Sanitize stack
|
||||
$this->sanitizeDependencies($stackDir);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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("");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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>");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"name" : "cloudobjects/phpmae",
|
||||
"description" : "The PHP Micro API Engine.",
|
||||
"keywords": ["cloudobjects", "serverless", "faas", "microapi", "sandbox"],
|
||||
"homepage": "https://github.com/CloudObjects/phpMAE",
|
||||
"license": "MPL-2.0",
|
||||
"require": {
|
||||
"symfony/console" : "^4.2.1",
|
||||
"corveda/php-sandbox": "^2.0.1",
|
||||
"cloudobjects/sdk" : "dev-main",
|
||||
"php-di/php-di": "^6.0",
|
||||
"slim/slim" : "^3.0",
|
||||
"jsonrpc/jsonrpc": "^1.0",
|
||||
"vlucas/phpdotenv" : "^2.4.0",
|
||||
"semsol/arc2" : "~2.3.1",
|
||||
"cloudobjects/rdfutilities" : "dev-main",
|
||||
"tuupola/slim-basic-auth": "^3.2",
|
||||
"dflydev/fig-cookies": "^1.0",
|
||||
"doctrine/collections": "^1.5",
|
||||
"twig/twig": "^2.5",
|
||||
"monolog/monolog": "^1.24",
|
||||
"guzzlehttp/oauth-subscriber": "^0.3.0",
|
||||
"webmozart/assert": "^1.3",
|
||||
"psr/simple-cache": "^1.0",
|
||||
"tuupola/cors-middleware": "^1.1",
|
||||
"relay/relay": "^2.1",
|
||||
"symfony/mailer": "^5.3"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"CloudObjects\\PhpMAE\\" : "classes"
|
||||
},
|
||||
"files" : [
|
||||
"vendor/semsol/arc2/ARC2_getFormat.php",
|
||||
"vendor/semsol/arc2/ARC2_getPreferredFormat.php"
|
||||
]
|
||||
},
|
||||
"require-dev" : {
|
||||
"phpunit/phpunit": "~5.7",
|
||||
"consolidation/robo": "~1"
|
||||
}
|
||||
}
|
||||
Generated
+6578
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'uploads' => false,
|
||||
'interactive_run' => false,
|
||||
'client_authentication' => 'none',
|
||||
'mode' => 'default',
|
||||
'agws.data_cache' => 'file',
|
||||
'log.target' => 'errorlog',
|
||||
'log.level' => Monolog\Logger::DEBUG,
|
||||
'cache_dir' => sys_get_temp_dir() . '/phpMAE/cache',
|
||||
'uploads_dir' => sys_get_temp_dir() . '/phpMAE/uploads',
|
||||
];
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/**
|
||||
* Specifies whether the /uploadTestenv endpoint can be used.
|
||||
* - false: no uploads are possible (except in interactive runtime, if enabled)
|
||||
* - true: any uploads are possible - DO NOT USE THIS IN PRODUCTION!
|
||||
*/
|
||||
'uploads' => false,
|
||||
|
||||
/**
|
||||
* Specifies whether the interactive runtime is enabled.
|
||||
* - false: disabled
|
||||
* - true: enabled
|
||||
*/
|
||||
'interactive_run' => false,
|
||||
|
||||
/**
|
||||
* Client authentication requires all requests to have HTTP Basic Authentication.
|
||||
* - none: Authentication is disabled.
|
||||
* - none:public_only: Authentication is disabled for all classes that have co:isVisibleTo
|
||||
* and co:permitsUsageTo both set to co:Public. All other classes will be treated
|
||||
* by other authentication schemes.
|
||||
* - shared_secret:runclass: Client must authenticate with the shared secret between
|
||||
* the identity of this phpMAE instance (auth_ns) and the namespace of the class
|
||||
* that is requested to run
|
||||
* - shared_secret:<COID>: Client must authenticate with the shared secret between
|
||||
* the identity of this phpMAE instance (auth_ns) and the namespace identified
|
||||
* by the COID.
|
||||
* Multiple authentication schemes (except "none") can be separated with the
|
||||
* pipe (|) character. The first valid scheme will be used.
|
||||
*/
|
||||
'client_authentication' => 'none',
|
||||
|
||||
/**
|
||||
* Choose whether authentication is only accepted over TLS (HTTPS) connections:
|
||||
* - true: Credentials are accepted only over HTTPS; this is the default value.
|
||||
* - false: Credentials are also accepted over insecure connections. This should only
|
||||
* be used for staging environments or local network deployments.
|
||||
*/
|
||||
'client_authentication_must_be_secure' => false,
|
||||
|
||||
/**
|
||||
* Choose whether CORS requests are possible and the origins that are allowed:
|
||||
* - empty string: No CORS
|
||||
* - '*': All origins allowed.
|
||||
* - Multiple origins can be separated with the pipe (|).
|
||||
'global_cors_origins' => '',
|
||||
|
||||
/**
|
||||
* Choose an operation mode of this phpMAE instance.
|
||||
* - default: Runs classes as specified in the request URI. Routing functionality is disabled.
|
||||
* - router:<COID>: Mounts the router specified by the COID. No other classes can be run.
|
||||
* - router:vhost: Mounts the router specified as phpmae:hasRouter for the namespace
|
||||
* of the HTTP Host header.
|
||||
* - router:header: Mounts the router specified by the "C-PhpMae-Router-COID" header.
|
||||
* Multiple modes can be separated with the pipe (|) character. The first mode
|
||||
* that applies will be used.
|
||||
*/
|
||||
'mode' => 'default',
|
||||
|
||||
/**
|
||||
* Specify the identity and CloudObjects Core authentication credential for this phpMAE instance.
|
||||
*/
|
||||
'co.auth_ns' => 'phpmae.dev',
|
||||
'co.auth_secret' => 'YOUR_SECRET_HERE',
|
||||
|
||||
/**
|
||||
* Specify whether and how data about accounts should be cached. This applies to requests that
|
||||
* originate from a CloudObjects Account Gateway.
|
||||
* - none: No caching.
|
||||
* - file: Cache in filesystem. Uses the "cache_dir" configuration option.
|
||||
*/
|
||||
'agws.data_cache' => 'file',
|
||||
|
||||
/**
|
||||
* Specifies whether and how phpMAE should write a log.
|
||||
* log.target:
|
||||
* - none: Write no log.
|
||||
* - errorlog: Write to the default error logger.
|
||||
* log.level: Use Monolog constants.
|
||||
*/
|
||||
'log.target' => 'errorlog',
|
||||
'log.level' => Monolog\Logger::DEBUG,
|
||||
|
||||
/**
|
||||
* Specifies a maximum time for script execution. Time limit starts
|
||||
* from the running class.
|
||||
*/
|
||||
'execution_time_limit' => 10
|
||||
|
||||
'cache_dir' => __DIR__.'/cache',
|
||||
'uploads_dir' => __DIR__.'/uploads',
|
||||
];
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,43 @@
|
||||
<h5>Public PHP Methods</h5>
|
||||
{% if custom.methods|length > 0 %}
|
||||
<ul class="collapsible popout" data-collapsible="accordion">
|
||||
{% for m in custom.methods %}
|
||||
<li id="method-{{m.name}}">
|
||||
<div class="collapsible-header">
|
||||
<code><strong>{{m.name}}</strong>({{m.params}})</code>
|
||||
</div>
|
||||
<div class="collapsible-body">
|
||||
{% if m.comment is empty %}
|
||||
<p>No documentation available.</p>
|
||||
{% else %}
|
||||
<p>{{m.comment|nl2br}}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if custom.public_source_code is defined and m.can_run == true %}
|
||||
<form>
|
||||
<div class="padded">
|
||||
<input type="hidden" name="class" value="{{object.id |slice(7)}}" />
|
||||
<input type="hidden" name="method" value="{{m.name}}" />
|
||||
{% for f in m.fields %}
|
||||
<div class="input-field">
|
||||
<input type="text" id="params-{{m.name}}-{{f.name}}" name="p-{{f.name}}" />
|
||||
<label for="params-{{m.name}}-{{f.name}}">{{f.name}}</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button type="submit" class="btn">Execute in phpMAE</button>
|
||||
</div>
|
||||
<h6 class="console-response-status"></h6>
|
||||
<div class="padded"><pre class="console-response"></pre></div>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>This class has no public methods yet or a problem occurred while retrieving them.</p>
|
||||
{% endif %}
|
||||
{% if custom.public_source_code is defined %}
|
||||
<h5>Source Code</h5>
|
||||
<pre class="co-console">{{ custom.public_source_code }}</pre>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,24 @@
|
||||
<h5>Public PHP Methods</h5>
|
||||
{% if custom.methods|length > 0 %}
|
||||
<ul class="collapsible popout" data-collapsible="accordion">
|
||||
{% for m in custom.methods %}
|
||||
<li id="method-{{m.name}}">
|
||||
<div class="collapsible-header">
|
||||
<code><strong>{{m.name}}</strong>({{m.params}})</code>
|
||||
</div>
|
||||
<div class="collapsible-body">
|
||||
{% if m.comment is empty %}
|
||||
<p>No documentation available.</p>
|
||||
{% else %}
|
||||
<p>{{m.comment|nl2br}}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% else %}
|
||||
<p>This interface has no public methods yet or a problem occurred while retrieving them.</p>
|
||||
{% endif %}
|
||||
<h5>Create Class from Interface (CLI)</h5>
|
||||
<p>With <a href="https://github.com/CloudObjects/phpMAE#installation">phpMAE installed locally</a>, you can create a PHP class that implements this interface with the following command:</p>
|
||||
<pre class="co-console">phpmae class:create --implements={{id}} --confjob <COID></pre>
|
||||
@@ -0,0 +1,40 @@
|
||||
<h5>Whitelisted Classes from Packages</h5>
|
||||
<ul>
|
||||
{% for className in config['coid://phpmae.dev/whitelistsClassname'] %}
|
||||
<li><code>{{ className.o_value }}</code></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<h5>Defined Packages</h5>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Version</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for package in custom.defined %}
|
||||
<tr>
|
||||
<td><a href="https://packagist.org/packages/{{ package.name }}">{{ package.name }}</a></td>
|
||||
<td>{{ package.version }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<h5>Actual Installed Packages</h5>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Version</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for package in custom.actual %}
|
||||
<tr>
|
||||
<td>{% if package.defined %}<strong>{% endif %}<a href="https://packagist.org/packages/{{ package.name }}">{{ package.name }}</a>{% if package.defined %}</strong>{% endif %}</td>
|
||||
<td>{{ package.version }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
@@ -0,0 +1,54 @@
|
||||
$('.collapsible-body form').on('submit', function(event) {
|
||||
event.preventDefault();
|
||||
var form = $(this);
|
||||
form.find('.btn').addClass('disabled');
|
||||
|
||||
var fields = form.serializeArray();
|
||||
var className = "";
|
||||
var method = "";
|
||||
var params = {};
|
||||
for (var f in fields) {
|
||||
switch (fields[f].name) {
|
||||
case "method":
|
||||
method = fields[f].value;
|
||||
break;
|
||||
case "class":
|
||||
className = fields[f].value;
|
||||
break;
|
||||
default:
|
||||
params[fields[f].name.substr(2)] = fields[f].value;
|
||||
}
|
||||
}
|
||||
|
||||
axios.post('https://phpmae.dev/' + className, {
|
||||
jsonrpc : '2.0',
|
||||
id : 'COWebsite',
|
||||
method : method,
|
||||
params : params
|
||||
}).then(function(response) {
|
||||
form.find('.btn').removeClass('disabled');
|
||||
|
||||
if (typeof(response.data) == "object" && response.data.hasOwnProperty('result')) {
|
||||
form.find('.console-response-status').text('Success');
|
||||
form.find('.console-response').text(
|
||||
typeof(response.data.result) == "string" ? response.data.result
|
||||
: JSON.stringify(response.data.result, null, 4));
|
||||
} else
|
||||
if (typeof(response.data) == "object" && !response.data.hasOwnProperty('error')) {
|
||||
form.find('.console-response-status').text('Success');
|
||||
form.find('.console-response').text(JSON.stringify(response.data, null, 4));
|
||||
} else
|
||||
if (typeof(response.data) == "object") {
|
||||
form.find('.console-response-status').text('Error');
|
||||
form.find('.console-response').text(JSON.stringify(response.data.error, null, 4))
|
||||
} else {
|
||||
form.find('.console-response-status').text('Success');
|
||||
form.find('.console-response').text(response.data);
|
||||
}
|
||||
}).catch(function(error) {
|
||||
form.find('.btn').removeClass('disabled');
|
||||
|
||||
form.find('.console-response-status').text('Exception');
|
||||
form.find('.console-response').text(JSON.stringify(error, null, 4))
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
app = "phpmae"
|
||||
|
||||
[[services]]
|
||||
internal_port = 80
|
||||
protocol = "tcp"
|
||||
|
||||
[services.concurrency]
|
||||
hard_limit = 25
|
||||
soft_limit = 20
|
||||
|
||||
[[services.ports]]
|
||||
handlers = ["http"]
|
||||
port = "80"
|
||||
|
||||
[[services.ports]]
|
||||
handlers = ["tls", "http"]
|
||||
port = "443"
|
||||
|
||||
[[services.tcp_checks]]
|
||||
interval = 10000
|
||||
timeout = 2000
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
const gulp = require('gulp');
|
||||
const concat = require('gulp-concat');
|
||||
const streamqueue = require('streamqueue');
|
||||
const uglify = require('gulp-uglify');
|
||||
const cssPurge = require('gulp-css-purge');
|
||||
const sass = require('gulp-sass');
|
||||
|
||||
// Concatenate and minify scripts
|
||||
gulp.task('scripts', function() {
|
||||
// inspired by https://stackoverflow.com/a/23507836
|
||||
return streamqueue({ objectMode: true },
|
||||
gulp.src('./node_modules/jquery/dist/jquery.min.js'),
|
||||
gulp.src('./node_modules/materialize-css/dist/js/materialize.js'),
|
||||
gulp.src('./node_modules/codemirror/lib/codemirror.js'),
|
||||
gulp.src('./node_modules/codemirror/mode/clike/clike.js'),
|
||||
gulp.src('./node_modules/codemirror/addon/edit/matchbrackets.js'),
|
||||
gulp.src('./node_modules/codemirror/mode/xml/xml.js'),
|
||||
gulp.src('./node_modules/codemirror/mode/javascript/javascript.js'),
|
||||
gulp.src('./node_modules/codemirror/mode/htmlmixed/htmlmixed.js'),
|
||||
gulp.src('./node_modules/codemirror/mode/css/css.js'),
|
||||
gulp.src('./node_modules/codemirror/mode/php/php.js'),
|
||||
gulp.src('./static/app.js')
|
||||
)
|
||||
.pipe(concat('scripts.js'))
|
||||
.pipe(uglify())
|
||||
.pipe(gulp.dest('./static'));
|
||||
});
|
||||
|
||||
// Concatenate and minify stylesheets
|
||||
gulp.task('styles', function() {
|
||||
return streamqueue({ objectMode: true },
|
||||
gulp.src('./scss/materialize-customized.scss')
|
||||
.pipe(sass().on('error', sass.logError)),
|
||||
gulp.src('./static/app.css')
|
||||
)
|
||||
.pipe(concat('styles.css'))
|
||||
.pipe(cssPurge({
|
||||
trim : true,
|
||||
shorten : true,
|
||||
verbose : false
|
||||
}))
|
||||
.pipe(gulp.dest('./static'));
|
||||
});
|
||||
|
||||
// Copy file that is needed as-is
|
||||
gulp.task('copy', function () {
|
||||
return gulp.src('./node_modules/codemirror/lib/codemirror.css')
|
||||
.pipe(gulp.dest('static'));
|
||||
});
|
||||
|
||||
// Run entire process
|
||||
gulp.task('default', gulp.series('scripts', 'styles', 'copy'));
|
||||
@@ -0,0 +1,30 @@
|
||||
# Apply defaults for unset variables
|
||||
if [ -z $MODE ]; then MODE=default; fi
|
||||
if [ -z $CLIENT_AUTH ]; then CLIENT_AUTH=shared_secret:runclass; fi
|
||||
if [ -z $INTERACTIVE ]; then INTERACTIVE=false; fi
|
||||
|
||||
# Write configuration file
|
||||
|
||||
echo "<?php return array(" \
|
||||
" 'uploads' => false, " \
|
||||
" 'interactive_run' => $INTERACTIVE, " \
|
||||
" 'cache_dir' => __DIR__.'/cache', " \
|
||||
" 'uploads_dir' => __DIR__.'/uploads', " \
|
||||
" 'mode' => '$MODE', " \
|
||||
" 'client_authentication' => '$CLIENT_AUTH', " \
|
||||
" 'client_authentication_must_be_secure' => false, " \
|
||||
" 'global_cors_origins' => 'https://cloudobjects.io', " \
|
||||
" 'co.auth_ns' => '$CO_AUTH_NS', " \
|
||||
" 'co.auth_secret' => '$CO_AUTH_SECRET', " \
|
||||
" 'agws.data_cache' => 'file', " \
|
||||
" 'log.target' => 'errorlog', " \
|
||||
" 'log.level' => Monolog\Logger::INFO, " \
|
||||
" ); " > /var/www/app/config.php
|
||||
|
||||
# Make cache folder
|
||||
mkdir /var/www/app/cache
|
||||
chown lighttpd:lighttpd /var/www/app/cache
|
||||
|
||||
# Run start script from parent container
|
||||
|
||||
sh /tmp/start.sh
|
||||
@@ -0,0 +1,34 @@
|
||||
.PHONY: all clean
|
||||
|
||||
all: stacks phpmae.phar
|
||||
|
||||
clean:
|
||||
# Delete PHAR and stacks
|
||||
rm *.phar
|
||||
rm -rf stacks
|
||||
|
||||
composer.lock: composer.json
|
||||
# Updating Dependencies with Composer
|
||||
composer update -n -o
|
||||
|
||||
vendor: composer.lock
|
||||
# Installing Dependencies with Composer
|
||||
composer install -n -o
|
||||
|
||||
robo.phar:
|
||||
# Get a copy of robo
|
||||
wget http://robo.li/robo.phar
|
||||
|
||||
config.php: config.php.default
|
||||
# Use .default if no other config provided
|
||||
cp config.php.default config.php
|
||||
|
||||
stacks: robo.phar vendor
|
||||
# Create stack directory
|
||||
mkdir stacks
|
||||
# Install default stack
|
||||
php robo.phar install:stack coid://phpmae.dev/DefaultStack
|
||||
|
||||
phpmae.phar: phpmae.php config.php vendor RoboFile.php robo.phar
|
||||
# Building archive with robo
|
||||
php robo.phar phar
|
||||
Generated
+4680
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "phpmae",
|
||||
"version": "1.0.0",
|
||||
"description": "Static files for phpMAE interactive mode.",
|
||||
"dependencies": {
|
||||
"codemirror": "^5.60.0",
|
||||
"gulp": "^4.0.2",
|
||||
"gulp-concat": "^2.6.1",
|
||||
"gulp-css-purge": "^3.0.9",
|
||||
"gulp-sass": "^4.0.2",
|
||||
"gulp-uglify": "^3.0.2",
|
||||
"lodash.template": ">=4.5.0",
|
||||
"materialize-css": "^1.0.0",
|
||||
"streamqueue": "^1.1.2",
|
||||
"jquery": "^3.4.1"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/CloudObjects/phpMAE.git"
|
||||
},
|
||||
"author": "Lukas Rosenstock",
|
||||
"license": "MPL",
|
||||
"bugs": {
|
||||
"url": "https://github.com/CloudObjects/phpMAE/issues"
|
||||
},
|
||||
"homepage": "https://github.com/CloudObjects/phpMAE#readme"
|
||||
}
|
||||
+512
@@ -0,0 +1,512 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* (c) Markus Lanthaler <mail@markus-lanthaler.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace ML\JsonLD;
|
||||
|
||||
use stdClass as JsonObject;
|
||||
|
||||
/**
|
||||
* A Node represents a node in a JSON-LD graph.
|
||||
*
|
||||
* @author Markus Lanthaler <mail@markus-lanthaler.com>
|
||||
*/
|
||||
class Node implements NodeInterface, JsonLdSerializable
|
||||
{
|
||||
/** The @type constant. */
|
||||
const TYPE = '@type';
|
||||
|
||||
/**
|
||||
* @var GraphInterface The graph the node belongs to.
|
||||
*/
|
||||
private $graph;
|
||||
|
||||
/**
|
||||
* @var string The ID of the node
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var array An associative array holding all properties of the node except it's ID
|
||||
*/
|
||||
private $properties = array();
|
||||
|
||||
/**
|
||||
* An associative array holding all reverse properties of this node, i.e.,
|
||||
* a pointers to all nodes that link to this node.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $revProperties = array();
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param GraphInterface $graph The graph the node belongs to.
|
||||
* @param null|string $id The ID of the node.
|
||||
*/
|
||||
public function __construct(GraphInterface $graph, $id = null)
|
||||
{
|
||||
$this->graph = $graph;
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setType($type)
|
||||
{
|
||||
if ((null !== $type) && !($type instanceof NodeInterface)) {
|
||||
if (is_array($type)) {
|
||||
foreach ($type as $val) {
|
||||
if ((null !== $val) && !($val instanceof NodeInterface)) {
|
||||
throw new \InvalidArgumentException('type must be null, a Node, or an array of Nodes');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new \InvalidArgumentException('type must be null, a Node, or an array of Nodes');
|
||||
}
|
||||
}
|
||||
|
||||
$this->setProperty(self::TYPE, $type);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addType(NodeInterface $type)
|
||||
{
|
||||
$this->addPropertyValue(self::TYPE, $type);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeType(NodeInterface $type)
|
||||
{
|
||||
$this->removePropertyValue(self::TYPE, $type);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getType()
|
||||
{
|
||||
return $this->getProperty(self::TYPE);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNodesWithThisType()
|
||||
{
|
||||
if (null === ($nodes = $this->getReverseProperty(self::TYPE))) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return (is_array($nodes)) ? $nodes : array($nodes);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getGraph()
|
||||
{
|
||||
return $this->graph;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeFromGraph()
|
||||
{
|
||||
// Remove other node's properties and reverse properties pointing to
|
||||
// this node
|
||||
foreach ($this->revProperties as $property => $nodes) {
|
||||
foreach ($nodes as $node) {
|
||||
$node->removePropertyValue($property, $this);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->properties as $property => $values) {
|
||||
if (!is_array($values)) {
|
||||
$values = array($values);
|
||||
}
|
||||
|
||||
foreach ($values as $value) {
|
||||
if ($value instanceof NodeInterface) {
|
||||
$this->removePropertyValue($property, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$g = $this->graph;
|
||||
$this->graph = null;
|
||||
|
||||
$g->removeNode($this);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isBlankNode()
|
||||
{
|
||||
return ((null === $this->id) || ('_:' === substr($this->id, 0, 2)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setProperty($property, $value)
|
||||
{
|
||||
if (null === $value) {
|
||||
$this->removeProperty($property);
|
||||
} else {
|
||||
$this->doMergeIntoProperty((string) $property, array(), $value);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function addPropertyValue($property, $value)
|
||||
{
|
||||
$existing = (isset($this->properties[(string) $property]))
|
||||
? $this->properties[(string) $property]
|
||||
: array();
|
||||
|
||||
if (!is_array($existing)) {
|
||||
$existing = array($existing);
|
||||
}
|
||||
|
||||
$this->doMergeIntoProperty((string) $property, $existing, $value);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a value into a set of existing values.
|
||||
*
|
||||
* @param string $property The name of the property.
|
||||
* @param array $existingValues The existing values.
|
||||
* @param mixed $value The value to merge into the existing
|
||||
* values. This MUST NOT be an array.
|
||||
*
|
||||
* @throws \InvalidArgumentException If value is an array or an object
|
||||
* which is neither a language-tagged
|
||||
* string nor a typed value or a node.
|
||||
*/
|
||||
private function doMergeIntoProperty($property, $existingValues, $value)
|
||||
{
|
||||
if (is_a($value, \PHPSandbox\SandboxedString::class))
|
||||
$value = (string)$value;
|
||||
|
||||
// TODO: Handle lists!
|
||||
|
||||
if (null === $value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->isValidPropertyValue($value)) {
|
||||
throw new \InvalidArgumentException(
|
||||
'value must be a scalar, a node, a language-tagged string, or a typed value'
|
||||
);
|
||||
}
|
||||
|
||||
$normalizedValue = $this->normalizePropertyValue($value);
|
||||
|
||||
foreach ($existingValues as $existing) {
|
||||
if ($this->equalValues($existing, $normalizedValue)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$existingValues[] = $normalizedValue;
|
||||
|
||||
if (1 === count($existingValues)) {
|
||||
$existingValues = $existingValues[0];
|
||||
}
|
||||
|
||||
$this->properties[$property] = $existingValues;
|
||||
|
||||
if ($normalizedValue instanceof NodeInterface) {
|
||||
$value->addReverseProperty($property, $this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removeProperty($property)
|
||||
{
|
||||
if (!isset($this->properties[(string) $property])) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$values = is_array($this->properties[(string) $property])
|
||||
? $this->properties[(string) $property]
|
||||
: array($this->properties[(string) $property]);
|
||||
|
||||
foreach ($values as $value) {
|
||||
if ($value instanceof NodeInterface) {
|
||||
$value->removeReverseProperty((string) $property, $this);
|
||||
}
|
||||
}
|
||||
|
||||
unset($this->properties[(string) $property]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function removePropertyValue($property, $value)
|
||||
{
|
||||
if (!$this->isValidPropertyValue($value) || !isset($this->properties[(string) $property])) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
$normalizedValue = $this->normalizePropertyValue($value);
|
||||
|
||||
$values =& $this->properties[(string) $property];
|
||||
|
||||
if (!is_array($this->properties[(string) $property])) {
|
||||
$values = array($values);
|
||||
}
|
||||
|
||||
for ($i = 0, $length = count($values); $i < $length; $i++) {
|
||||
if ($this->equalValues($values[$i], $normalizedValue)) {
|
||||
if ($normalizedValue instanceof NodeInterface) {
|
||||
$normalizedValue->removeReverseProperty((string) $property, $this);
|
||||
}
|
||||
|
||||
unset($values[$i]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (0 === count($values)) {
|
||||
unset($this->properties[(string) $property]);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->properties[(string) $property] = array_values($values); // re-index the array
|
||||
|
||||
if (1 === count($this->properties[(string) $property])) {
|
||||
$this->properties[(string) $property] = $this->properties[(string) $property][0];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getProperties()
|
||||
{
|
||||
return $this->properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getProperty($property)
|
||||
{
|
||||
return (isset($this->properties[(string) $property]))
|
||||
? $this->properties[(string) $property]
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getReverseProperties()
|
||||
{
|
||||
$result = array();
|
||||
foreach ($this->revProperties as $key => $nodes) {
|
||||
$result[$key] = array_values($nodes);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getReverseProperty($property)
|
||||
{
|
||||
if (!isset($this->revProperties[(string) $property])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$result = array_values($this->revProperties[(string) $property]);
|
||||
|
||||
return (1 === count($result))
|
||||
? $result[0]
|
||||
: $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function equals(NodeInterface $other)
|
||||
{
|
||||
return $this === $other;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function toJsonLd($useNativeTypes = true)
|
||||
{
|
||||
$node = new \stdClass();
|
||||
|
||||
// Only label blank nodes if other nodes point to it
|
||||
if ((false === $this->isBlankNode()) || (count($this->getReverseProperties()) > 0)) {
|
||||
$node->{'@id'} = $this->getId();
|
||||
}
|
||||
|
||||
$properties = $this->getProperties();
|
||||
|
||||
foreach ($properties as $prop => $values) {
|
||||
if (false === is_array($values)) {
|
||||
$values = array($values);
|
||||
}
|
||||
|
||||
if (self::TYPE === $prop) {
|
||||
$node->{'@type'} = array();
|
||||
foreach ($values as $val) {
|
||||
$node->{'@type'}[] = $val->getId();
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$node->{$prop} = array();
|
||||
|
||||
foreach ($values as $value) {
|
||||
if ($value instanceof NodeInterface) {
|
||||
$ref = new \stdClass();
|
||||
$ref->{'@id'} = $value->getId();
|
||||
$node->{$prop}[] = $ref;
|
||||
} elseif (is_object($value)) { // language-tagged string or typed value
|
||||
$node->{$prop}[] = $value->toJsonLd($useNativeTypes);
|
||||
} else {
|
||||
$val = new JsonObject();
|
||||
$val->{'@value'} = $value;
|
||||
$node->{$prop}[] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a reverse property.
|
||||
*
|
||||
* @param string $property The name of the property.
|
||||
* @param NodeInterface $node The node which has a property pointing
|
||||
* to this node instance.
|
||||
*/
|
||||
protected function addReverseProperty($property, NodeInterface $node)
|
||||
{
|
||||
$this->revProperties[$property][$node->getId()] = $node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a reverse property.
|
||||
*
|
||||
* @param string $property The name of the property.
|
||||
* @param NodeInterface $node The node which has a property pointing
|
||||
* to this node instance.
|
||||
*/
|
||||
protected function removeReverseProperty($property, NodeInterface $node)
|
||||
{
|
||||
unset($this->revProperties[$property][$node->getId()]);
|
||||
|
||||
if (0 === count($this->revProperties[$property])) {
|
||||
unset($this->revProperties[$property]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a value is a valid property value.
|
||||
*
|
||||
* @param mixed $value The value to check.
|
||||
*
|
||||
* @return bool Returns true if the value is a valid property value;
|
||||
* false otherwise.
|
||||
*/
|
||||
protected function isValidPropertyValue($value)
|
||||
{
|
||||
return (is_scalar($value) ||
|
||||
(is_object($value) &&
|
||||
((($value instanceof NodeInterface) && ($value->getGraph() === $this->graph)) ||
|
||||
($value instanceof Value))));
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a property value by converting scalars to Value objects.
|
||||
*
|
||||
* @param mixed $value The value to normalize.
|
||||
*
|
||||
* @return NodeInterface|Value The normalized value.
|
||||
*/
|
||||
protected function normalizePropertyValue($value)
|
||||
{
|
||||
if (false === is_scalar($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return Value::fromJsonLd((object) array('@value' => $value));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the two specified values are the same.
|
||||
*
|
||||
* Scalars and nodes are checked for identity, value objects for
|
||||
* equality.
|
||||
*
|
||||
* @param mixed $value1 Value 1.
|
||||
* @param mixed $value2 Value 2.
|
||||
*
|
||||
* @return bool Returns true if the two values are equals; otherwise false.
|
||||
*/
|
||||
protected function equalValues($value1, $value2)
|
||||
{
|
||||
if (gettype($value1) !== gettype($value2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_object($value1) && ($value1 instanceof Value)) {
|
||||
return $value1->equals($value2);
|
||||
}
|
||||
|
||||
return ($value1 === $value2);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
cp patch/Node.php vendor/ml/json-ld/Node.php
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
use CloudObjects\SDK\NodeReader, CloudObjects\SDK\ObjectRetriever;
|
||||
use ML\IRI\IRI;
|
||||
|
||||
/**
|
||||
* Implementation for coid://phpmae.dev/DirectoryTemplateVariableGenerator
|
||||
*/
|
||||
class DirectoryTemplateVariableGenerator implements DirectoryTemplateVariableGeneratorInterface {
|
||||
|
||||
const CO_PUBLIC = 'coid://cloudobjects.io/Public';
|
||||
|
||||
private $retriever;
|
||||
private $reader;
|
||||
|
||||
public function __construct(ObjectRetriever $retriever) {
|
||||
$this->retriever = $retriever;
|
||||
$this->reader = new NodeReader([
|
||||
'prefixes' => [
|
||||
'co' => 'coid://cloudobjects.io/',
|
||||
'phpmae' => 'coid://phpmae.dev/'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
public function getTemplateVariables($coid) {
|
||||
$coid = new IRI($coid);
|
||||
$object = $this->retriever->getObject($coid);
|
||||
if (!isset($object))
|
||||
return false;
|
||||
|
||||
$isInterface = $this->reader->hasType($object, 'phpmae:Interface');
|
||||
|
||||
// Retrieve source code
|
||||
$sourceUrl = $isInterface
|
||||
? $this->reader->getFirstValueString($object, 'phpmae:hasDefinitionFile')
|
||||
: $this->reader->getFirstValueString($object, 'phpmae:hasSourceFile');
|
||||
if (!$sourceUrl)
|
||||
return false;
|
||||
$sourceCode = $this->retriever->getAttachment($coid, $sourceUrl);
|
||||
|
||||
// Find method signatures and comment blocks using regular expression
|
||||
$matches = [];
|
||||
preg_match_all("/(?:\/\*\*((?:[\s\S](?!\/\*))*?)\*\/+\s*)?public\s+function\s+(\w+)\s*\((.*)\)/",
|
||||
$sourceCode, $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++) {
|
||||
// Remove * and whitespace from from comment string.
|
||||
$commentString = trim(preg_replace('/\n\s+\*/', "\n", $matches[1][$i]));
|
||||
|
||||
// Filter magic methods (incl. constructor), except __invoke
|
||||
if (substr($matches[2][$i], 0, 2) == '__' && $matches[2][$i] != '__invoke')
|
||||
continue;
|
||||
|
||||
// Get parameters and decode for test client
|
||||
$paramString = trim($matches[3][$i]);
|
||||
$paramsDecoded = [];
|
||||
$canRun = true;
|
||||
foreach (explode(',', $paramString) as $p) {
|
||||
if ($p == '') continue; // ignore empty parameter string
|
||||
$p = trim($p);
|
||||
if ($p[0] == '$') {
|
||||
// Parameter without type hint
|
||||
$paramsDecoded[] = [
|
||||
'type' => 'text',
|
||||
'name' => substr($p, 1)
|
||||
];
|
||||
} else {
|
||||
// Parameter with type hint
|
||||
$p = explode(' ', $p);
|
||||
$type = null;
|
||||
switch ($p[0]) {
|
||||
case "string":
|
||||
$type = 'text';
|
||||
}
|
||||
if (isset($type))
|
||||
$paramsDecoded[] = [
|
||||
'type' => $type,
|
||||
'name' => substr($p[1], 1)
|
||||
];
|
||||
else
|
||||
// Type hint points to an object which we cannot provide via JSON-RPC
|
||||
$canRun = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// List methods with parameters and comment
|
||||
$methods[] = [
|
||||
'name' => $matches[2][$i],
|
||||
'params' => $paramString,
|
||||
'fields' => $paramsDecoded,
|
||||
'can_run' => $canRun,
|
||||
'comment' => $commentString
|
||||
];
|
||||
}
|
||||
|
||||
$result = [ 'methods' => $methods ];
|
||||
|
||||
// Check if public, in that case include source code
|
||||
if (!$isInterface
|
||||
&& $this->reader->hasProperty($object, 'co:isVisibleTo')
|
||||
&& $this->reader->getFirstValueIRI($object, 'co:isVisibleTo')
|
||||
->equals(self::CO_PUBLIC)
|
||||
&& $this->reader->hasProperty($object, 'co:permitsUsageTo')
|
||||
&& $this->reader->getFirstValueIRI($object, 'co:permitsUsageTo')
|
||||
->equals(self::CO_PUBLIC))
|
||||
$result['public_source_code'] = $sourceCode;
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:phpmae="coid://phpmae.dev/"
|
||||
xmlns:co="coid://cloudobjects.io/">
|
||||
|
||||
<phpmae:Class rdf:about="coid://phpmae.dev/DirectoryTemplateVariableGenerator">
|
||||
<rdf:type rdf:resource="coid://website.cloudobjects.io/DirectoryTemplateVariableGeneratorInterface" />
|
||||
<co:isVisibleTo rdf:resource="coid://cloudobjects.io/Public"/>
|
||||
<co:permitsUsageTo rdf:resource="coid://cloudobjects.io/Public"/>
|
||||
<phpmae:hasSourceFile rdf:resource="file:///DirectoryTemplateVariableGenerator.php"/>
|
||||
</phpmae:Class>
|
||||
|
||||
</rdf:RDF>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
use CloudObjects\SDK\NodeReader, CloudObjects\SDK\ObjectRetriever;
|
||||
use ML\IRI\IRI;
|
||||
|
||||
/**
|
||||
* Implementation for coid://phpmae.dev/StackTemplateVariableGenerator
|
||||
* Using interface coid://cloudobjects.io/DirectoryTemplateVariableGeneratorInterface
|
||||
*/
|
||||
class StackTemplateVariableGenerator implements DirectoryTemplateVariableGeneratorInterface {
|
||||
|
||||
private $retriever;
|
||||
private $reader;
|
||||
|
||||
public function __construct(ObjectRetriever $retriever) {
|
||||
$this->retriever = $retriever;
|
||||
$this->reader = new NodeReader([
|
||||
'prefixes' => [
|
||||
'co' => 'coid://cloudobjects.io/',
|
||||
'phpmae' => 'coid://phpmae.dev/'
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a key-value array of template variables for an object.
|
||||
* @param string $coid The COID of the object.
|
||||
*/
|
||||
public function getTemplateVariables($coid) {
|
||||
$coid = new IRI($coid);
|
||||
$object = $this->retriever->getObject($coid);
|
||||
if (!isset($object) || !$this->reader->hasType($object, 'phpmae:Stack'))
|
||||
return false;
|
||||
|
||||
// Retrieve files
|
||||
$composerFile = json_decode($this->retriever->getAttachment($coid,
|
||||
$this->reader->getFirstValueString($object, 'phpmae:hasAttachedComposerFile')), true);
|
||||
$lockFile = json_decode($this->retriever->getAttachment($coid,
|
||||
$this->reader->getFirstValueString($object, 'phpmae:hasAttachedLockFile')), true);
|
||||
|
||||
// Compile output
|
||||
$output = [
|
||||
'defined' => [],
|
||||
'actual' => []
|
||||
];
|
||||
|
||||
foreach ($composerFile['require'] as $name => $version) {
|
||||
$output['defined'][] = [
|
||||
'name' => $name,
|
||||
'version' => $version
|
||||
];
|
||||
}
|
||||
|
||||
foreach ($lockFile['packages'] as $package) {
|
||||
$output['actual'][] = [
|
||||
'name' => $package['name'],
|
||||
'version' => $package['version'],
|
||||
'defined' => isset($composerFile['require'][$package['name']])
|
||||
];
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:phpmae="coid://phpmae.dev/"
|
||||
xmlns:co="coid://cloudobjects.io/">
|
||||
|
||||
<phpmae:Class rdf:about="coid://phpmae.dev/StackTemplateVariableGenerator">
|
||||
<rdf:type rdf:resource="coid://website.cloudobjects.io/DirectoryTemplateVariableGeneratorInterface"/>
|
||||
<co:isVisibleTo rdf:resource="coid://cloudobjects.io/Public"/>
|
||||
<co:permitsUsageTo rdf:resource="coid://cloudobjects.io/Public"/>
|
||||
<phpmae:hasSourceFile rdf:resource="file:///StackTemplateVariableGenerator.php"/>
|
||||
</phpmae:Class>
|
||||
|
||||
</rdf:RDF>
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
include("/var/www/app/phpmae.php");
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
<?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;
|
||||
|
||||
require_once __DIR__."/vendor/autoload.php";
|
||||
$app = new \Symfony\Component\Console\Application('phpMAE', '1.0.0');
|
||||
|
||||
$app->add(new ClassCreateCommand);
|
||||
$app->add(new ClassDeployCommand);
|
||||
$app->add(new ClassValidateCommand);
|
||||
$app->add(new ClassTestEnvCommand);
|
||||
$app->add(new ClassTestEnvRPCCommand);
|
||||
|
||||
$app->add(new DependenciesAddClassCommand);
|
||||
$app->add(new DependenciesAddWebAPICommand);
|
||||
$app->add(new DependenciesAddStaticTextCommand);
|
||||
$app->add(new DependenciesAddAttachmentCommand);
|
||||
$app->add(new DependenciesAddTemplateCommand);
|
||||
|
||||
$app->add(new InterfaceCreateCommand);
|
||||
$app->add(new InterfaceDeployCommand);
|
||||
$app->add(new InterfaceValidateCommand);
|
||||
|
||||
$app->add(new TestEnvironmentStartCommand);
|
||||
$app->run();
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
syntaxCheck="false"
|
||||
bootstrap="./tests/bootstrap.php">
|
||||
<testsuites>
|
||||
<testsuite name="Default">
|
||||
<directory>./tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,377 @@
|
||||
// Custom Color
|
||||
|
||||
$co-primary: (
|
||||
"base" : #7976FF
|
||||
);
|
||||
|
||||
// Google Color Palette defined: http://www.google.com/design/spec/style/color.html
|
||||
|
||||
$materialize-red: (
|
||||
"base": #e51c23,
|
||||
"lighten-5": #fdeaeb,
|
||||
"lighten-4": #f8c1c3,
|
||||
"lighten-3": #f3989b,
|
||||
"lighten-2": #ee6e73,
|
||||
"lighten-1": #ea454b,
|
||||
"darken-1": #d0181e,
|
||||
"darken-2": #b9151b,
|
||||
"darken-3": #a21318,
|
||||
"darken-4": #8b1014,
|
||||
);
|
||||
|
||||
$red: (
|
||||
"base": #F44336,
|
||||
"lighten-5": #FFEBEE,
|
||||
"lighten-4": #FFCDD2,
|
||||
"lighten-3": #EF9A9A,
|
||||
"lighten-2": #E57373,
|
||||
"lighten-1": #EF5350,
|
||||
"darken-1": #E53935,
|
||||
"darken-2": #D32F2F,
|
||||
"darken-3": #C62828,
|
||||
"darken-4": #B71C1C,
|
||||
"accent-1": #FF8A80,
|
||||
"accent-2": #FF5252,
|
||||
"accent-3": #FF1744,
|
||||
"accent-4": #D50000
|
||||
);
|
||||
|
||||
$pink: (
|
||||
"base": #e91e63,
|
||||
"lighten-5": #fce4ec,
|
||||
"lighten-4": #f8bbd0,
|
||||
"lighten-3": #f48fb1,
|
||||
"lighten-2": #f06292,
|
||||
"lighten-1": #ec407a,
|
||||
"darken-1": #d81b60,
|
||||
"darken-2": #c2185b,
|
||||
"darken-3": #ad1457,
|
||||
"darken-4": #880e4f,
|
||||
"accent-1": #ff80ab,
|
||||
"accent-2": #ff4081,
|
||||
"accent-3": #f50057,
|
||||
"accent-4": #c51162
|
||||
);
|
||||
|
||||
$purple: (
|
||||
"base": #9c27b0,
|
||||
"lighten-5": #f3e5f5,
|
||||
"lighten-4": #e1bee7,
|
||||
"lighten-3": #ce93d8,
|
||||
"lighten-2": #ba68c8,
|
||||
"lighten-1": #ab47bc,
|
||||
"darken-1": #8e24aa,
|
||||
"darken-2": #7b1fa2,
|
||||
"darken-3": #6a1b9a,
|
||||
"darken-4": #4a148c,
|
||||
"accent-1": #ea80fc,
|
||||
"accent-2": #e040fb,
|
||||
"accent-3": #d500f9,
|
||||
"accent-4": #aa00ff
|
||||
);
|
||||
|
||||
$deep-purple: (
|
||||
"base": #673ab7,
|
||||
"lighten-5": #ede7f6,
|
||||
"lighten-4": #d1c4e9,
|
||||
"lighten-3": #b39ddb,
|
||||
"lighten-2": #9575cd,
|
||||
"lighten-1": #7e57c2,
|
||||
"darken-1": #5e35b1,
|
||||
"darken-2": #512da8,
|
||||
"darken-3": #4527a0,
|
||||
"darken-4": #311b92,
|
||||
"accent-1": #b388ff,
|
||||
"accent-2": #7c4dff,
|
||||
"accent-3": #651fff,
|
||||
"accent-4": #6200ea
|
||||
);
|
||||
|
||||
$indigo: (
|
||||
"base": #3f51b5,
|
||||
"lighten-5": #e8eaf6,
|
||||
"lighten-4": #c5cae9,
|
||||
"lighten-3": #9fa8da,
|
||||
"lighten-2": #7986cb,
|
||||
"lighten-1": #5c6bc0,
|
||||
"darken-1": #3949ab,
|
||||
"darken-2": #303f9f,
|
||||
"darken-3": #283593,
|
||||
"darken-4": #1a237e,
|
||||
"accent-1": #8c9eff,
|
||||
"accent-2": #536dfe,
|
||||
"accent-3": #3d5afe,
|
||||
"accent-4": #304ffe
|
||||
);
|
||||
|
||||
$blue: (
|
||||
"base": #2196F3,
|
||||
"lighten-5": #E3F2FD,
|
||||
"lighten-4": #BBDEFB,
|
||||
"lighten-3": #90CAF9,
|
||||
"lighten-2": #64B5F6,
|
||||
"lighten-1": #42A5F5,
|
||||
"darken-1": #1E88E5,
|
||||
"darken-2": #1976D2,
|
||||
"darken-3": #1565C0,
|
||||
"darken-4": #0D47A1,
|
||||
"accent-1": #82B1FF,
|
||||
"accent-2": #448AFF,
|
||||
"accent-3": #2979FF,
|
||||
"accent-4": #2962FF
|
||||
);
|
||||
|
||||
$light-blue: (
|
||||
"base": #03a9f4,
|
||||
"lighten-5": #e1f5fe,
|
||||
"lighten-4": #b3e5fc,
|
||||
"lighten-3": #81d4fa,
|
||||
"lighten-2": #4fc3f7,
|
||||
"lighten-1": #29b6f6,
|
||||
"darken-1": #039be5,
|
||||
"darken-2": #0288d1,
|
||||
"darken-3": #0277bd,
|
||||
"darken-4": #01579b,
|
||||
"accent-1": #80d8ff,
|
||||
"accent-2": #40c4ff,
|
||||
"accent-3": #00b0ff,
|
||||
"accent-4": #0091ea
|
||||
);
|
||||
|
||||
$cyan: (
|
||||
"base": #00bcd4,
|
||||
"lighten-5": #e0f7fa,
|
||||
"lighten-4": #b2ebf2,
|
||||
"lighten-3": #80deea,
|
||||
"lighten-2": #4dd0e1,
|
||||
"lighten-1": #26c6da,
|
||||
"darken-1": #00acc1,
|
||||
"darken-2": #0097a7,
|
||||
"darken-3": #00838f,
|
||||
"darken-4": #006064,
|
||||
"accent-1": #84ffff,
|
||||
"accent-2": #18ffff,
|
||||
"accent-3": #00e5ff,
|
||||
"accent-4": #00b8d4
|
||||
);
|
||||
|
||||
$teal: (
|
||||
"base": #009688,
|
||||
"lighten-5": #e0f2f1,
|
||||
"lighten-4": #b2dfdb,
|
||||
"lighten-3": #80cbc4,
|
||||
"lighten-2": #4db6ac,
|
||||
"lighten-1": #26a69a,
|
||||
"darken-1": #00897b,
|
||||
"darken-2": #00796b,
|
||||
"darken-3": #00695c,
|
||||
"darken-4": #004d40,
|
||||
"accent-1": #a7ffeb,
|
||||
"accent-2": #64ffda,
|
||||
"accent-3": #1de9b6,
|
||||
"accent-4": #00bfa5
|
||||
);
|
||||
|
||||
$green: (
|
||||
"base": #4CAF50,
|
||||
"lighten-5": #E8F5E9,
|
||||
"lighten-4": #C8E6C9,
|
||||
"lighten-3": #A5D6A7,
|
||||
"lighten-2": #81C784,
|
||||
"lighten-1": #66BB6A,
|
||||
"darken-1": #43A047,
|
||||
"darken-2": #388E3C,
|
||||
"darken-3": #2E7D32,
|
||||
"darken-4": #1B5E20,
|
||||
"accent-1": #B9F6CA,
|
||||
"accent-2": #69F0AE,
|
||||
"accent-3": #00E676,
|
||||
"accent-4": #00C853
|
||||
);
|
||||
|
||||
$light-green: (
|
||||
"base": #8bc34a,
|
||||
"lighten-5": #f1f8e9,
|
||||
"lighten-4": #dcedc8,
|
||||
"lighten-3": #c5e1a5,
|
||||
"lighten-2": #aed581,
|
||||
"lighten-1": #9ccc65,
|
||||
"darken-1": #7cb342,
|
||||
"darken-2": #689f38,
|
||||
"darken-3": #558b2f,
|
||||
"darken-4": #33691e,
|
||||
"accent-1": #ccff90,
|
||||
"accent-2": #b2ff59,
|
||||
"accent-3": #76ff03,
|
||||
"accent-4": #64dd17
|
||||
);
|
||||
|
||||
$lime: (
|
||||
"base": #cddc39,
|
||||
"lighten-5": #f9fbe7,
|
||||
"lighten-4": #f0f4c3,
|
||||
"lighten-3": #e6ee9c,
|
||||
"lighten-2": #dce775,
|
||||
"lighten-1": #d4e157,
|
||||
"darken-1": #c0ca33,
|
||||
"darken-2": #afb42b,
|
||||
"darken-3": #9e9d24,
|
||||
"darken-4": #827717,
|
||||
"accent-1": #f4ff81,
|
||||
"accent-2": #eeff41,
|
||||
"accent-3": #c6ff00,
|
||||
"accent-4": #aeea00
|
||||
);
|
||||
|
||||
$yellow: (
|
||||
"base": #ffeb3b,
|
||||
"lighten-5": #fffde7,
|
||||
"lighten-4": #fff9c4,
|
||||
"lighten-3": #fff59d,
|
||||
"lighten-2": #fff176,
|
||||
"lighten-1": #ffee58,
|
||||
"darken-1": #fdd835,
|
||||
"darken-2": #fbc02d,
|
||||
"darken-3": #f9a825,
|
||||
"darken-4": #f57f17,
|
||||
"accent-1": #ffff8d,
|
||||
"accent-2": #ffff00,
|
||||
"accent-3": #ffea00,
|
||||
"accent-4": #ffd600
|
||||
);
|
||||
|
||||
$amber: (
|
||||
"base": #ffc107,
|
||||
"lighten-5": #fff8e1,
|
||||
"lighten-4": #ffecb3,
|
||||
"lighten-3": #ffe082,
|
||||
"lighten-2": #ffd54f,
|
||||
"lighten-1": #ffca28,
|
||||
"darken-1": #ffb300,
|
||||
"darken-2": #ffa000,
|
||||
"darken-3": #ff8f00,
|
||||
"darken-4": #ff6f00,
|
||||
"accent-1": #ffe57f,
|
||||
"accent-2": #ffd740,
|
||||
"accent-3": #ffc400,
|
||||
"accent-4": #ffab00
|
||||
);
|
||||
|
||||
$orange: (
|
||||
"base": #ff9800,
|
||||
"lighten-5": #fff3e0,
|
||||
"lighten-4": #ffe0b2,
|
||||
"lighten-3": #ffcc80,
|
||||
"lighten-2": #ffb74d,
|
||||
"lighten-1": #ffa726,
|
||||
"darken-1": #fb8c00,
|
||||
"darken-2": #f57c00,
|
||||
"darken-3": #ef6c00,
|
||||
"darken-4": #e65100,
|
||||
"accent-1": #ffd180,
|
||||
"accent-2": #ffab40,
|
||||
"accent-3": #ff9100,
|
||||
"accent-4": #ff6d00
|
||||
);
|
||||
|
||||
$deep-orange: (
|
||||
"base": #ff5722,
|
||||
"lighten-5": #fbe9e7,
|
||||
"lighten-4": #ffccbc,
|
||||
"lighten-3": #ffab91,
|
||||
"lighten-2": #ff8a65,
|
||||
"lighten-1": #ff7043,
|
||||
"darken-1": #f4511e,
|
||||
"darken-2": #e64a19,
|
||||
"darken-3": #d84315,
|
||||
"darken-4": #bf360c,
|
||||
"accent-1": #ff9e80,
|
||||
"accent-2": #ff6e40,
|
||||
"accent-3": #ff3d00,
|
||||
"accent-4": #dd2c00
|
||||
);
|
||||
|
||||
$brown: (
|
||||
"base": #795548,
|
||||
"lighten-5": #efebe9,
|
||||
"lighten-4": #d7ccc8,
|
||||
"lighten-3": #bcaaa4,
|
||||
"lighten-2": #a1887f,
|
||||
"lighten-1": #8d6e63,
|
||||
"darken-1": #6d4c41,
|
||||
"darken-2": #5d4037,
|
||||
"darken-3": #4e342e,
|
||||
"darken-4": #3e2723
|
||||
);
|
||||
|
||||
$blue-grey: (
|
||||
"base": #607d8b,
|
||||
"lighten-5": #eceff1,
|
||||
"lighten-4": #cfd8dc,
|
||||
"lighten-3": #b0bec5,
|
||||
"lighten-2": #90a4ae,
|
||||
"lighten-1": #78909c,
|
||||
"darken-1": #546e7a,
|
||||
"darken-2": #455a64,
|
||||
"darken-3": #37474f,
|
||||
"darken-4": #263238
|
||||
);
|
||||
|
||||
$grey: (
|
||||
"base": #9e9e9e,
|
||||
"lighten-5": #fafafa,
|
||||
"lighten-4": #f5f5f5,
|
||||
"lighten-3": #eeeeee,
|
||||
"lighten-2": #e0e0e0,
|
||||
"lighten-1": #bdbdbd,
|
||||
"darken-1": #757575,
|
||||
"darken-2": #616161,
|
||||
"darken-3": #424242,
|
||||
"darken-4": #212121
|
||||
);
|
||||
|
||||
$shades: (
|
||||
"black": #000000,
|
||||
"white": #FFFFFF,
|
||||
"transparent": transparent
|
||||
);
|
||||
|
||||
$colors: (
|
||||
"materialize-red": $materialize-red,
|
||||
"red": $red,
|
||||
"pink": $pink,
|
||||
"purple": $purple,
|
||||
"deep-purple": $deep-purple,
|
||||
"indigo": $indigo,
|
||||
"blue": $blue,
|
||||
"light-blue": $light-blue,
|
||||
"cyan": $cyan,
|
||||
"teal": $teal,
|
||||
"green": $green,
|
||||
"light-green": $light-green,
|
||||
"lime": $lime,
|
||||
"yellow": $yellow,
|
||||
"amber": $amber,
|
||||
"orange": $orange,
|
||||
"deep-orange": $deep-orange,
|
||||
"brown": $brown,
|
||||
"blue-grey": $blue-grey,
|
||||
"grey": $grey,
|
||||
"shades": $shades,
|
||||
"co-primary": $co-primary
|
||||
) !default;
|
||||
|
||||
|
||||
// usage: color("name_of_color", "type_of_color")
|
||||
// to avoid to repeating map-get($colors, ...)
|
||||
|
||||
@function color($color, $type) {
|
||||
@if map-has-key($colors, $color) {
|
||||
$curr_color: map-get($colors, $color);
|
||||
@if map-has-key($curr_color, $type) {
|
||||
@return map-get($curr_color, $type);
|
||||
}
|
||||
}
|
||||
@warn "Unknown `#{$color}` - `#{$type}` in $colors.";
|
||||
@return null;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
// Color
|
||||
@import "materialize-colors.scss";
|
||||
@import "../node_modules/materialize-css/sass/components/color-classes";
|
||||
|
||||
// Variables;
|
||||
@import "materialize-variables";
|
||||
|
||||
// Reset
|
||||
@import "../node_modules/materialize-css/sass/components/normalize";
|
||||
|
||||
// components
|
||||
@import "../node_modules/materialize-css/sass/components/global";
|
||||
@import "../node_modules/materialize-css/sass/components/badges";
|
||||
@import "../node_modules/materialize-css/sass/components/icons-material-design";
|
||||
@import "../node_modules/materialize-css/sass/components/grid";
|
||||
@import "../node_modules/materialize-css/sass/components/navbar";
|
||||
@import "../node_modules/materialize-css/sass/components/typography";
|
||||
@import "../node_modules/materialize-css/sass/components/transitions";
|
||||
@import "../node_modules/materialize-css/sass/components/cards";
|
||||
@import "../node_modules/materialize-css/sass/components/toast";
|
||||
@import "../node_modules/materialize-css/sass/components/tabs";
|
||||
@import "../node_modules/materialize-css/sass/components/tooltip";
|
||||
@import "../node_modules/materialize-css/sass/components/buttons";
|
||||
@import "../node_modules/materialize-css/sass/components/dropdown";
|
||||
@import "../node_modules/materialize-css/sass/components/waves";
|
||||
@import "../node_modules/materialize-css/sass/components/modal";
|
||||
@import "../node_modules/materialize-css/sass/components/collapsible";
|
||||
@import "../node_modules/materialize-css/sass/components/chips";
|
||||
@import "../node_modules/materialize-css/sass/components/materialbox";
|
||||
@import "../node_modules/materialize-css/sass/components/forms/forms";
|
||||
@import "../node_modules/materialize-css/sass/components/table_of_contents";
|
||||
@import "../node_modules/materialize-css/sass/components/sidenav";
|
||||
@import "../node_modules/materialize-css/sass/components/preloader";
|
||||
@import "../node_modules/materialize-css/sass/components/slider";
|
||||
// @import "../node_modules/materialize-css/sass/components/carousel"; - currently not used
|
||||
@import "../node_modules/materialize-css/sass/components/tapTarget";
|
||||
@import "../node_modules/materialize-css/sass/components/pulse";
|
||||
// @import "../node_modules/materialize-css/sass/components/datepicker"; - currently not used
|
||||
// @import "../node_modules/materialize-css/sass/components/timepicker"; - currently not used
|
||||
@@ -0,0 +1,350 @@
|
||||
// ==========================================================================
|
||||
// Materialize variables
|
||||
// ==========================================================================
|
||||
//
|
||||
// Table of Contents:
|
||||
//
|
||||
// 1. Colors
|
||||
// 2. Badges
|
||||
// 3. Buttons
|
||||
// 4. Cards
|
||||
// 5. Carousel
|
||||
// 6. Collapsible
|
||||
// 7. Chips
|
||||
// 8. Date + Time Picker
|
||||
// 9. Dropdown
|
||||
// 10. Forms
|
||||
// 11. Global
|
||||
// 12. Grid
|
||||
// 13. Navigation Bar
|
||||
// 14. Side Navigation
|
||||
// 15. Photo Slider
|
||||
// 16. Spinners | Loaders
|
||||
// 17. Tabs
|
||||
// 18. Tables
|
||||
// 19. Toasts
|
||||
// 20. Typography
|
||||
// 21. Footer
|
||||
// 22. Flow Text
|
||||
// 23. Collections
|
||||
// 24. Progress Bar
|
||||
|
||||
|
||||
|
||||
// 1. Colors
|
||||
// ==========================================================================
|
||||
|
||||
$primary-color: color("co-primary", "base") !default;
|
||||
$primary-color-light: lighten($primary-color, 15%) !default;
|
||||
$primary-color-dark: darken($primary-color, 15%) !default;
|
||||
|
||||
$secondary-color: color("co-primary", "base") !default;
|
||||
// origina: $secondary-color: color("teal", "lighten-1") !default;
|
||||
$success-color: color("green", "base") !default;
|
||||
$error-color: color("red", "base") !default;
|
||||
$link-color: color("light-blue", "darken-1") !default;
|
||||
|
||||
|
||||
// 2. Badges
|
||||
// ==========================================================================
|
||||
|
||||
$badge-bg-color: $secondary-color !default;
|
||||
$badge-height: 22px !default;
|
||||
|
||||
|
||||
// 3. Buttons
|
||||
// ==========================================================================
|
||||
|
||||
// Shared styles
|
||||
$button-border: none !default;
|
||||
$button-background-focus: lighten($secondary-color, 4%) !default;
|
||||
$button-font-size: 14px !default;
|
||||
$button-icon-font-size: 1.3rem !default;
|
||||
$button-height: 36px !default;
|
||||
$button-padding: 0 16px !default;
|
||||
$button-radius: 2px !default;
|
||||
|
||||
// Disabled styles
|
||||
$button-disabled-background: #DFDFDF !default;
|
||||
$button-disabled-color: #9F9F9F !default;
|
||||
|
||||
// Raised buttons
|
||||
$button-raised-background: $secondary-color !default;
|
||||
$button-raised-background-hover: lighten($button-raised-background, 5%) !default;
|
||||
$button-raised-color: #fff !default;
|
||||
|
||||
// Large buttons
|
||||
$button-large-font-size: 15px !default;
|
||||
$button-large-icon-font-size: 1.6rem !default;
|
||||
$button-large-height: $button-height * 1.5 !default;
|
||||
$button-floating-large-size: 56px !default;
|
||||
|
||||
// Small buttons
|
||||
$button-small-font-size: 13px !default;
|
||||
$button-small-icon-font-size: 1.2rem !default;
|
||||
$button-small-height: $button-height * .9 !default;
|
||||
$button-floating-small-size: $button-height * .9 !default;
|
||||
|
||||
// Flat buttons
|
||||
$button-flat-color: #343434 !default;
|
||||
$button-flat-disabled-color: lighten(#999, 10%) !default;
|
||||
|
||||
// Floating buttons
|
||||
$button-floating-background: $secondary-color !default;
|
||||
$button-floating-background-hover: $button-floating-background !default;
|
||||
$button-floating-color: #fff !default;
|
||||
$button-floating-size: 40px !default;
|
||||
$button-floating-radius: 50% !default;
|
||||
|
||||
|
||||
// 4. Cards
|
||||
// ==========================================================================
|
||||
|
||||
$card-padding: 24px !default;
|
||||
$card-bg-color: #fff !default;
|
||||
$card-link-color: color("orange", "accent-2") !default;
|
||||
$card-link-color-light: lighten($card-link-color, 20%) !default;
|
||||
|
||||
|
||||
// 5. Carousel
|
||||
// ==========================================================================
|
||||
|
||||
$carousel-height: 400px !default;
|
||||
$carousel-item-height: $carousel-height / 2 !default;
|
||||
$carousel-item-width: $carousel-item-height !default;
|
||||
|
||||
|
||||
// 6. Collapsible
|
||||
// ==========================================================================
|
||||
|
||||
$collapsible-height: 3rem !default;
|
||||
$collapsible-line-height: $collapsible-height !default;
|
||||
$collapsible-header-color: #fff !default;
|
||||
$collapsible-border-color: #ddd !default;
|
||||
|
||||
|
||||
// 7. Chips
|
||||
// ==========================================================================
|
||||
|
||||
$chip-bg-color: #e4e4e4 !default;
|
||||
$chip-border-color: #9e9e9e !default;
|
||||
$chip-selected-color: #26a69a !default;
|
||||
$chip-margin: 5px !default;
|
||||
|
||||
|
||||
// 8. Date + Time Picker
|
||||
// ==========================================================================
|
||||
|
||||
$datepicker-display-font-size: 2.8rem;
|
||||
$datepicker-calendar-header-color: #999;
|
||||
$datepicker-weekday-color: rgba(0, 0, 0, .87) !default;
|
||||
$datepicker-weekday-bg: darken($secondary-color, 7%) !default;
|
||||
$datepicker-date-bg: $secondary-color !default;
|
||||
$datepicker-year: rgba(255, 255, 255, .7) !default;
|
||||
$datepicker-focus: rgba(0,0,0, .05) !default;
|
||||
$datepicker-selected: $secondary-color !default;
|
||||
$datepicker-selected-outfocus: desaturate(lighten($secondary-color, 35%), 15%) !default;
|
||||
$datepicker-day-focus: transparentize(desaturate($secondary-color, 5%), .75) !default;
|
||||
$datepicker-disabled-day-color: rgba(0, 0, 0, .3) !default;
|
||||
|
||||
$timepicker-clock-color: rgba(0, 0, 0, .87) !default;
|
||||
$timepicker-clock-plate-bg: #eee !default;
|
||||
|
||||
|
||||
// 9. Dropdown
|
||||
// ==========================================================================
|
||||
|
||||
$dropdown-bg-color: #fff !default;
|
||||
$dropdown-hover-bg-color: #eee !default;
|
||||
$dropdown-color: $secondary-color !default;
|
||||
$dropdown-item-height: 50px !default;
|
||||
|
||||
|
||||
// 10. Forms
|
||||
// ==========================================================================
|
||||
|
||||
// Text Inputs + Textarea
|
||||
$input-height: 3rem !default;
|
||||
$input-border-color: color("grey", "base") !default;
|
||||
$input-border: 1px solid $input-border-color !default;
|
||||
$input-background: #fff !default;
|
||||
$input-error-color: $error-color !default;
|
||||
$input-success-color: $success-color !default;
|
||||
$input-focus-color: $secondary-color !default;
|
||||
$input-font-size: 16px !default;
|
||||
$input-margin-bottom: 8px;
|
||||
$input-margin: 0 0 $input-margin-bottom 0 !default;
|
||||
$input-padding: 0 !default;
|
||||
$label-font-size: .8rem !default;
|
||||
$input-disabled-color: rgba(0,0,0, .42) !default;
|
||||
$input-disabled-solid-color: #949494 !default;
|
||||
$input-disabled-border: 1px dotted $input-disabled-color !default;
|
||||
$input-invalid-border: 1px solid $input-error-color !default;
|
||||
$input-icon-size: 2rem;
|
||||
$placeholder-text-color: lighten($input-border-color, 20%) !default;
|
||||
|
||||
// Radio Buttons
|
||||
$radio-fill-color: $secondary-color !default;
|
||||
$radio-empty-color: #5a5a5a !default;
|
||||
$radio-border: 2px solid $radio-fill-color !default;
|
||||
|
||||
// Range
|
||||
$range-height: 14px !default;
|
||||
$range-width: 14px !default;
|
||||
$track-height: 3px !default;
|
||||
|
||||
// Select
|
||||
$select-border: 1px solid #f2f2f2 !default;
|
||||
$select-background: rgba(255, 255, 255, 0.90) !default;
|
||||
$select-focus: 1px solid lighten($secondary-color, 47%) !default;
|
||||
$select-option-hover: rgba(0,0,0,.08) !default;
|
||||
$select-option-focus: rgba(0,0,0,.08) !default;
|
||||
$select-option-selected: rgba(0,0,0,.03) !default;
|
||||
$select-padding: 5px !default;
|
||||
$select-radius: 2px !default;
|
||||
$select-disabled-color: rgba(0,0,0,.3) !default;
|
||||
|
||||
// Switches
|
||||
$switch-bg-color: $secondary-color !default;
|
||||
$switch-checked-lever-bg: desaturate(lighten($switch-bg-color, 25%), 25%) !default;
|
||||
$switch-unchecked-bg: #F1F1F1 !default;
|
||||
$switch-unchecked-lever-bg: rgba(0,0,0,.38) !default;
|
||||
$switch-radius: 15px !default;
|
||||
|
||||
|
||||
// 11. Global
|
||||
// ==========================================================================
|
||||
|
||||
// Media Query Ranges
|
||||
$small-screen-up: 601px !default;
|
||||
$medium-screen-up: 993px !default;
|
||||
$large-screen-up: 1201px !default;
|
||||
$small-screen: 600px !default;
|
||||
$medium-screen: 992px !default;
|
||||
$large-screen: 1200px !default;
|
||||
|
||||
$medium-and-up: "only screen and (min-width : #{$small-screen-up})" !default;
|
||||
$large-and-up: "only screen and (min-width : #{$medium-screen-up})" !default;
|
||||
$extra-large-and-up: "only screen and (min-width : #{$large-screen-up})" !default;
|
||||
$small-and-down: "only screen and (max-width : #{$small-screen})" !default;
|
||||
$medium-and-down: "only screen and (max-width : #{$medium-screen})" !default;
|
||||
$medium-only: "only screen and (min-width : #{$small-screen-up}) and (max-width : #{$medium-screen})" !default;
|
||||
|
||||
|
||||
// 12. Grid
|
||||
// ==========================================================================
|
||||
|
||||
$num-cols: 12 !default;
|
||||
$gutter-width: 1.5rem !default;
|
||||
$element-top-margin: $gutter-width/3 !default;
|
||||
$element-bottom-margin: ($gutter-width*2)/3 !default;
|
||||
|
||||
|
||||
// 13. Navigation Bar
|
||||
// ==========================================================================
|
||||
|
||||
$navbar-height: 64px !default;
|
||||
$navbar-line-height: $navbar-height !default;
|
||||
$navbar-height-mobile: 56px !default;
|
||||
$navbar-line-height-mobile: $navbar-height-mobile !default;
|
||||
$navbar-font-size: 1rem !default;
|
||||
$navbar-font-color: #fff !default;
|
||||
$navbar-brand-font-size: 2.1rem !default;
|
||||
|
||||
// 14. Side Navigation
|
||||
// ==========================================================================
|
||||
|
||||
$sidenav-width: 300px !default;
|
||||
$sidenav-font-size: 14px !default;
|
||||
$sidenav-font-color: rgba(0,0,0,.87) !default;
|
||||
$sidenav-bg-color: #fff !default;
|
||||
$sidenav-padding: 16px !default;
|
||||
$sidenav-item-height: 48px !default;
|
||||
$sidenav-line-height: $sidenav-item-height !default;
|
||||
|
||||
|
||||
// 15. Photo Slider
|
||||
// ==========================================================================
|
||||
|
||||
$slider-bg-color: color('grey', 'base') !default;
|
||||
$slider-bg-color-light: color('grey', 'lighten-2') !default;
|
||||
$slider-indicator-color: color('green', 'base') !default;
|
||||
|
||||
|
||||
// 16. Spinners | Loaders
|
||||
// ==========================================================================
|
||||
|
||||
$spinner-default-color: $secondary-color !default;
|
||||
|
||||
|
||||
// 17. Tabs
|
||||
// ==========================================================================
|
||||
|
||||
$tabs-underline-color: $primary-color-light !default;
|
||||
$tabs-text-color: $primary-color !default;
|
||||
$tabs-bg-color: #fff !default;
|
||||
|
||||
|
||||
// 18. Tables
|
||||
// ==========================================================================
|
||||
|
||||
$table-border-color: rgba(0,0,0,.12) !default;
|
||||
$table-striped-color: rgba(242, 242, 242, 0.5) !default;
|
||||
|
||||
|
||||
// 19. Toasts
|
||||
// ==========================================================================
|
||||
|
||||
$toast-height: 48px !default;
|
||||
$toast-color: #323232 !default;
|
||||
$toast-text-color: #fff !default;
|
||||
$toast-action-color: #eeff41;
|
||||
|
||||
|
||||
// 20. Typography
|
||||
// ==========================================================================
|
||||
|
||||
$font-stack: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif !default;
|
||||
$off-black: rgba(0, 0, 0, 0.87) !default;
|
||||
// Header Styles
|
||||
$h1-fontsize: 4.2rem !default;
|
||||
$h2-fontsize: 3.56rem !default;
|
||||
$h3-fontsize: 2.92rem !default;
|
||||
$h4-fontsize: 2.28rem !default;
|
||||
$h5-fontsize: 1.64rem !default;
|
||||
$h6-fontsize: 1.15rem !default;
|
||||
|
||||
|
||||
// 21. Footer
|
||||
// ==========================================================================
|
||||
|
||||
$footer-font-color: #fff !default;
|
||||
$footer-bg-color: $primary-color !default;
|
||||
$footer-copyright-font-color: rgba(255,255,255,.8) !default;
|
||||
$footer-copyright-bg-color: rgba(51,51,51,.08) !default;
|
||||
|
||||
|
||||
// 22. Flow Text
|
||||
// ==========================================================================
|
||||
|
||||
$range : $large-screen - $small-screen !default;
|
||||
$intervals: 20 !default;
|
||||
$interval-size: $range / $intervals !default;
|
||||
|
||||
|
||||
// 23. Collections
|
||||
// ==========================================================================
|
||||
|
||||
$collection-border-color: #e0e0e0 !default;
|
||||
$collection-bg-color: #fff !default;
|
||||
$collection-active-bg-color: $secondary-color !default;
|
||||
$collection-active-color: lighten($secondary-color, 55%) !default;
|
||||
$collection-hover-bg-color: #ddd !default;
|
||||
$collection-link-color: $secondary-color !default;
|
||||
$collection-line-height: 1.5rem !default;
|
||||
|
||||
|
||||
// 24. Progress Bar
|
||||
// ==========================================================================
|
||||
|
||||
$progress-bar-color: $secondary-color !default;
|
||||
@@ -0,0 +1,15 @@
|
||||
#phpmae-main-grid {
|
||||
margin:15px 0;
|
||||
}
|
||||
|
||||
#phpmae-result {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#phpmae-source-editor, #phpmae-config-editor {
|
||||
margin:15px 0;
|
||||
}
|
||||
|
||||
.phpmae-signedout-only, .phpmae-signedin-only {
|
||||
display:none;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>phpMAE</title>
|
||||
<link rel="stylesheet" type="text/css" href="styles.css" />
|
||||
<link rel="stylesheet" type="text/css" href="codemirror.css" />
|
||||
<script type="application/javascript" src="https://webcdn.co-n.net/phpmae.dev/InteractiveRunController/wtm.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<ul id="phpmae-templates-menu" class="dropdown-content">
|
||||
<li><a href="javascript:phpMAE.loadTemplate('helloworld');">Hello World<i class="material-icons">article</i></a></li>
|
||||
<li><a href="javascript:phpMAE.loadTemplate('mathdemo');">Math Demo<i class="material-icons">article</i></a></li>
|
||||
</ul>
|
||||
<ul id="phpmae-help-menu" class="dropdown-content">
|
||||
<li><a href="https://github.com/CloudObjects/phpMAE" target="_blank">GitHub<i class="material-icons">open_in_new</i></a></li>
|
||||
<li><a href="https://cloudobjects.io/phpmae.dev" target="_blank">CloudObjects Directory<i class="material-icons">open_in_new</i></a></li>
|
||||
</ul>
|
||||
<div class="nav-wrapper">
|
||||
<div class="brand-logo right">phpMAE</div>
|
||||
<ul class="left">
|
||||
<li class="phpmae-signedin-only"><a class="modal-trigger" href="#phpmae-load">Load<i class="material-icons right">download</i></a></li>
|
||||
<li><a class="dropdown-trigger" href="#!" data-target="phpmae-templates-menu">Templates<i class="material-icons right">arrow_drop_down</i></a></li>
|
||||
<li><a class="dropdown-trigger" href="#!" data-target="phpmae-help-menu">Help<i class="material-icons right">arrow_drop_down</i></a></li>
|
||||
<li class="phpmae-signedout-only"><a href="javascript:phpMAE.signin()">Sign in with CloudObjects<i class="material-icons right">login</i></a></li>
|
||||
<li class="phpmae-signedin-only"><a href="javascript:phpMAE.signout()">Sign out<i class="material-icons right">logout</i></a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="phpmae-main-grid" class="row">
|
||||
<div class="phpmae-element col s8">
|
||||
<ul class="tabs">
|
||||
<li class="tab"><a class="active" href="#phpmae-source-editor">PHP Code</a></li>
|
||||
<li class="tab"><a href="#phpmae-config-editor">Configuration</a></li>
|
||||
</ul>
|
||||
<div id="phpmae-source-editor"></div>
|
||||
<div id="phpmae-config-editor"></div>
|
||||
</div>
|
||||
<div class="phpmae-element col s4">
|
||||
<form onsubmit="phpMAE.execute(this); return false;" class="row">
|
||||
<div class="input-field col s12">
|
||||
<label>Class</label>
|
||||
<input name="class" type="text" readonly="readonly" value="" />
|
||||
</div>
|
||||
<div class="input-field col s12">
|
||||
<select name="function"></select>
|
||||
<label>Function</label>
|
||||
</div>
|
||||
<div id="phpmae-parameter-container"></div>
|
||||
<div class="input-field col s12">
|
||||
<button type="submit" class="waves-effect waves-light btn">Execute</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id="phpmae-result"></div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="application/javascript" src="scripts.js"></script>
|
||||
<div id="phpmae-load" class="modal">
|
||||
<form id="phpmae-load-form">
|
||||
<div class="modal-content">
|
||||
<h4>Load phpMAE class</h4>
|
||||
<div id="phpmae-load-form-step1" class="input-field">
|
||||
<select id="phpmae-load-form-domain">
|
||||
<option value="" selected>Choose your domain</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="phpmae-load-form-step2" style="display:none;" class="input-field">
|
||||
<select id="phpmae-load-form-object">
|
||||
<option value="" selected>Choose your object</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn waves-effect waves-light disabled" type="submit" name="action">Load object</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+265
@@ -0,0 +1,265 @@
|
||||
$(function() {
|
||||
var sessionId = sessionStorage.getItem('phpMaeSessionId');
|
||||
var functionMatcher = /(?:\/\*\*((?:[\s\S](?!\/\*))*?)\*\/+\s*)?public\s+function\s+(\w+)\s*\((.*)\)/g;
|
||||
var codeEditor = CodeMirror(document.getElementById('phpmae-source-editor'), {
|
||||
lineNumbers: false,
|
||||
matchBrackets: true,
|
||||
mode: "application/x-httpd-php",
|
||||
indentUnit: 4,
|
||||
indentWithTabs: false
|
||||
});
|
||||
var configEditor = CodeMirror(document.getElementById('phpmae-config-editor'), {
|
||||
lineNumbers: false,
|
||||
matchBrackets: true,
|
||||
mode: "application/xml",
|
||||
indentUnit: 4,
|
||||
indentWithTabs: false
|
||||
});
|
||||
var dirty = false;
|
||||
|
||||
function checkSignInStatus() {
|
||||
if (typeof(COWebApp) !== "undefined" && COWebApp.getAccountContext() !== null) {
|
||||
// Signed in
|
||||
$('.phpmae-signedout-only').hide();
|
||||
$('.phpmae-signedin-only').show();
|
||||
} else {
|
||||
// Signed out
|
||||
$('.phpmae-signedin-only').hide();
|
||||
$('.phpmae-signedout-only').show();
|
||||
}
|
||||
}
|
||||
|
||||
function updateParameters(functionName) {
|
||||
var sourceCode = codeEditor.getValue();
|
||||
var container = $('#phpmae-parameter-container');
|
||||
var parametersHtml = "";
|
||||
do {
|
||||
functionMatch = functionMatcher.exec(sourceCode);
|
||||
if (functionMatch && functionMatch[2] == functionName) {
|
||||
var parameters = functionMatch[3].split(",");
|
||||
for (var p in parameters) {
|
||||
var parameter = parameters[p].trim();
|
||||
if (parameter[0] != "$" || parameter.length < 2) continue;
|
||||
var noPrefix = parameter.substr(1);
|
||||
var oldValue = container.find('[name=' + noPrefix + ']').val();
|
||||
parametersHtml += '<div class="input-field col s12">'
|
||||
+ '<label for="' + noPrefix + '">' + parameter + '</label>'
|
||||
+ '<input name="' + noPrefix+ '" type="text" value="'
|
||||
+ (typeof(oldValue) != 'undefined' ? oldValue : '') + '" />'
|
||||
+ '</div>';
|
||||
}
|
||||
}
|
||||
} while (functionMatch);
|
||||
container.html(parametersHtml);
|
||||
M.updateTextFields();
|
||||
}
|
||||
|
||||
window.phpMAE = {
|
||||
execute : function(form) {
|
||||
form = $(form);
|
||||
form.find('button').addClass('disabled');
|
||||
|
||||
var request = {
|
||||
config : configEditor.getValue(),
|
||||
sourceCode : codeEditor.getValue(),
|
||||
class : form.find('[name=class]').val(),
|
||||
method : form.find('[name=function]').val(),
|
||||
params : {}
|
||||
};
|
||||
if (sessionId != null) request.session = sessionId;
|
||||
var parameters = form.find('input');
|
||||
for (var p in parameters)
|
||||
request.params[parameters[p].name] = parameters[p].value;
|
||||
|
||||
fetch('/run', {
|
||||
method : 'POST',
|
||||
headers : { 'Content-Type' : 'application/json' },
|
||||
body : JSON.stringify(request)
|
||||
}).then(function(response) {
|
||||
response.json().then(function(data) {
|
||||
if (data.session != sessionId) {
|
||||
sessionId = data.session;
|
||||
sessionStorage.setItem('phpMaeSessionId', sessionId);
|
||||
}
|
||||
$('#phpmae-result')
|
||||
.text((typeof(data.content) == "object")
|
||||
? JSON.stringify(data.content, null, 2)
|
||||
: data.content)
|
||||
.css('color', (data.status == "error") ? "#ff0000" : "#000000");
|
||||
form.find('button').removeClass('disabled');
|
||||
});
|
||||
});
|
||||
},
|
||||
loadTemplate : function(template) {
|
||||
if (dirty == false || window.confirm("Do you want to load this template? This will delete your code changes!"))
|
||||
fetch('/templates/' + template + '.txt').then(function (response) {
|
||||
response.text().then(function(data) {
|
||||
var template = data.split("\n----\n");
|
||||
if (template.length !== 2) {
|
||||
alert("Invalid template file!");
|
||||
return;
|
||||
}
|
||||
codeEditor.setValue(template[0]);
|
||||
configEditor.setValue(template[1]);
|
||||
|
||||
// Add class name
|
||||
var classDefinition = template[0].match(/class\s+(\w+)/);
|
||||
$('input[name="class"]').val(
|
||||
(classDefinition.length > 1 ? classDefinition[1] : "")
|
||||
);
|
||||
|
||||
// Build executor function list
|
||||
var functionOptionsHtml = "";
|
||||
var firstFunction = null;
|
||||
do {
|
||||
functionMatch = functionMatcher.exec(template[0]);
|
||||
if (functionMatch && functionMatch[2] != "__construct") {
|
||||
firstFunction = firstFunction || functionMatch[2];
|
||||
functionOptionsHtml += '<option value="' + functionMatch[2] + '">'
|
||||
+ functionMatch[2] + '()</option>';
|
||||
}
|
||||
} while (functionMatch);
|
||||
$('select[name=function]').html(functionOptionsHtml).formSelect();
|
||||
updateParameters(firstFunction);
|
||||
|
||||
// Clear output
|
||||
$('#phpmae-result').text('');
|
||||
|
||||
dirty = false;
|
||||
});
|
||||
});
|
||||
},
|
||||
signin : function() {
|
||||
alert("Coming soon!")
|
||||
},
|
||||
signout : function() {
|
||||
COWebApp.signOut();
|
||||
}
|
||||
};
|
||||
|
||||
codeEditor.on("change", function(instance, change) {
|
||||
dirty = true;
|
||||
var line = instance.getLine(change.from.line);
|
||||
if (line.indexOf("class") > -1) {
|
||||
var classDefinition = instance.getValue().match(/class\s+(\w+)/);
|
||||
$('input[name="class"]').val(
|
||||
(classDefinition.length > 1 ? classDefinition[1] : "")
|
||||
);
|
||||
} else
|
||||
if (line.indexOf("function") > -1) {
|
||||
var sourceCode = instance.getValue();
|
||||
var selector = $('select[name=function]');
|
||||
var currentlySelected = selector.val();
|
||||
var previousExists = false;
|
||||
var functionOptionsHtml = "";
|
||||
do {
|
||||
functionMatch = functionMatcher.exec(sourceCode);
|
||||
if (functionMatch && functionMatch[2] != "__construct") {
|
||||
previousExists = previousExists || (functionMatch[2] == currentlySelected);
|
||||
functionOptionsHtml += '<option value="' + functionMatch[2] + '"'
|
||||
+ (functionMatch[2] == currentlySelected ? ' selected="selected"' : '')
|
||||
+ '>' + functionMatch[2] + '()</option>';
|
||||
}
|
||||
} while (functionMatch);
|
||||
selector.html(functionOptionsHtml).formSelect();
|
||||
updateParameters(selector.val());
|
||||
}
|
||||
});
|
||||
|
||||
configEditor.on("change", function(instance, change) {
|
||||
dirty = true;
|
||||
});
|
||||
|
||||
$('.tabs').tabs({
|
||||
onShow : function() {
|
||||
codeEditor.refresh();
|
||||
configEditor.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
$('select[name=function]').on('change', function() {
|
||||
updateParameters($(this).val());
|
||||
}).formSelect();
|
||||
|
||||
$('.dropdown-trigger').dropdown({
|
||||
constrainWidth : false,
|
||||
coverTrigger : false
|
||||
});
|
||||
|
||||
function fixHeight() {
|
||||
$('.CodeMirror').css('height', ($(window).height() - $('.CodeMirror').offset().top - 30) + 'px');
|
||||
}
|
||||
|
||||
$(window).on('resize', fixHeight);
|
||||
fixHeight();
|
||||
checkSignInStatus();
|
||||
phpMAE.loadTemplate('helloworld');
|
||||
|
||||
// Initialize loader modal
|
||||
$('#phpmae-load').modal({
|
||||
onOpenStart : function() {
|
||||
var domainSelector = $('#phpmae-load-form-domain');
|
||||
var objectSelector = $('#phpmae-load-form-domain-object');
|
||||
var step2 = $('#phpmae-load-form-step2');
|
||||
var coAgwClient = COWebApp.getAccountContext().getClient();
|
||||
|
||||
// Get domains and create list in domain selector field
|
||||
domainSelector.attr('disabled', true).formSelect();
|
||||
|
||||
coAgwClient.get('/dr/').then(function(response) {
|
||||
response.data.domains.forEach(function(domain) {
|
||||
domainSelector.append('<option value="'
|
||||
+ domain + '">' + domain + '</option>');
|
||||
});
|
||||
|
||||
domainSelector.attr('disabled', false)
|
||||
.formSelect(); // re-render with Materialize UI
|
||||
});
|
||||
|
||||
domainSelector.on('change', function() {
|
||||
if (domainSelector.val() != "") {
|
||||
step2.show();
|
||||
|
||||
// Reset input and view
|
||||
objectSelector.html('<option value="" selected>Choose your object</option>')
|
||||
.attr('disabled', true)
|
||||
.formSelect();
|
||||
|
||||
// Get objects and create list in object selector field
|
||||
coAgwClient.get('/ws/' + domainSelector.val() + '/all.jsonld?type='
|
||||
+ encodeURIComponent('coid://phpmae.dev/Class') + '&jsonld_format=expanded')
|
||||
.then(function(response) {
|
||||
currentDomainDoc = LD(response.data, {
|
||||
'rdfs' : 'http://www.w3.org/2000/01/rdf-schema#'
|
||||
});
|
||||
|
||||
var ids = currentDomainDoc.queryAll('[@type=' + options.domain + '] > @id');
|
||||
if (ids.length == 1) {
|
||||
// Single result is automatically selected
|
||||
var label = currentDomainDoc
|
||||
.query('[@id=' + ids[0] + '] > rdfs:label @value');
|
||||
objectSelector.html('<option value="'
|
||||
+ ids[0] + '" selected>'
|
||||
+ (label ? label + ' (' + ids[0] + ')' : ids[0])
|
||||
+ '</option>');
|
||||
objectSelector.trigger('change');
|
||||
} else {
|
||||
// Multiple results give options
|
||||
ids.forEach(function(id) {
|
||||
var label = currentDomainDoc
|
||||
.query('[@id=' + id + '] > rdfs:label @value');
|
||||
objectSelector.append('<option value="'
|
||||
+ id + '">' + (label ? label + ' (' + id + ')' : id) + '</option>');
|
||||
});
|
||||
}
|
||||
|
||||
objectSelector.attr('disabled', false)
|
||||
.formSelect(); // re-render with Materialize UI
|
||||
});
|
||||
} else {
|
||||
step2.hide();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>phpMAE</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>phpMAE</h1>
|
||||
<p>This is a <a href="https://github.com/CloudObjects/phpMAE">phpMAE</a> instance.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
class HelloWorld {
|
||||
|
||||
public function hello($name) {
|
||||
return "Hello ".$name."!";
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:phpmae="coid://phpmae.dev/">
|
||||
|
||||
<phpmae:Class rdf:about="coid://playground.phpmae/HelloWorld">
|
||||
</phpmae:Class>
|
||||
|
||||
</rdf:RDF>
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
class MathDemo {
|
||||
|
||||
public function add($a, $b) {
|
||||
return $a + $b;
|
||||
}
|
||||
|
||||
public function substract($a, $b) {
|
||||
return $a - $b;
|
||||
}
|
||||
|
||||
public function multiply($a, $b) {
|
||||
return $a * $b;
|
||||
}
|
||||
|
||||
public function divide($a, $b) {
|
||||
return $a / $b;
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:phpmae="coid://phpmae.dev/">
|
||||
|
||||
<phpmae:Class rdf:about="coid://playground.phpmae/MathDemo">
|
||||
</phpmae:Class>
|
||||
|
||||
</rdf:RDF>
|
||||
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
Phar::mapPhar('');
|
||||
require 'phar://' . __FILE__ . '/phpmae.php';
|
||||
__HALT_COMPILER();
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace CloudObjects\PhpMAE;
|
||||
|
||||
use ML\IRI\IRI;
|
||||
|
||||
class ClassValidatorTest extends \PHPUnit_Framework_TestCase {
|
||||
|
||||
private $validator;
|
||||
|
||||
private function loadFromFile($filename) {
|
||||
return file_get_contents(__DIR__.'/fixtures/'.$filename);
|
||||
}
|
||||
|
||||
protected function setUp() {
|
||||
$this->validator = new ClassValidator;
|
||||
}
|
||||
|
||||
public function testValidate() {
|
||||
$this->validator->validate($this->loadFromFile('class1.php'),
|
||||
new IRI('coid://example.com/TestClass1'));
|
||||
}
|
||||
|
||||
public function testNonWhitelistedClass() {
|
||||
$this->expectException(\PHPSandbox\Error::class);
|
||||
$this->validator->validate($this->loadFromFile('class2.php'),
|
||||
new IRI('coid://example.com/TestClass2'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__."/../vendor/autoload.php";
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
class TestClass1 {
|
||||
|
||||
public function __invoke() {
|
||||
return "Hello World!";
|
||||
}
|
||||
|
||||
}
|
||||
Vendored
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
class TestClass2 {
|
||||
|
||||
public function __invoke() {
|
||||
$x = new ClassValidator;
|
||||
return "Hello World!";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
date_default_timezone_set('UTC');
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
$container = Configurator::getContainer(require __DIR__.'/../config.php');
|
||||
|
||||
$mode = $container->get('mode');
|
||||
if ($mode == 'default') {
|
||||
// Default mode
|
||||
$container->get(Engine::class)->run();
|
||||
} elseif (substr($mode, 0, 7) == 'router:' || $mode == 'hybrid') {
|
||||
// Router or hybrid mode
|
||||
$container->get(Router::class)->run();
|
||||
} else {
|
||||
// Error
|
||||
echo "Invalid mode!";
|
||||
}
|
||||
Reference in New Issue
Block a user