From 0290c106f2cca3a7d7ce7512591445d6955876da Mon Sep 17 00:00:00 2001 From: "S. Mohammad M. Ziabary" Date: Mon, 15 Aug 2016 23:33:04 +0430 Subject: [PATCH 1/5] New features added * URL queries can be passed to functions using $args keyword in function arguments. * You can define default value for arguments passed using URL queries by @param keyword in function documentation. * You can now define a parent dir in order to server REST API in sub-paths * temp directory has been defined as cache directory --- source/Jacwright/RestServer/RestServer.php | 1013 ++++++++++---------- 1 file changed, 533 insertions(+), 480 deletions(-) diff --git a/source/Jacwright/RestServer/RestServer.php b/source/Jacwright/RestServer/RestServer.php index d2f0d3d..0cf224a 100755 --- a/source/Jacwright/RestServer/RestServer.php +++ b/source/Jacwright/RestServer/RestServer.php @@ -41,486 +41,539 @@ */ class RestServer { - //@todo add type hint - public $url; - public $method; - public $params; - public $format; - public $cacheDir = __DIR__; - public $realm; - public $mode; - public $root; - public $rootPath; - public $jsonAssoc = false; - - protected $map = array(); - protected $errorClasses = array(); - protected $cached; - - /** - * The constructor. - * - * @param string $mode The mode, either debug or production - */ - public function __construct($mode = 'debug', $realm = 'Rest Server') - { - $this->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') { + //@todo add type hint + public $url; + public $method; + public $params; + public $format; + public $cacheDir = __DIR__.'/temp'; + public $parentDir; + public $realm; + public $mode; + public $root; + public $rootPath; + public $jsonAssoc = false; + + protected $map = array(); + protected $errorClasses = array(); + protected $cached; + + /** + * The constructor. + * + * @param string $mode The mode, either debug or production + */ + public function __construct($mode = 'debug', $realm = 'Rest Server', $parentDir = '/') + { + $this->mode = $mode; + $this->realm = $realm; + $this->parentDir = $parentDir; + // 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 { + if (!file_exists($this->cacheDir)) { + mkdir($this->cacheDir, 0777, true); + } + 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()); + }catch(Exception $e){ + $this->handleError(400, $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'); + } + } + } + + private function initCallArgsByRequestParams($call){ + $Args = array(); + if(is_array($call[5])){ + foreach($call[5] as $key => $value){ + if(isset($_REQUEST[$key])) + $Args[$key] = $_REQUEST[$key]; + else + $Args[$key] = $value['default']; + } + } + foreach($_REQUEST as $key => $value){ + if (isset($Args[$key]) == false) + $Args[$key] = $value; + } + return $Args; + } + + private function endsWith($haystack, $needle) { + // search forward starting from end minus needle length characters + return $needle === "" || (($temp = strlen($haystack) - strlen($needle)) >= 0 && strpos($haystack, $needle, $temp) !== false); + } + + protected function findUrl() + { + $this->url = substr($this->url, strpos($this->url, $this->parentDir) + strlen($this->parentDir)); + if ($this->endsWith($this->url, '/')) + $this->url = substr($this->url, 0, strlen($this->url) - 1); + + $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 + if (isset($args['args'])) + $call[2] = array($params, $this->initCallArgsByRequestParams($call)); + else + $call[2] = $params; + } elseif (isset($args['args'])) { + $call[2] = array($this->initCallArgsByRequestParams($call)); + } 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; + } + + if(isset($args['args'])){ + $params[$args['args']] = $this->initCallArgsByRequestParams($call); + } + + $paramMap['args'] = $params[$args['args']]; + 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; + $Params = null; + if(preg_match_all('/@param\s+(\w+)\s+(["\'])((?:.*?(\\\2)?)*)\2\s+(?:(["\'])((?:.*?(\\\2)?)*)\2|([^"\'\s]+))/s', $doc, $matches, PREG_SET_ORDER)){ + foreach ($matches as $match) { + $ParamInfo['help'] = $match[3]; + $ParamInfo['default'] = ($match[6] == '' ? (sizeof($match) >= 8 ? $match[8] : '') : $match[6]); + $Params[$match[1]] = $ParamInfo; + } + } + + 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; + $call[] = $Params; + + $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' - ); + 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' + ); } From c830533afdbc173afafb4f2e537c2fcf880ef50d Mon Sep 17 00:00:00 2001 From: "S. Mohammad M. Ziabary" Date: Mon, 15 Aug 2016 23:38:07 +0430 Subject: [PATCH 2/5] new test function added Sample method which can receive URL queries added --- example/TestController.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/example/TestController.php b/example/TestController.php index 4a0ae1b..1f08351 100644 --- a/example/TestController.php +++ b/example/TestController.php @@ -13,6 +13,21 @@ public function test() { return "Hello World"; } + + /** + * Returns a JSON array containing all arguments passed as URL Query. If you pass "test" argument it will be set to what + * you defined else it eill be set to "default_value" + * + * @url GET /wa + * + * @param test "Sample argument passed by URL Query" "default_value" + * @param bool "Sample boolean argument" true + */ + public function testWithArg($args = NULL) + { + return $args; + } + /** * Logs in a user with the given username and password POSTed. Though true @@ -81,4 +96,4 @@ public function getCharts($id=null, $date=null, $interval = 30, $interval_months public function throwError() { throw new RestException(401, "Empty password not allowed"); } -} \ No newline at end of file +} From 3de2d27cb3b48fb33d7bcdb7953a9817f2e7fa28 Mon Sep 17 00:00:00 2001 From: "S. Mohammad M. Ziabary" Date: Mon, 15 Aug 2016 23:42:36 +0430 Subject: [PATCH 3/5] Argument enabled method enhanced --- example/TestController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/example/TestController.php b/example/TestController.php index 1f08351..99e898a 100644 --- a/example/TestController.php +++ b/example/TestController.php @@ -19,11 +19,12 @@ public function test() * you defined else it eill be set to "default_value" * * @url GET /wa + * @url GET /wa/$par1 * * @param test "Sample argument passed by URL Query" "default_value" * @param bool "Sample boolean argument" true */ - public function testWithArg($args = NULL) + public function testWithArg($par1 = null, $args = NULL) { return $args; } From f33d81c916754f3f35b48f7bcd6953006ad69918 Mon Sep 17 00:00:00 2001 From: "S. Mohammad M. Ziabary" Date: Mon, 15 Aug 2016 23:45:05 +0430 Subject: [PATCH 4/5] Update TestController.php --- example/TestController.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/example/TestController.php b/example/TestController.php index 99e898a..699ca40 100644 --- a/example/TestController.php +++ b/example/TestController.php @@ -14,19 +14,19 @@ public function test() return "Hello World"; } - /** - * Returns a JSON array containing all arguments passed as URL Query. If you pass "test" argument it will be set to what - * you defined else it eill be set to "default_value" + /** + * Returns a JSON array containing all arguments passed as URL. If you pass "test" argument it will be set to what + * you defined else it will be set to "default_value", this is same on "bool" argument where default value is "true" * * @url GET /wa * @url GET /wa/$par1 - * + * * @param test "Sample argument passed by URL Query" "default_value" * @param bool "Sample boolean argument" true */ - public function testWithArg($par1 = null, $args = NULL) + public function testWithArg($par1 = null, $args = null) { - return $args; + return array($par1, $args); } From 17b1a1805349af05a7dddafb84b6436ee3efe571 Mon Sep 17 00:00:00 2001 From: "S. Mohammad M. Ziabary" Date: Mon, 15 Aug 2016 23:56:37 +0430 Subject: [PATCH 5/5] New features were documented --- README.md | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f875f55..f2791bb 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ class TestController { return "Hello World"; } - + /** * Logs in a user with the given username and password POSTed. Though true * REST doesn't believe in sessions, it is often desirable for an AJAX server. @@ -71,6 +71,21 @@ class TestController $user = User::saveUser($data); // saving the user to the database return $user; // returning the updated or newly created user object } + + /** + * Returns a JSON array containing all arguments passed as URL. If you pass "test" argument it will be set to what + * you defined else it will be set to "default_value", this is same on "bool" argument where default value is "true" + * + * @url GET /wa + * @url GET /wa/$par1 + * + * @param test "Sample argument passed by URL Query" "default_value" + * @param bool "Sample boolean argument" true + */ + public function testWithArg($par1 = null, $args = null) + { + return array($par1, $args); + } } ``` @@ -102,6 +117,19 @@ but POSTing a new user object for our saveUser method could look like this: So you’re able to allow POSTing JSON in addition to regular web style POSTs. +Last method is `testWithArg`, where you’ll notice there is a new kind of doc-comment tag in the docblock. `@param` maps a URL query to the method below it and is in the form: + +`@param ` + +All parameters passed to the URL as URL query will be stored in `$args` parameter but the two predefined parameters named `test` and `bool` will be set to their defined default value if not set when calling. for example requesting GET on http://www.example.com/wa?test=Sample%20text&extra=1 will set `test` equal to "Sample%20text" and let `bool` equal to true. So `$args` will be: +```php + array( + [test] => Sample text, + [bool] => true, + [extra] => 1 + ) +``` + I call these classes that handle the requests `Controllers`. And they can be completely self-contained with their URL mappings, database configs, etc. so that you could drop them into other RestServer services without any hassle. ### REST index.php @@ -116,7 +144,7 @@ $server = new RestServer($mode); // $server->refreshCache(); // uncomment momentarily to clear the cache if classes change in production mode $server->addClass('TestController'); -$server->addClass('ProductsController', '/products'); // adds this as a base to all the URLs in this class +$server->addClass('ProductsController', '/products',); // adds this as a base to all the URLs in this class $server->handle(); ``` @@ -196,4 +224,4 @@ composer install ``` cd composer require 'jacwright/restserver:dev-master' -``` \ No newline at end of file +```