From 3b0272a19646be7665f840d70a711781a5840b98 Mon Sep 17 00:00:00 2001 From: Pierre Date: Wed, 17 May 2017 14:05:23 +0200 Subject: [PATCH] Implemented RestServer controller --- 3rdparty/.htaccess | 1 + 3rdparty/RestServer/RestException.php | 36 ++ 3rdparty/RestServer/RestFormat.php | 46 +++ 3rdparty/RestServer/RestServer.php | 526 ++++++++++++++++++++++++++ RestControllers/changesController.php | 28 -- RestControllers/listsController.php | 82 ---- RestControllers/sitesController.php | 56 --- RestControllers/welcomeController.php | 6 +- index.php | 31 ++ 9 files changed, 642 insertions(+), 170 deletions(-) create mode 100755 3rdparty/.htaccess create mode 100755 3rdparty/RestServer/RestException.php create mode 100755 3rdparty/RestServer/RestFormat.php create mode 100755 3rdparty/RestServer/RestServer.php delete mode 100644 RestControllers/changesController.php delete mode 100644 RestControllers/listsController.php delete mode 100644 RestControllers/sitesController.php diff --git a/3rdparty/.htaccess b/3rdparty/.htaccess new file mode 100755 index 0000000..14249c5 --- /dev/null +++ b/3rdparty/.htaccess @@ -0,0 +1 @@ +Deny from all \ No newline at end of file diff --git a/3rdparty/RestServer/RestException.php b/3rdparty/RestServer/RestException.php new file mode 100755 index 0000000..d8344be --- /dev/null +++ b/3rdparty/RestServer/RestException.php @@ -0,0 +1,36 @@ + RestFormat::PLAIN, + 'txt' => RestFormat::PLAIN, + 'html' => RestFormat::HTML, + 'json' => RestFormat::JSON, + 'xml' => RestFormat::XML, + ); +} \ No newline at end of file diff --git a/3rdparty/RestServer/RestServer.php b/3rdparty/RestServer/RestServer.php new file mode 100755 index 0000000..d2f0d3d --- /dev/null +++ b/3rdparty/RestServer/RestServer.php @@ -0,0 +1,526 @@ +mode = $mode; + $this->realm = $realm; + // Set the root + $dir = str_replace('\\', '/', dirname(str_replace($_SERVER['DOCUMENT_ROOT'], '', $_SERVER['SCRIPT_FILENAME']))); + if ($dir == '.') { + $dir = '/'; + } else { + // add a slash at the beginning and end + if (substr($dir, -1) != '/') $dir .= '/'; + if (substr($dir, 0, 1) != '/') $dir = '/' . $dir; + } + $this->root = $dir; + } + + public function __destruct() + { + if ($this->mode == 'production' && !$this->cached) { + if (function_exists('apc_store')) { + apc_store('urlMap', $this->map); + } else { + file_put_contents($this->cacheDir . '/urlMap.cache', serialize($this->map)); + } + } + } + + public function refreshCache() + { + $this->map = array(); + $this->cached = false; + } + + public function unauthorized($ask = false) + { + if ($ask) { + header("WWW-Authenticate: Basic realm=\"$this->realm\""); + } + throw new RestException(401, "You are not authorized to access this resource."); + } + + + public function handle() + { + $this->url = $this->getPath(); + $this->method = $this->getMethod(); + $this->format = $this->getFormat(); + + if ($this->method == 'PUT' || $this->method == 'POST' || $this->method == 'PATCH') { + $this->data = $this->getData(); + } + + list($obj, $method, $params, $this->params, $noAuth) = $this->findUrl(); + + if ($obj) { + if (is_string($obj)) { + if (class_exists($obj)) { + $obj = new $obj(); + } else { + throw new Exception("Class $obj does not exist"); + } + } + + $obj->server = $this; + + try { + if (method_exists($obj, 'init')) { + $obj->init(); + } + + if (!$noAuth && method_exists($obj, 'authorize')) { + if (!$obj->authorize()) { + $this->sendData($this->unauthorized(true)); //@todo unauthorized returns void + exit; + } + } + + $result = call_user_func_array(array($obj, $method), $params); + + if ($result !== null) { + $this->sendData($result); + } + } catch (RestException $e) { + $this->handleError($e->getCode(), $e->getMessage()); + } + + } else { + $this->handleError(404); + } + } + public function setRootPath($path) + { + $this->rootPath = '/'.trim($path, '/').'/'; + } + public function setJsonAssoc($value) + { + $this->jsonAssoc = ($value === true); + } + + public function addClass($class, $basePath = '') + { + $this->loadCache(); + + if (!$this->cached) { + if (is_string($class) && !class_exists($class)){ + throw new Exception('Invalid method or class'); + } elseif (!is_string($class) && !is_object($class)) { + throw new Exception('Invalid method or class; must be a classname or object'); + } + + if (substr($basePath, 0, 1) == '/') { + $basePath = substr($basePath, 1); + } + if ($basePath && substr($basePath, -1) != '/') { + $basePath .= '/'; + } + + $this->generateMap($class, $basePath); + } + } + + public function addErrorClass($class) + { + $this->errorClasses[] = $class; + } + + public function handleError($statusCode, $errorMessage = null) + { + $method = "handle$statusCode"; + foreach ($this->errorClasses as $class) { + if (is_object($class)) { + $reflection = new ReflectionObject($class); + } elseif (class_exists($class)) { + $reflection = new ReflectionClass($class); + } + + if (isset($reflection)) + { + if ($reflection->hasMethod($method)) + { + $obj = is_string($class) ? new $class() : $class; + $obj->$method(); + return; + } + } + } + + if (!$errorMessage) + { + $errorMessage = $this->codes[$statusCode]; + } + + $this->setStatus($statusCode); + $this->sendData(array('error' => array('code' => $statusCode, 'message' => $errorMessage))); + } + + protected function loadCache() + { + if ($this->cached !== null) { + return; + } + + $this->cached = false; + + if ($this->mode == 'production') { + if (function_exists('apc_fetch')) { + $map = apc_fetch('urlMap'); + } elseif (file_exists($this->cacheDir . '/urlMap.cache')) { + $map = unserialize(file_get_contents($this->cacheDir . '/urlMap.cache')); + } + if (isset($map) && is_array($map)) { + $this->map = $map; + $this->cached = true; + } + } else { + if (function_exists('apc_delete')) { + apc_delete('urlMap'); + } else { + @unlink($this->cacheDir . '/urlMap.cache'); + } + } + } + + protected function findUrl() + { + $urls = $this->map[$this->method]; + if (!$urls) return null; + + foreach ($urls as $url => $call) { + $args = $call[2]; + + if (!strstr($url, '$')) { + if ($url == $this->url) { + if (isset($args['data'])) { + $params = array_fill(0, $args['data'] + 1, null); + $params[$args['data']] = $this->data; //@todo data is not a property of this class + $call[2] = $params; + } else { + $call[2] = array(); + } + return $call; + } + } else { + $regex = preg_replace('/\\\\\$([\w\d]+)\.\.\./', '(?P<$1>.+)', str_replace('\.\.\.', '...', preg_quote($url))); + $regex = preg_replace('/\\\\\$([\w\d]+)/', '(?P<$1>[^\/]+)', $regex); + if (preg_match(":^$regex$:", urldecode($this->url), $matches)) { + $params = array(); + $paramMap = array(); + if (isset($args['data'])) { + $params[$args['data']] = $this->data; + } + + foreach ($matches as $arg => $match) { + if (is_numeric($arg)) continue; + $paramMap[$arg] = $match; + + if (isset($args[$arg])) { + $params[$args[$arg]] = $match; + } + } + ksort($params); + // make sure we have all the params we need + end($params); + $max = key($params); + for ($i = 0; $i < $max; $i++) { + if (!array_key_exists($i, $params)) { + $params[$i] = null; + } + } + ksort($params); + $call[2] = $params; + $call[3] = $paramMap; + return $call; + } + } + } + } + + protected function generateMap($class, $basePath) + { + if (is_object($class)) { + $reflection = new ReflectionObject($class); + } elseif (class_exists($class)) { + $reflection = new ReflectionClass($class); + } + + $methods = $reflection->getMethods(ReflectionMethod::IS_PUBLIC); //@todo $reflection might not be instantiated + + foreach ($methods as $method) { + $doc = $method->getDocComment(); + $noAuth = strpos($doc, '@noAuth') !== false; + if (preg_match_all('/@url[ \t]+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)[ \t]+\/?(\S*)/s', $doc, $matches, PREG_SET_ORDER)) { + + $params = $method->getParameters(); + + foreach ($matches as $match) { + $httpMethod = $match[1]; + $url = $basePath . $match[2]; + if ($url && $url[strlen($url) - 1] == '/') { + $url = substr($url, 0, -1); + } + $call = array($class, $method->getName()); + $args = array(); + foreach ($params as $param) { + $args[$param->getName()] = $param->getPosition(); + } + $call[] = $args; + $call[] = null; + $call[] = $noAuth; + + $this->map[$httpMethod][$url] = $call; + } + } + } + } + + public function getPath() + { + $path = preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']); + // remove root from path + if ($this->root) $path = preg_replace('/^' . preg_quote($this->root, '/') . '/', '', $path); + // remove trailing format definition, like /controller/action.json -> /controller/action + $path = preg_replace('/\.(\w+)$/i', '', $path); + // remove root path from path, like /root/path/api -> /api + if ($this->rootPath) $path = str_replace($this->rootPath, '', $path); + return $path; + } + + public function getMethod() + { + $method = $_SERVER['REQUEST_METHOD']; + $override = isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']) ? $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] : (isset($_GET['method']) ? $_GET['method'] : ''); + if ($method == 'POST' && strtoupper($override) == 'PUT') { + $method = 'PUT'; + } elseif ($method == 'POST' && strtoupper($override) == 'DELETE') { + $method = 'DELETE'; + } elseif ($method == 'POST' && strtoupper($override) == 'PATCH') { + $method = 'PATCH'; + } + return $method; + } + + public function getFormat() + { + $format = RestFormat::PLAIN; + $accept_mod = null; + if(isset($_SERVER["HTTP_ACCEPT"])) { + $accept_mod = preg_replace('/\s+/i', '', $_SERVER['HTTP_ACCEPT']); // ensures that exploding the HTTP_ACCEPT string does not get confused by whitespaces + } + $accept = explode(',', $accept_mod); + $override = ''; + + if (isset($_REQUEST['format']) || isset($_SERVER['HTTP_FORMAT'])) { + // give GET/POST precedence over HTTP request headers + $override = isset($_SERVER['HTTP_FORMAT']) ? $_SERVER['HTTP_FORMAT'] : ''; + $override = isset($_REQUEST['format']) ? $_REQUEST['format'] : $override; + $override = trim($override); + } + + // Check for trailing dot-format syntax like /controller/action.format -> action.json + if(preg_match('/\.(\w+)$/i', strtok($_SERVER["REQUEST_URI"],'?'), $matches)) { + $override = $matches[1]; + } + + // Give GET parameters precedence before all other options to alter the format + $override = isset($_GET['format']) ? $_GET['format'] : $override; + if (isset(RestFormat::$formats[$override])) { + $format = RestFormat::$formats[$override]; + } elseif (in_array(RestFormat::JSON, $accept)) { + $format = RestFormat::JSON; + } + return $format; + } + + public function getData() + { + $data = file_get_contents('php://input'); + $data = json_decode($data, $this->jsonAssoc); + + return $data; + } + + + public function sendData($data) + { + header("Cache-Control: no-cache, must-revalidate"); + header("Expires: 0"); + header('Content-Type: ' . $this->format); + + if ($this->format == RestFormat::XML) { + + if (is_object($data) && method_exists($data, '__keepOut')) { + $data = clone $data; + foreach ($data->__keepOut() as $prop) { + unset($data->$prop); + } + } + $this->xml_encode($data); + } else { + if (is_object($data) && method_exists($data, '__keepOut')) { + $data = clone $data; + foreach ($data->__keepOut() as $prop) { + unset($data->$prop); + } + } + $options = 0; + if ($this->mode == 'debug') { + $options = JSON_PRETTY_PRINT; + } + $options = $options | JSON_UNESCAPED_UNICODE; + echo json_encode($data, $options); + } + } + + public function setStatus($code) + { + if (function_exists('http_response_code')) { + http_response_code($code); + } else { + $protocol = $_SERVER['SERVER_PROTOCOL'] ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0'; + $code .= ' ' . $this->codes[strval($code)]; + header("$protocol $code"); + } + } + + private function xml_encode($mixed, $domElement=null, $DOMDocument=null) { //@todo add type hint for $domElement and $DOMDocument + if (is_null($DOMDocument)) { + $DOMDocument =new DOMDocument; + $DOMDocument->formatOutput = true; + $this->xml_encode($mixed, $DOMDocument, $DOMDocument); + echo $DOMDocument->saveXML(); + } + else { + if (is_array($mixed)) { + foreach ($mixed as $index => $mixedElement) { + if (is_int($index)) { + if ($index === 0) { + $node = $domElement; + } + else { + $node = $DOMDocument->createElement($domElement->tagName); + $domElement->parentNode->appendChild($node); + } + } + else { + $plural = $DOMDocument->createElement($index); + $domElement->appendChild($plural); + $node = $plural; + if (!(rtrim($index, 's') === $index)) { + $singular = $DOMDocument->createElement(rtrim($index, 's')); + $plural->appendChild($singular); + $node = $singular; + } + } + + $this->xml_encode($mixedElement, $node, $DOMDocument); + } + } + else { + $domElement->appendChild($DOMDocument->createTextNode($mixed)); + } + } + } + + + private $codes = array( + '100' => 'Continue', + '200' => 'OK', + '201' => 'Created', + '202' => 'Accepted', + '203' => 'Non-Authoritative Information', + '204' => 'No Content', + '205' => 'Reset Content', + '206' => 'Partial Content', + '300' => 'Multiple Choices', + '301' => 'Moved Permanently', + '302' => 'Found', + '303' => 'See Other', + '304' => 'Not Modified', + '305' => 'Use Proxy', + '307' => 'Temporary Redirect', + '400' => 'Bad Request', + '401' => 'Unauthorized', + '402' => 'Payment Required', + '403' => 'Forbidden', + '404' => 'Not Found', + '405' => 'Method Not Allowed', + '406' => 'Not Acceptable', + '409' => 'Conflict', + '410' => 'Gone', + '411' => 'Length Required', + '412' => 'Precondition Failed', + '413' => 'Request Entity Too Large', + '414' => 'Request-URI Too Long', + '415' => 'Unsupported Media Type', + '416' => 'Requested Range Not Satisfiable', + '417' => 'Expectation Failed', + '500' => 'Internal Server Error', + '501' => 'Not Implemented', + '503' => 'Service Unavailable' + ); +} diff --git a/RestControllers/changesController.php b/RestControllers/changesController.php deleted file mode 100644 index 444777d..0000000 --- a/RestControllers/changesController.php +++ /dev/null @@ -1,28 +0,0 @@ - $to*1) - Rest_fatal_error(401, "Please specify a valid interval !"); - - - //We try to get changes of the specified period - $changes = DW::get()->changes->get($from*1, $to*1); - if($changes === false) - Rest_fatal_error(500, "Couldn't get changes of the specified period !"); - - //Return the informations - return $changes; - } -} \ No newline at end of file diff --git a/RestControllers/listsController.php b/RestControllers/listsController.php deleted file mode 100644 index fb30a88..0000000 --- a/RestControllers/listsController.php +++ /dev/null @@ -1,82 +0,0 @@ -lists->getCurrent()) - Rest_fatal_error(500, "Couldn't get current list !"); - } - else { - //Get the list of the specified timestamp - if(!$list = DW::get()->lists->getOnTimestamp($time*1)) - Rest_fatal_error(500, "Couldn't get the list on specified timestamp !"); - } - - //Return the list - return $list; - } - - - /** - * Update the current list - * - * @url POST /list/update - */ - public function updateList(){ - - //Authentication required (protected method) - if(!DW::get()->auth->restAuth()) - Rest_fatal_error(401, "Authentication required !"); - - //Try to update list - if(!DW::get()->lists->update()) - Rest_fatal_error(500, "Couldn't update Decodex list !"); - - //Else it is a success - return array("success" => "This list was successfully updated !"); - } - - /** - * Get the list of available websites using urls - * - * @url GET /list/urls - */ - public function getListSites(){ - //We try to get the list of urls - if(!$list = DW::get()->lists->getListUrls()) - Rest_fatal_error(500, "Couldn't get the list of urls !"); - - //Return the list - return $list; - } - - /** - * Get the list of URLs only - * - * @url GET /list/urls/only - */ - public function getURLsOnly(){ - //We try to get the list of urls - if(!$list = DW::get()->lists->getListUrls(true)) - Rest_fatal_error(500, "Couldn't get the list of urls !"); - - //Return the list - return $list; - } -} \ No newline at end of file diff --git a/RestControllers/sitesController.php b/RestControllers/sitesController.php deleted file mode 100644 index d6dbfa1..0000000 --- a/RestControllers/sitesController.php +++ /dev/null @@ -1,56 +0,0 @@ -sites->getInfosFromURL($url)) - Rest_fatal_error(500, "Couldn't get informations about the URL !"); - - //Return the informations - return $infos; - } - - /** - * Get the informations history about a website given a URL - * - * @url GET /site/$url/history - * @url POST /site/history - */ - public function getInfosURLHistory($url=false){ - - //We check if the URL was passed in $_POST mode - if(!$url){ - if(!isset($_POST['url'])) - Rest_fatal_error(401, "Please specify an URL !"); - - $url = $_POST['url']; - } - - //We try to get informations about a websites using its URL - if(!$infos = DW::get()->sites->getInfosFromURL($url, 0)) - Rest_fatal_error(500, "Couldn't get history informations about the URL !"); - - //Return the informations - return $infos; - } -} \ No newline at end of file diff --git a/RestControllers/welcomeController.php b/RestControllers/welcomeController.php index a3f87d6..346756b 100644 --- a/RestControllers/welcomeController.php +++ b/RestControllers/welcomeController.php @@ -15,10 +15,8 @@ class welcomeController { */ public function getInfos(){ return array( - "serviceDescription" => "This service watches DecodexList evolutions, stores them and let its client access them.", - "githubURL" => "https://github.com/pierre42100/decodexwatcherapi/", - "clientURL" => "https://decodexwatcher.communiquons.org/", - "apiSchema" => "https://swagger.decodexwatcher.communiquons.org/" + "serviceDescription" => "This is the Comunic API Server.", + "clientURL" => "https://communiquons.org/", ); } diff --git a/index.php b/index.php index 06f6608..6689263 100644 --- a/index.php +++ b/index.php @@ -12,3 +12,34 @@ */ include(__DIR__."/init.php"); +//Include RestControllers +foreach(glob(PROJECT_PATH."RestControllers/*.php") as $restControllerFile){ + require_once $restControllerFile; +} + +//Include RestServer library +require PROJECT_PATH."3rdparty/RestServer/RestServer.php"; + +//Allow remote requests +header("Access-Control-Allow-Origin: *"); + +//By default format is json +if(!isset($_GET["format"])) + $_GET['format'] = "json"; + +/** + * Handle Rest requests + */ +$server = new \Jacwright\RestServer\RestServer($cs->config->get("site_mode")); + +//Include controllers +foreach(get_included_files() as $filePath){ + if(preg_match("", $filePath)){ + $className = strstr($filePath, "RestControllers/"); + $className = str_replace(array("RestControllers/", ".php"), "", $className); + $server->addClass($className); + } +} + +//Hanlde +$server->handle(); \ No newline at end of file