212 lines
8.1 KiB
PHP
212 lines
8.1 KiB
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/. */
|
|
|
|
namespace CloudObjects\PhpMAE;
|
|
|
|
use CloudObjects\PhpMAE\Sandbox\CustomizedSandbox;
|
|
use PHPSandbox\SandboxWhitelistVisitor, PHPSandbox\ValidatorVisitor;
|
|
use PhpParser\ParserFactory, PhpParser\PhpVersion, 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\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)->createForVersion(PhpVersion::getHostVersion());
|
|
$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');
|
|
}
|
|
|
|
}
|