objectRetriever = $objectRetriever; $this->classRepository = $classRepository; $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) { // Explicit arguments take precedence $input = $args; } elseif ($request->getMethod() == 'GET') { // Handle GET requests only if class allows it if ($this->reader->getFirstValueBool($this->object, 'phpmae:allowsGETRequests')) $input = $request->getQueryParams(); else return new Response(405); } elseif ($request->getMethod() == 'POST') { // POST is always allowed $input = $request->getParsedBody(); if (!is_array($input)) $input = []; } else return new Response(405); 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) == 'getBody()->write($content); } elseif (is_string($content) && (substr($lowercaseContent, 0, 7) == 'http://' || substr($lowercaseContent, 0, 8) == 'https://')) { // Redirect response $response = (new Response(302))->withHeader('Location', $content); } elseif (is_string($content) || is_numeric($content)) { // Plain text response $response = (new Response(200))->withHeader('Content-Type', 'text/plain'); $response->getBody()->write((string)$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(200))->withHeader('Content-Type', 'application/json'); $response->getBody()->write(json_encode($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 = $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(200); $response->getBody()->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->getObjectNode($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); $request = ServerRequestCreatorFactory::create()->createServerRequestFromGlobals(); try { $response = $this->execute($request); } catch (PhpMAEException $e) { // Create plain-text error response $response = (new Response(500))->withHeader('Content-Type', 'text/plain'); $response->getBody()->write($e->getMessage()); } (new ResponseEmitter())->emit($response); } }