1455 lines
46 KiB
PHP
1455 lines
46 KiB
PHP
<?php
|
|
/**
|
|
* REST API Server. It is the server part of the Restler framework.
|
|
* Based on the RestServer code from <http://jacwright.com/blog/resources/RestServer.txt>
|
|
*
|
|
* @category Framework
|
|
* @package restler
|
|
* @author Jac Wright <jacwright@gmail.com>
|
|
* @author R.Arul Kumaran <arul@luracast.com>
|
|
* @copyright 2010 Luracast
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
|
* @link http://luracast.com/products/restler/
|
|
* @version 2.2.0
|
|
*/
|
|
class Restler
|
|
{
|
|
// ==================================================================
|
|
//
|
|
// Public variables
|
|
//
|
|
// ------------------------------------------------------------------
|
|
|
|
const VERSION = '2.2.0';
|
|
|
|
/**
|
|
* URL of the currently mapped service
|
|
* @var string
|
|
*/
|
|
public $url;
|
|
|
|
/**
|
|
* Http request method of the current request.
|
|
* Any value between [GET, PUT, POST, DELETE]
|
|
* @var string
|
|
*/
|
|
public $request_method;
|
|
|
|
/**
|
|
* Requested data format. Instance of the current format class
|
|
* which implements the iFormat interface
|
|
* @var iFormat
|
|
* @example jsonFormat, xmlFormat, yamlFormat etc
|
|
*/
|
|
public $request_format;
|
|
|
|
/**
|
|
* Data sent to the service
|
|
* @var array
|
|
*/
|
|
public $request_data = array();
|
|
|
|
/**
|
|
* Used in production mode to store the URL Map to disk
|
|
* @var string
|
|
*/
|
|
public $cache_dir;
|
|
|
|
/**
|
|
* base directory to locate format and auth files
|
|
* @var string
|
|
*/
|
|
public $base_dir;
|
|
|
|
/**
|
|
* Name of an iRespond implementation class
|
|
* @var string
|
|
*/
|
|
public $response = 'DefaultResponse';
|
|
|
|
/**
|
|
* Response data format. Instance of the current format class
|
|
* which implements the iFormat interface
|
|
* @var iFormat
|
|
* @example jsonFormat, xmlFormat, yamlFormat etc
|
|
*/
|
|
public $response_format;
|
|
|
|
// ==================================================================
|
|
//
|
|
// Private & Protected variables
|
|
//
|
|
// ------------------------------------------------------------------
|
|
|
|
/**
|
|
* When set to FALSE, it will run in debug mode and parse the
|
|
* class files every time to map it to the URL
|
|
* @var boolean
|
|
*/
|
|
protected $production_mode;
|
|
|
|
/**
|
|
* Associated array that maps urls to their respective class and method
|
|
* @var array
|
|
*/
|
|
protected $routes = array();
|
|
|
|
/**
|
|
* Associated array that maps formats to their respective format class name
|
|
* @var array
|
|
*/
|
|
protected $format_map = array();
|
|
|
|
/**
|
|
* Instance of the current api service class
|
|
* @var object
|
|
*/
|
|
protected $service_class_instance;
|
|
|
|
/**
|
|
* Name of the api method being called
|
|
* @var string
|
|
*/
|
|
protected $service_method;
|
|
|
|
/**
|
|
* list of authentication classes
|
|
* @var array
|
|
*/
|
|
protected $auth_classes = array();
|
|
|
|
/**
|
|
* list of error handling classes
|
|
* @var array
|
|
*/
|
|
protected $error_classes = array();
|
|
|
|
/**
|
|
* HTTP status codes
|
|
* @var array
|
|
*/
|
|
private $codes = array(
|
|
100 => 'Continue',
|
|
101 => 'Switching Protocols',
|
|
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',
|
|
306 => '(Unused)',
|
|
307 => 'Temporary Redirect',
|
|
400 => 'Bad Request',
|
|
401 => 'Unauthorized',
|
|
402 => 'Payment Required',
|
|
403 => 'Forbidden',
|
|
404 => 'Not Found',
|
|
405 => 'Method Not Allowed',
|
|
406 => 'Not Acceptable',
|
|
407 => 'Proxy Authentication Required',
|
|
408 => 'Request Timeout',
|
|
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',
|
|
417 => 'Record not found',
|
|
500 => 'Internal Server Error',
|
|
501 => 'Not Implemented',
|
|
502 => 'Bad Gateway',
|
|
503 => 'Service Unavailable',
|
|
504 => 'Gateway Timeout',
|
|
505 => 'HTTP Version Not Supported'
|
|
);
|
|
|
|
/**
|
|
* Caching of url map is enabled or not
|
|
* @var boolean
|
|
*/
|
|
protected $cached;
|
|
|
|
// ==================================================================
|
|
//
|
|
// Public functions
|
|
//
|
|
// ------------------------------------------------------------------
|
|
|
|
/**
|
|
* Constructor
|
|
* @param boolean $production_mode When set to FALSE, it will run in
|
|
* debug mode and parse the class files every time to map it to the URL
|
|
*/
|
|
public function __construct($production_mode = FALSE)
|
|
{
|
|
$this->production_mode = $production_mode;
|
|
$this->cache_dir = getcwd();
|
|
$this->base_dir = RESTLER_PATH;
|
|
}
|
|
|
|
/**
|
|
* Store the url map cache if needed
|
|
*/
|
|
public function __destruct()
|
|
{
|
|
if ($this->production_mode && !($this->cached)) {
|
|
$this->saveCache();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use it in production mode to refresh the url map cache
|
|
*/
|
|
public function refreshCache()
|
|
{
|
|
$this->routes = array();
|
|
$this->cached = FALSE;
|
|
}
|
|
|
|
/**
|
|
* Call this method and pass all the formats that should be
|
|
* supported by the API. Accepts multiple parameters
|
|
* @param string class name of the format class that implements iFormat
|
|
* @example $restler->setSupportedFormats('JsonFormat', 'XmlFormat'...);
|
|
*/
|
|
public function setSupportedFormats()
|
|
{
|
|
$args = func_get_args();
|
|
$extensions = array();
|
|
foreach ($args as $class_name) {
|
|
if (!is_string($class_name) || !class_exists($class_name)) {
|
|
throw new Exception("$class_name is not a vaild Format Class.");
|
|
}
|
|
$obj = new $class_name;
|
|
if (!($obj instanceof iFormat)) {
|
|
throw new Exception('Invalid format class; must implement '
|
|
. 'iFormat interface');
|
|
}
|
|
foreach ($obj->getMIMEMap() as $extension => $mime) {
|
|
if (!isset($this->format_map[$extension])) {
|
|
$this->format_map[$extension] = $class_name;
|
|
}
|
|
$mime = explode(',', $mime);
|
|
if (!is_array($mime)) {
|
|
$mime = array($mime);
|
|
}
|
|
foreach ($mime as $value) {
|
|
if (!isset($this->format_map[$value]))
|
|
$this->format_map[$value] = $class_name;
|
|
}
|
|
$extensions[".$extension"] = TRUE;
|
|
}
|
|
}
|
|
$this->format_map['default'] = $args[0];
|
|
$this->format_map['extensions'] = array_keys($extensions);
|
|
}
|
|
|
|
/**
|
|
* Add api classes throgh this method. All the public methods
|
|
* that do not start with _ (underscore) will be will be exposed
|
|
* as the public api by default.
|
|
*
|
|
* All the protected methods that do not start with _ (underscore)
|
|
* will exposed as protected api which will require authentication
|
|
* @param string $class name of the service class
|
|
* @param string $basePath optional url prefix for mapping, uses
|
|
* lowercase version of the class name when not specified
|
|
* @throws Exception when supplied with invalid class name
|
|
*/
|
|
public function addAPIClass($class_name, $base_path = NULL)
|
|
{
|
|
if (!class_exists($class_name)) {
|
|
throw new Exception("API class $class_name is missing.");
|
|
}
|
|
$this->loadCache();
|
|
if (!$this->cached) {
|
|
if (is_null($base_path)) {
|
|
$base_path = strtolower($class_name);
|
|
$index = strrpos($class_name, '\\');
|
|
if ($index !== FALSE) {
|
|
$base_path = substr($base_path, $index + 1);
|
|
}
|
|
} else {
|
|
$base_path = trim($base_path,'/');
|
|
}
|
|
if (strlen($base_path) > 0) {
|
|
$base_path .= '/';
|
|
}
|
|
$this->generateMap($class_name, $base_path);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* protected methods will need atleast one authentication class to be set
|
|
* in order to allow that method to be executed
|
|
* @param string $class_name of the authentication class
|
|
* @param string $base_path optional url prefix for mapping
|
|
*/
|
|
public function addAuthenticationClass($class_name, $base_path = NULL)
|
|
{
|
|
$this->auth_classes[] = $class_name;
|
|
$this->addAPIClass($class_name, $base_path);
|
|
}
|
|
|
|
/**
|
|
* Add class for custom error handling
|
|
* @param string $class_name of the error handling class
|
|
*/
|
|
public function addErrorClass($class_name)
|
|
{
|
|
$this->error_classes[] = $class_name;
|
|
}
|
|
|
|
/**
|
|
* Convenience method to respond with an error message
|
|
* @param int $statusCode http error code
|
|
* @param string $errorMessage optional custom error message
|
|
*/
|
|
public function handleError($status_code, $error_message = NULL)
|
|
{
|
|
$method = "handle$status_code";
|
|
$handled = FALSE;
|
|
foreach ($this->error_classes as $class_name) {
|
|
if (method_exists($class_name, $method)) {
|
|
$obj = new $class_name();
|
|
$obj->restler = $this;
|
|
$obj->$method();
|
|
$handled = TRUE;
|
|
}
|
|
}
|
|
if ($handled) {
|
|
return;
|
|
}
|
|
$message = $this->codes[$status_code]
|
|
. (!$error_message ? '' : ': ' . $error_message);
|
|
$this->setStatus($status_code);
|
|
$responder = new $this->response();
|
|
$responder->restler = $this;
|
|
$this->sendData($responder->__formatError($status_code, $message));
|
|
}
|
|
|
|
/**
|
|
* Main function for processing the api request
|
|
* and return the response
|
|
* @throws Exception when the api service class is missing
|
|
* @throws RestException to send error response
|
|
*/
|
|
public function handle()
|
|
{
|
|
if (empty($this->format_map)) {
|
|
$this->setSupportedFormats('JsonFormat');
|
|
}
|
|
$this->url = $this->getPath();
|
|
$this->request_method = $this->getRequestMethod();
|
|
$this->response_format = $this->getResponseFormat();
|
|
$this->request_format = $this->getRequestFormat();
|
|
if (is_null($this->request_format)) {
|
|
$this->request_format = $this->response_format;
|
|
}
|
|
if ($this->request_method == 'PUT' || $this->request_method == 'POST') {
|
|
$this->request_data = $this->getRequestData();
|
|
}
|
|
$o = $this->mapUrlToMethod();
|
|
if (!isset($o->class_name)) {
|
|
$this->handleError(404);
|
|
} else {
|
|
try {
|
|
if ($o->method_flag) {
|
|
$auth_method = '__isAuthenticated';
|
|
if (!count($this->auth_classes)) {
|
|
throw new RestException(401);
|
|
}
|
|
foreach ($this->auth_classes as $auth_class) {
|
|
$auth_obj = new $auth_class();
|
|
$auth_obj->restler = $this;
|
|
$this->applyClassMetadata($auth_class, $auth_obj, $o);
|
|
if (!method_exists($auth_obj, $auth_method)) {
|
|
throw new RestException(401, 'Authentication Class '
|
|
. 'should implement iAuthenticate');
|
|
} else if (!$auth_obj->$auth_method()) {
|
|
throw new RestException(401);
|
|
}
|
|
}
|
|
}
|
|
|
|
$this->applyClassMetadata(get_class($this->request_format),
|
|
$this->request_format, $o);
|
|
$pre_process = '_' . $this->request_format->getExtension() . '_'
|
|
. $o->method_name;
|
|
$this->service_method = $o->method_name;
|
|
if ($o->method_flag == 2) {
|
|
$o = unprotect($o);
|
|
}
|
|
$object = $this->service_class_instance = new $o->class_name();
|
|
$object->restler = $this;
|
|
if (method_exists($o->class_name, $pre_process)) {
|
|
call_user_func_array(array($object, $pre_process),
|
|
$o->arguments);
|
|
}
|
|
switch ($o->method_flag) {
|
|
case 3:
|
|
$reflection_method = new ReflectionMethod($object,
|
|
$o->method_name);
|
|
$reflection_method->setAccessible(TRUE);
|
|
$result = $reflection_method->invokeArgs($object,
|
|
$o->arguments);
|
|
break;
|
|
case 2:
|
|
case 1:
|
|
default:
|
|
$result = call_user_func_array(array(
|
|
$object,
|
|
$o->method_name),
|
|
$o->arguments
|
|
);
|
|
break;
|
|
}
|
|
} catch (RestException $e) {
|
|
$this->handleError($e->getCode(), $e->getMessage());
|
|
}
|
|
}
|
|
$responder = new $this->response();
|
|
$responder->restler = $this;
|
|
$this->applyClassMetadata($this->response, $responder, $o);
|
|
if (isset($result) && $result !== NULL) {
|
|
$result = $responder->__formatResponse($result);
|
|
$this->sendData($result);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Encodes the response in the prefered format
|
|
* and sends back
|
|
* @param $data array php data
|
|
*/
|
|
public function sendData($data)
|
|
{
|
|
$data = $this->response_format->encode($data, !($this->production_mode));
|
|
$post_process = '_' . $this->service_method . '_'
|
|
. $this->response_format->getExtension();
|
|
if (isset($this->service_class_instance)
|
|
&& method_exists($this->service_class_instance,$post_process)
|
|
) {
|
|
$data = call_user_func(array($this->service_class_instance,
|
|
$post_process), $data);
|
|
}
|
|
header("Cache-Control: no-cache, must-revalidate");
|
|
header("Expires: 0");
|
|
header('Content-Type: ' . $this->response_format->getMIME());
|
|
//header('Content-Type: ' . $this->response_format->getMIME().'; charset=utf-8');
|
|
header("X-Powered-By: Luracast Restler v" . Restler::VERSION);
|
|
die($data);
|
|
}
|
|
|
|
/**
|
|
* Sets the HTTP response status
|
|
* @param int $code response code
|
|
*/
|
|
public function setStatus($code)
|
|
{
|
|
header("{$_SERVER['SERVER_PROTOCOL']} $code " . $this->codes[strval($code)]);
|
|
}
|
|
|
|
/**
|
|
* Compare two strings and remove the common
|
|
* sub string from the first string and return it
|
|
* @param string $first
|
|
* @param string $second
|
|
* @param string $char optional, set it as
|
|
* blank string for char by char comparison
|
|
* @return string
|
|
*/
|
|
public function removeCommonPath($first, $second, $char = '/')
|
|
{
|
|
$first = explode($char, $first);
|
|
$second = explode($char, $second);
|
|
while (count($second)) {
|
|
if ($first[0] == $second[0]) {
|
|
array_shift($first);
|
|
} else {
|
|
break;
|
|
}
|
|
array_shift($second);
|
|
}
|
|
return implode($char, $first);
|
|
}
|
|
|
|
/**
|
|
* Save cache to file
|
|
*/
|
|
public function saveCache()
|
|
{
|
|
$file = $this->cache_dir . '/routes.php';
|
|
$s = '$o=array();' . PHP_EOL;
|
|
foreach ($this->routes as $key => $value) {
|
|
$s .= PHP_EOL . PHP_EOL . PHP_EOL . "############### $key ###############"
|
|
. PHP_EOL . PHP_EOL;
|
|
$s .= '$o[\'' . $key . '\']=array();';
|
|
foreach ($value as $ke => $va) {
|
|
$s .= PHP_EOL . PHP_EOL . "#==== $key $ke" . PHP_EOL . PHP_EOL;
|
|
$s .= '$o[\'' . $key . '\'][\'' . $ke . '\']=' . str_replace(
|
|
PHP_EOL,
|
|
PHP_EOL . "\t",
|
|
var_export($va, TRUE)
|
|
) . ';';
|
|
}
|
|
}
|
|
$s .= PHP_EOL . 'return $o;';
|
|
$r = @file_put_contents($file, "<?php $s");
|
|
@chmod($file, 0777);
|
|
if ($r === FALSE) {
|
|
throw new Exception(
|
|
"The cache directory located at '$this->cache_dir' needs to have "
|
|
. "the permissions set to read/write/execute for everyone in order "
|
|
. "to save cache and improve performance.");
|
|
}
|
|
}
|
|
|
|
// ==================================================================
|
|
//
|
|
// Protected functions
|
|
//
|
|
// ------------------------------------------------------------------
|
|
|
|
/**
|
|
* Parses the requst url and get the api path
|
|
* @return string api path
|
|
*/
|
|
protected function getPath()
|
|
{
|
|
$path = urldecode($this->removeCommonPath($_SERVER['REQUEST_URI'],
|
|
$_SERVER['SCRIPT_NAME']));
|
|
$path = preg_replace('/(\/*\?.*$)|(\/$)/', '', $path);
|
|
$path = str_replace($this->format_map['extensions'], '', $path);
|
|
return $path;
|
|
}
|
|
|
|
/**
|
|
* Parses the request to figure out the http request type
|
|
* @return string which will be one of the following
|
|
* [GET, POST, PUT, DELETE]
|
|
* @example GET
|
|
*/
|
|
protected function getRequestMethod()
|
|
{
|
|
$method = $_SERVER['REQUEST_METHOD'];
|
|
if (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
|
|
$method = $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'];
|
|
}
|
|
//support for HEAD request
|
|
if ($method == 'HEAD') {
|
|
$method = 'GET';
|
|
}
|
|
return $method;
|
|
}
|
|
|
|
/**
|
|
* Parses the request to figure out format of the request data
|
|
* @return iFormat any class that implements iFormat
|
|
* @example JsonFormat
|
|
*/
|
|
protected function getRequestFormat()
|
|
{
|
|
$format = NULL;
|
|
//check if client has sent any information on request format
|
|
if (isset($_SERVER['CONTENT_TYPE'])) {
|
|
$mime = explode(';', $_SERVER['CONTENT_TYPE']);
|
|
$mime = $mime[0];
|
|
if ($mime == UrlEncodedFormat::MIME) {
|
|
$format = new UrlEncodedFormat();
|
|
} else {
|
|
if (isset($this->format_map[$mime])) {
|
|
$format = $this->format_map[$mime];
|
|
$format = is_string($format) ? new $format : $format;
|
|
$format->setMIME($mime);
|
|
}
|
|
}
|
|
}
|
|
return $format;
|
|
}
|
|
|
|
/**
|
|
* Parses the request to figure out the best format for response
|
|
* @return iFormat any class that implements iFormat
|
|
* @example JsonFormat
|
|
*/
|
|
protected function getResponseFormat()
|
|
{
|
|
//check if client has specified an extension
|
|
/**
|
|
* @var iFormat
|
|
*/
|
|
$format = NULL;
|
|
$extensions = explode('.', parse_url($_SERVER['REQUEST_URI'],
|
|
PHP_URL_PATH));
|
|
while($extensions) {
|
|
$extension = array_pop($extensions);
|
|
$extension = explode('/', $extension);
|
|
$extension = array_shift($extension);
|
|
if ($extension && isset($this->format_map[$extension])) {
|
|
$format = $this->format_map[$extension];
|
|
$format = is_string($format) ? new $format : $format;
|
|
$format->setExtension($extension);
|
|
//echo "Extension $extension";
|
|
return $format;
|
|
}
|
|
}
|
|
//check if client has sent list of accepted data formats
|
|
if (isset($_SERVER['HTTP_ACCEPT'])) {
|
|
$acceptList = array();
|
|
$accepts = explode(',', strtolower($_SERVER['HTTP_ACCEPT']));
|
|
if (!is_array($accepts)) {
|
|
$accepts = array($accepts);
|
|
}
|
|
foreach ($accepts as $pos => $accept) {
|
|
$parts = explode(';q=', trim($accept));
|
|
$type = array_shift($parts);
|
|
$quality = count($parts)
|
|
? floatval(array_shift($parts))
|
|
: (1000 - $pos) / 1000;
|
|
$acceptList[$type] = $quality;
|
|
}
|
|
arsort($acceptList);
|
|
foreach ($acceptList as $accept => $quality) {
|
|
if (isset($this->format_map[$accept])) {
|
|
$format = $this->format_map[$accept];
|
|
$format = is_string($format) ? new $format : $format;
|
|
$format->setMIME($accept);
|
|
//echo "MIME $accept";
|
|
header("Vary: Accept"); // Tell cache content is based on Accept header
|
|
return $format;
|
|
}
|
|
}
|
|
} else {
|
|
// RFC 2616: If no Accept header field is
|
|
// present, then it is assumed that the
|
|
// client accepts all media types.
|
|
$_SERVER['HTTP_ACCEPT'] = '*/*';
|
|
}
|
|
if (strpos($_SERVER['HTTP_ACCEPT'], '*') !== FALSE) {
|
|
if (strpos($_SERVER['HTTP_ACCEPT'], 'application/*') !== FALSE) {
|
|
$format = new JsonFormat;
|
|
}
|
|
else if (strpos($_SERVER['HTTP_ACCEPT'], 'text/*') !== FALSE) {
|
|
$format = new XmlFormat;
|
|
}
|
|
else if (strpos($_SERVER['HTTP_ACCEPT'], '*/*') !== FALSE) {
|
|
$format = new $this->format_map['default'];
|
|
}
|
|
}
|
|
if (empty($format)) {
|
|
// RFC 2616: If an Accept header field is present, and if the server
|
|
// cannot send a response which is acceptable according to the combined
|
|
// Accept field value, then the server SHOULD send a 406 (not acceptable)
|
|
// response.
|
|
header('HTTP/1.1 406 Not Acceptable');
|
|
die('406 Not Acceptable: The server was unable to negotiate content for this request.');
|
|
} else {
|
|
header("Vary: Accept"); // Tell cache content is based ot Accept header
|
|
return $format;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses the request data and returns it
|
|
* @return array php data
|
|
*/
|
|
protected function getRequestData()
|
|
{
|
|
try{
|
|
$r = file_get_contents('php://input');
|
|
if (is_null($r)) {
|
|
return $_GET;
|
|
}
|
|
$r = $this->request_format->decode($r);
|
|
return is_null($r) ? array() : $r;
|
|
} catch (RestException $e) {
|
|
$this->handleError($e->getCode(), $e->getMessage());
|
|
}
|
|
}
|
|
|
|
protected function mapUrlToMethod()
|
|
{
|
|
if (!isset($this->routes[$this->request_method])) {
|
|
return array();
|
|
}
|
|
$urls = $this->routes[$this->request_method];
|
|
if (!$urls) {
|
|
return array();
|
|
}
|
|
|
|
$found = FALSE;
|
|
$this->request_data += $_GET;
|
|
$params = array('request_data' => $this->request_data);
|
|
$params += $this->request_data;
|
|
$lc = strtolower($this->url);
|
|
foreach ($urls as $url => $call) {
|
|
//echo PHP_EOL.$url.' = '.$this->url.PHP_EOL;
|
|
$call = (object) $call;
|
|
if (strstr($url, ':')) {
|
|
$regex = preg_replace('/\\\:([^\/]*)/', '(?P<$1>[^/]*)',
|
|
preg_quote($url));
|
|
if (preg_match(":^$regex$:i", $this->url, $matches)) {
|
|
foreach ($matches as $arg => $match) {
|
|
if (isset($call->arguments[$arg])) {
|
|
//flog("$arg => $match $args[$arg]");
|
|
$params[$arg] = $match;
|
|
}
|
|
}
|
|
$found = TRUE;
|
|
break;
|
|
}
|
|
} else if ($url == $lc) {
|
|
$found = TRUE;
|
|
break;
|
|
}
|
|
}
|
|
if ($found) {
|
|
$p = $call->defaults;
|
|
foreach ($call->arguments as $key => $value) {
|
|
//echo "$key => $value \n";
|
|
if (isset($params[$key])) {
|
|
$p[$value] = $params[$key];
|
|
}
|
|
}
|
|
$call->arguments = $p;
|
|
return $call;
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Apply static and non-static properties defined in
|
|
* the method information anotation
|
|
* @param String $class_name
|
|
* @param Object $instance instance of that class
|
|
* @param Object $method_info method information and metadata
|
|
*/
|
|
protected function applyClassMetadata($class_name, $instance, $method_info)
|
|
{
|
|
if (isset($method_info->metadata[$class_name])
|
|
&& is_array($method_info->metadata[$class_name])
|
|
) {
|
|
foreach ($method_info->metadata[$class_name] as $property => $value ) {
|
|
if (property_exists($class_name, $property)) {
|
|
$reflection_property = new ReflectionProperty($class_name, $property);
|
|
$reflection_property->setValue($instance, $value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
protected function loadCache()
|
|
{
|
|
if ($this->cached !== NULL) {
|
|
return;
|
|
}
|
|
$file = $this->cache_dir . '/routes.php';
|
|
$this->cached = FALSE;
|
|
|
|
if ($this->production_mode) {
|
|
if (file_exists($file)) {
|
|
$routes = include($file);
|
|
}
|
|
if (isset($routes) && is_array($routes)) {
|
|
$this->routes = $routes;
|
|
$this->cached = TRUE;
|
|
}
|
|
} else {
|
|
//@unlink($this->cache_dir . "/$name.php");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates cachable url to method mapping
|
|
* @param string $class_name
|
|
* @param string $base_path
|
|
*/
|
|
protected function generateMap ($class_name, $base_path = '')
|
|
{
|
|
$reflection = new ReflectionClass($class_name);
|
|
$class_metadata = parse_doc($reflection->getDocComment());
|
|
$methods = $reflection->getMethods(
|
|
ReflectionMethod::IS_PUBLIC + ReflectionMethod::IS_PROTECTED
|
|
);
|
|
foreach ($methods as $method) {
|
|
$doc = $method->getDocComment();
|
|
$arguments = array();
|
|
$defaults = array();
|
|
$metadata = $class_metadata + parse_doc($doc);
|
|
$params = $method->getParameters();
|
|
$position = 0;
|
|
foreach ($params as $param) {
|
|
$arguments[$param->getName()] = $position;
|
|
$defaults[$position] = $param->isDefaultValueAvailable()
|
|
? $param->getDefaultValue() : NULL;
|
|
$position++;
|
|
}
|
|
$method_flag = $method->isProtected()
|
|
? (isRestlerCompatibilityModeEnabled() ? 2 : 3)
|
|
: (isset($metadata['protected']) ? 1 : 0);
|
|
|
|
//take note of the order
|
|
$call = array(
|
|
'class_name' => $class_name,
|
|
'method_name' => $method->getName(),
|
|
'arguments' => $arguments,
|
|
'defaults' => $defaults,
|
|
'metadata' => $metadata,
|
|
'method_flag' => $method_flag
|
|
);
|
|
$method_url = strtolower($method->getName());
|
|
if (preg_match_all(
|
|
'/@url\s+(GET|POST|PUT|DELETE|HEAD|OPTIONS)[ \t]*\/?(\S*)/s',
|
|
$doc,
|
|
$matches,
|
|
PREG_SET_ORDER)
|
|
) {
|
|
foreach ($matches as $match) {
|
|
$http_method = $match[1];
|
|
$url = rtrim($base_path . $match[2],'/');
|
|
$this->routes[$http_method][$url] = $call;
|
|
}
|
|
} else if ($method_url[0] != '_') { //not prefixed with underscore
|
|
// no configuration found so use convention
|
|
if (preg_match_all('/^(GET|POST|PUT|DELETE|HEAD|OPTIONS)/i',
|
|
$method_url,
|
|
$matches)
|
|
) {
|
|
$http_method = strtoupper($matches[0][0]);
|
|
$method_url = substr($method_url, strlen($http_method));
|
|
} else {
|
|
$http_method = 'GET';
|
|
}
|
|
$url = $base_path
|
|
. ($method_url == 'index' || $method_url == 'default' ? '' : $method_url);
|
|
$url = rtrim($url,'/');
|
|
$this->routes[$http_method][$url] = $call;
|
|
foreach ($params as $param) {
|
|
if ($param->getName() == 'request_data') {
|
|
break;
|
|
}
|
|
$url .= $url == '' ? ':' : '/:';
|
|
$url .= $param->getName();
|
|
$this->routes[$http_method][$url] = $call;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (version_compare(PHP_VERSION, '5.3.0') < 0) {
|
|
require_once 'compat.php';
|
|
}
|
|
|
|
// ==================================================================
|
|
//
|
|
// Secondary classes
|
|
//
|
|
// ------------------------------------------------------------------
|
|
|
|
/**
|
|
* Special Exception for raising API errors
|
|
* that can be used in API methods
|
|
* @category Framework
|
|
* @package restler
|
|
* @subpackage exception
|
|
* @author R.Arul Kumaran <arul@luracast.com>
|
|
* @copyright 2010 Luracast
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
|
* @link http://luracast.com/products/restler/
|
|
*/
|
|
class RestException extends Exception
|
|
{
|
|
|
|
public function __construct($http_status_code, $error_message = NULL)
|
|
{
|
|
parent::__construct($error_message, $http_status_code);
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Interface for creating response classes
|
|
* @category Framework
|
|
* @package restler
|
|
* @subpackage result
|
|
* @author R.Arul Kumaran <arul@luracast.com>
|
|
* @copyright 2010 Luracast
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
|
* @link http://luracast.com/products/restler/
|
|
*/
|
|
interface iRespond
|
|
{
|
|
/**
|
|
* Result of an api call is passed to this method
|
|
* to create a standard structure for the data
|
|
* @param unknown_type $result can be a primitive or array or object
|
|
*/
|
|
public function __formatResponse($result);
|
|
|
|
/**
|
|
* When the api call results in RestException this method
|
|
* will be called to return the error message
|
|
* @param int $status_code
|
|
* @param String $message
|
|
*/
|
|
public function __formatError($status_code, $message);
|
|
}
|
|
|
|
/**
|
|
* Default response formating class
|
|
* @category Framework
|
|
* @package restler
|
|
* @subpackage result
|
|
* @author R.Arul Kumaran <arul@luracast.com>
|
|
* @copyright 2010 Luracast
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
|
* @link http://luracast.com/products/restler/
|
|
*/
|
|
class DefaultResponse implements iRespond
|
|
{
|
|
function __formatResponse($result)
|
|
{
|
|
return $result;
|
|
}
|
|
function __formatError($statusCode, $message)
|
|
{
|
|
return array(
|
|
'error' => array(
|
|
'code' => $statusCode,
|
|
'message' => $message
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Interface for creating authentication classes
|
|
* @category Framework
|
|
* @package restler
|
|
* @subpackage auth
|
|
* @author R.Arul Kumaran <arul@luracast.com>
|
|
* @copyright 2010 Luracast
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
|
* @link http://luracast.com/products/restler/
|
|
*/
|
|
interface iAuthenticate
|
|
{
|
|
/**
|
|
* Auth function that is called when a protected method is requested
|
|
* @return boolean TRUE or FALSE
|
|
*/
|
|
public function __isAuthenticated();
|
|
}
|
|
|
|
/**
|
|
* Interface for creating custom data formats
|
|
* like xml, json, yaml, amf etc
|
|
* @category Framework
|
|
* @package restler
|
|
* @subpackage format
|
|
* @author R.Arul Kumaran <arul@luracast.com>
|
|
* @copyright 2010 Luracast
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
|
* @link http://luracast.com/products/restler/
|
|
*/
|
|
interface iFormat
|
|
{
|
|
/**
|
|
* Get Extension => MIME type mappings as an associative array
|
|
* @return array list of mime strings for the format
|
|
* @example array('json'=>'application/json');
|
|
*/
|
|
public function getMIMEMap();
|
|
|
|
/**
|
|
* Set the selected MIME type
|
|
* @param string $mime MIME type
|
|
*/
|
|
public function setMIME($mime);
|
|
/**
|
|
* Get selected MIME type
|
|
*/
|
|
public function getMIME();
|
|
|
|
/**
|
|
* Set the selected file extension
|
|
* @param string $extension file extension
|
|
*/
|
|
public function setExtension($extension);
|
|
|
|
/**
|
|
* Get the selected file extension
|
|
* @return string file extension
|
|
*/
|
|
public function getExtension();
|
|
|
|
/**
|
|
* Encode the given data in the format
|
|
* @param array $data resulting data that needs to
|
|
* be encoded in the given format
|
|
* @param boolean $human_readable set to TRUE when restler
|
|
* is not running in production mode. Formatter has to
|
|
* make the encoded output more human readable
|
|
* @return string encoded string
|
|
*/
|
|
public function encode($data, $human_readable = FALSE);
|
|
|
|
/**
|
|
* Decode the given data from the format
|
|
* @param string $data data sent from client to
|
|
* the api in the given format.
|
|
* @return array associative array of the parsed data
|
|
*/
|
|
public function decode($data);
|
|
}
|
|
|
|
/**
|
|
* URL Encoded String Format
|
|
* @category Framework
|
|
* @package restler
|
|
* @subpackage format
|
|
* @author R.Arul Kumaran <arul@luracast.com>
|
|
* @copyright 2010 Luracast
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
|
* @link http://luracast.com/products/restler/
|
|
*/
|
|
class UrlEncodedFormat implements iFormat
|
|
{
|
|
const MIME = 'application/x-www-form-urlencoded';
|
|
const EXTENSION = 'post';
|
|
|
|
public function getMIMEMap()
|
|
{
|
|
return array(self::EXTENSION => self::MIME);
|
|
}
|
|
|
|
public function getMIME()
|
|
{
|
|
return self::MIME;
|
|
}
|
|
|
|
public function getExtension()
|
|
{
|
|
return self::EXTENSION;
|
|
}
|
|
|
|
public function setMIME($mime)
|
|
{
|
|
//do nothing
|
|
}
|
|
|
|
public function setExtension($extension)
|
|
{
|
|
//do nothing
|
|
}
|
|
|
|
public function encode($data, $human_readable = FALSE)
|
|
{
|
|
return http_build_query($data);
|
|
}
|
|
|
|
public function decode($data)
|
|
{
|
|
parse_str($data,$r);
|
|
return $r;
|
|
}
|
|
|
|
public function __toString()
|
|
{
|
|
return $this->getExtension();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Javascript Object Notation Format
|
|
* @category Framework
|
|
* @package restler
|
|
* @subpackage format
|
|
* @author R.Arul Kumaran <arul@luracast.com>
|
|
* @copyright 2010 Luracast
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
|
* @link http://luracast.com/products/restler/
|
|
*/
|
|
class JsonFormat implements iFormat
|
|
{
|
|
const MIME = 'application/json,application/javascript';
|
|
static $mime = 'application/json';
|
|
const EXTENSION = 'json';
|
|
|
|
public function getMIMEMap()
|
|
{
|
|
return array(self::EXTENSION => self::MIME);
|
|
}
|
|
|
|
public function getMIME()
|
|
{
|
|
return self::$mime;
|
|
}
|
|
|
|
public function getExtension()
|
|
{
|
|
return self::EXTENSION;
|
|
}
|
|
|
|
public function setMIME($mime)
|
|
{
|
|
self::$mime = $mime;
|
|
}
|
|
|
|
public function setExtension($extension)
|
|
{
|
|
//do nothing
|
|
}
|
|
|
|
public function encode($data, $human_readable = FALSE)
|
|
{
|
|
return $human_readable
|
|
? $this->json_format(json_encode(object_to_array($data)))
|
|
: json_encode(object_to_array($data));
|
|
}
|
|
|
|
public function decode($data)
|
|
{
|
|
$decoded = json_decode ($data);
|
|
if (function_exists ('json_last_error')) {
|
|
$message = '';
|
|
switch (json_last_error ()) {
|
|
case JSON_ERROR_NONE:
|
|
return object_to_array ($decoded);
|
|
break;
|
|
case JSON_ERROR_DEPTH:
|
|
$message = 'maximum stack depth exceeded';
|
|
break;
|
|
case JSON_ERROR_STATE_MISMATCH:
|
|
$message = 'underflow or the modes mismatch';
|
|
break;
|
|
case JSON_ERROR_CTRL_CHAR:
|
|
$message = 'unexpected control character found';
|
|
break;
|
|
case JSON_ERROR_SYNTAX:
|
|
$message = 'malformed JSON';
|
|
break;
|
|
case JSON_ERROR_UTF8:
|
|
$message = 'malformed UTF-8 characters, possibly incorrectly encoded';
|
|
break;
|
|
default:
|
|
$message = 'unknown error';
|
|
break;
|
|
}
|
|
throw new RestException (400, 'Error parsing JSON, ' . $message);
|
|
} else if (strlen ($data) && $decoded === NULL || $decoded === $data) {
|
|
throw new RestException (400, 'Error parsing JSON');
|
|
}
|
|
return object_to_array ($decoded);
|
|
}
|
|
|
|
/**
|
|
* Pretty print JSON string
|
|
* @param string $json
|
|
* @return string formated json
|
|
*/
|
|
private function json_format($json)
|
|
{
|
|
$tab = " ";
|
|
$new_json = "";
|
|
$indent_level = 0;
|
|
$in_string = FALSE;
|
|
$len = strlen($json);
|
|
|
|
for($c = 0; $c < $len; $c++) {
|
|
$char = $json[$c];
|
|
switch($char) {
|
|
case '{':
|
|
case '[':
|
|
if (!$in_string) {
|
|
$new_json .= $char . "\n" .
|
|
str_repeat($tab, $indent_level + 1);
|
|
$indent_level++;
|
|
} else {
|
|
$new_json .= $char;
|
|
}
|
|
break;
|
|
case '}':
|
|
case ']':
|
|
if (!$in_string) {
|
|
$indent_level--;
|
|
$new_json .= "\n" . str_repeat($tab, $indent_level) . $char;
|
|
} else {
|
|
$new_json .= $char;
|
|
}
|
|
break;
|
|
case ',':
|
|
if (!$in_string) {
|
|
$new_json .= ",\n" . str_repeat($tab, $indent_level);
|
|
} else {
|
|
$new_json .= $char;
|
|
}
|
|
break;
|
|
case ':':
|
|
if (!$in_string) {
|
|
$new_json .= ": ";
|
|
} else {
|
|
$new_json .= $char;
|
|
}
|
|
break;
|
|
case '"':
|
|
if ($c==0) {
|
|
$in_string = TRUE;
|
|
} else if ($c > 0 && $json[$c-1] != '\\') {
|
|
$in_string = !$in_string;
|
|
}
|
|
default:
|
|
$new_json .= $char;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $new_json;
|
|
}
|
|
|
|
public function __toString()
|
|
{
|
|
return $this->getExtension();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses the PHPDoc comments for metadata. Inspired by Documentor code base
|
|
* @category Framework
|
|
* @package restler
|
|
* @subpackage helper
|
|
* @author Murray Picton <info@murraypicton.com>
|
|
* @author R.Arul Kumaran <arul@luracast.com>
|
|
* @copyright 2010 Luracast
|
|
* @license http://www.gnu.org/licenses/ GNU General Public License
|
|
* @link https://github.com/murraypicton/Doqumentor
|
|
*/
|
|
class DocParser
|
|
{
|
|
private $params = array();
|
|
|
|
function parse($doc = '') {
|
|
if ($doc == '') {
|
|
return $this->params;
|
|
}
|
|
//Get the comment
|
|
if (preg_match('#^/\*\*(.*)\*/#s', $doc, $comment) === false) {
|
|
return $this->params;
|
|
}
|
|
$comment = trim($comment[1]);
|
|
//Get all the lines and strip the * from the first character
|
|
if (preg_match_all('#^\s*\*(.*)#m', $comment, $lines) === false) {
|
|
return $this->params;
|
|
}
|
|
$this->parseLines($lines[1]);
|
|
return $this->params;
|
|
}
|
|
|
|
private function parseLines($lines)
|
|
{
|
|
foreach($lines as $line) {
|
|
$parsedLine = $this->parseLine($line); //Parse the line
|
|
|
|
if ($parsedLine === false && !isset($this->params['description'])) {
|
|
if (isset($desc)) {
|
|
//Store the first line in the short description
|
|
$this->params['description'] = implode(PHP_EOL, $desc);
|
|
}
|
|
$desc = array();
|
|
} else if ($parsedLine !== false) {
|
|
$desc[] = $parsedLine; //Store the line in the long description
|
|
}
|
|
}
|
|
$desc = implode(' ', $desc);
|
|
if (!empty($desc)) {
|
|
$this->params['long_description'] = $desc;
|
|
}
|
|
}
|
|
|
|
private function parseLine($line)
|
|
{
|
|
//trim the whitespace from the line
|
|
$line = trim($line);
|
|
|
|
if (empty($line)) {
|
|
return false; //Empty line
|
|
}
|
|
|
|
if (strpos($line, '@') === 0) {
|
|
if (strpos($line, ' ') > 0) {
|
|
//Get the parameter name
|
|
$param = substr($line, 1, strpos($line, ' ') - 1);
|
|
$value = substr($line, strlen($param) + 2); //Get the value
|
|
} else {
|
|
$param = substr($line, 1);
|
|
$value = '';
|
|
}
|
|
//Parse the line and return false if the parameter is valid
|
|
if ($this->setParam($param, $value)) return false;
|
|
}
|
|
return $line;
|
|
}
|
|
|
|
private function setParam($param, $value)
|
|
{
|
|
if ($param == 'param' || $param == 'return') {
|
|
$value = $this->formatParamOrReturn($value);
|
|
}
|
|
if ($param == 'class') {
|
|
list($param, $value) = $this->formatClass($value);
|
|
}
|
|
|
|
if (empty($this->params[$param])) {
|
|
$this->params[$param] = $value;
|
|
} else if ($param == 'param') {
|
|
$arr = array($this->params[$param], $value);
|
|
$this->params[$param] = $arr;
|
|
} else {
|
|
$this->params[$param] = $value + $this->params[$param];
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private function formatClass($value)
|
|
{
|
|
$r = preg_split("[\(|\)]", $value);
|
|
if (count($r) > 1) {
|
|
$param = $r[0];
|
|
parse_str($r[1],$value);
|
|
foreach ($value as $key => $val) {
|
|
$val = explode(',', $val);
|
|
if (count($val) > 1) {
|
|
$value[$key] = $val;
|
|
}
|
|
}
|
|
} else {
|
|
$param = 'Unknown';
|
|
}
|
|
return array($param, $value);
|
|
}
|
|
|
|
private function formatParamOrReturn($string)
|
|
{
|
|
$pos = strpos($string, ' ');
|
|
$type = substr($string, 0, $pos);
|
|
return '(' . $type . ')' . substr($string, $pos + 1);
|
|
}
|
|
}
|
|
|
|
|
|
// ==================================================================
|
|
//
|
|
// Individual functions
|
|
//
|
|
// ------------------------------------------------------------------
|
|
|
|
function parse_doc($php_doc_comment)
|
|
{
|
|
$p = new DocParser();
|
|
return $p->parse($php_doc_comment);
|
|
|
|
$p = new Parser($php_doc_comment);
|
|
return $p;
|
|
|
|
$php_doc_comment = preg_replace(
|
|
"/(^[\\s]*\\/\\*\\*)
|
|
|(^[\\s]\\*\\/)
|
|
|(^[\\s]*\\*?\\s)
|
|
|(^[\\s]*)
|
|
|(^[\\t]*)/ixm", "", $php_doc_comment);
|
|
$php_doc_comment = str_replace("\r", "", $php_doc_comment);
|
|
$php_doc_comment = preg_replace("/([\\t])+/", "\t", $php_doc_comment);
|
|
return explode("\n", $php_doc_comment);
|
|
|
|
$php_doc_comment = trim(preg_replace('/\r?\n *\* */', ' ',
|
|
$php_doc_comment));
|
|
return $php_doc_comment;
|
|
|
|
preg_match_all('/@([a-z]+)\s+(.*?)\s*(?=$|@[a-z]+\s)/s',
|
|
$php_doc_comment, $matches);
|
|
return array_combine($matches[1], $matches[2]);
|
|
}
|
|
|
|
/**
|
|
* Conveniance function that converts the given object
|
|
* in to associative array, leaves object alone if
|
|
* JsonSerializable interface is detected
|
|
* @param object $object that needs to be converted
|
|
* @category Framework
|
|
* @package restler
|
|
* @subpackage format
|
|
* @author R.Arul Kumaran <arul@luracast.com>
|
|
* @copyright 2010 Luracast
|
|
* @license http://www.opensource.org/licenses/lgpl-license.php LGPL
|
|
* @link http://luracast.com/products/restler/
|
|
*/
|
|
function object_to_array($object, $utf_encode = FALSE)
|
|
{
|
|
if (is_array($object)
|
|
|| (is_object($object)
|
|
&& !($object instanceof JsonSerializable))
|
|
) {
|
|
$array = array();
|
|
foreach($object as $key => $value) {
|
|
$value = object_to_array($value, $utf_encode);
|
|
if ($utf_encode && is_string($value)) {
|
|
$value = utf8_encode($value);
|
|
}
|
|
$array[$key] = $value;
|
|
}
|
|
return $array;
|
|
}
|
|
return $object;
|
|
}
|
|
|
|
/**
|
|
* an autoloader function for loading format classes
|
|
* @param String $class_name class name of a class that implements iFormat
|
|
*/
|
|
function autoload_formats($class_name)
|
|
{
|
|
$class_name = strtolower($class_name);
|
|
$file = RESTLER_PATH . "/$class_name/$class_name.php";
|
|
if (file_exists ($file)) {
|
|
require_once ($file);
|
|
} else {
|
|
$file = RESTLER_PATH . "/$class_name.php";
|
|
if (file_exists ($file)) {
|
|
require_once ($file);
|
|
} elseif (file_exists ("$class_name.php")) {
|
|
require_once ("$class_name.php");
|
|
}
|
|
}
|
|
}
|
|
|
|
// ==================================================================
|
|
//
|
|
// Autoload
|
|
//
|
|
// ------------------------------------------------------------------
|
|
|
|
spl_autoload_register('autoload_formats');
|
|
|
|
/**
|
|
* Manage compatibility with PHP 5 < PHP 5.3
|
|
*/
|
|
if (!function_exists('isRestlerCompatibilityModeEnabled')) {
|
|
function isRestlerCompatibilityModeEnabled()
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
define('RESTLER_PATH', dirname(__FILE__));
|