mirror of
https://github.com/YunoHost-Apps/dolibarr_ynh.git
synced 2024-09-03 18:35:53 +02:00
1006 lines
35 KiB
PHP
1006 lines
35 KiB
PHP
<?php
|
|
namespace Luracast\Restler;
|
|
|
|
use Luracast\Restler\Data\String;
|
|
use Luracast\Restler\Scope;
|
|
use stdClass;
|
|
|
|
/**
|
|
* API Class to create Swagger Spec 1.1 compatible id and operation
|
|
* listing
|
|
*
|
|
* @category Framework
|
|
* @package Restler
|
|
* @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 3.0.0rc5
|
|
*/
|
|
class Resources implements iUseAuthentication, iProvideMultiVersionApi
|
|
{
|
|
/**
|
|
* @var bool should protected resources be shown to unauthenticated users?
|
|
*/
|
|
public static $hideProtected = true;
|
|
/**
|
|
* @var bool should we use format as extension?
|
|
*/
|
|
public static $useFormatAsExtension = true;
|
|
/**
|
|
* @var bool should we include newer apis in the list? works only when
|
|
* Defaults::$useUrlBasedVersioning is set to true;
|
|
*/
|
|
public static $listHigherVersions = true;
|
|
/**
|
|
* @var array all http methods specified here will be excluded from
|
|
* documentation
|
|
*/
|
|
public static $excludedHttpMethods = array('OPTIONS');
|
|
/**
|
|
* @var array all paths beginning with any of the following will be excluded
|
|
* from documentation
|
|
*/
|
|
public static $excludedPaths = array();
|
|
/**
|
|
* @var bool
|
|
*/
|
|
public static $placeFormatExtensionBeforeDynamicParts = true;
|
|
/**
|
|
* @var bool should we group all the operations with the same url or not
|
|
*/
|
|
public static $groupOperations = false;
|
|
/**
|
|
* @var null|callable if the api methods are under access control mechanism
|
|
* you can attach a function here that returns true or false to determine
|
|
* visibility of a protected api method. this function will receive method
|
|
* info as the only parameter.
|
|
*/
|
|
public static $accessControlFunction = null;
|
|
/**
|
|
* @var array type mapping for converting data types to javascript / swagger
|
|
*/
|
|
public static $dataTypeAlias = array(
|
|
'string' => 'string',
|
|
'int' => 'int',
|
|
'number' => 'float',
|
|
'float' => 'float',
|
|
'bool' => 'boolean',
|
|
'boolean' => 'boolean',
|
|
'NULL' => 'null',
|
|
'array' => 'Array',
|
|
'object' => 'Object',
|
|
'stdClass' => 'Object',
|
|
'mixed' => 'string',
|
|
'DateTime' => 'Date'
|
|
);
|
|
/**
|
|
* @var array configurable symbols to differentiate public, hybrid and
|
|
* protected api
|
|
*/
|
|
public static $apiDescriptionSuffixSymbols = array(
|
|
0 => ' <i class="icon-unlock-alt icon-large"></i>', //public api
|
|
1 => ' <i class="icon-adjust icon-large"></i>', //hybrid api
|
|
2 => ' <i class="icon-lock icon-large"></i>', //protected api
|
|
);
|
|
|
|
/**
|
|
* Injected at runtime
|
|
*
|
|
* @var Restler instance of restler
|
|
*/
|
|
public $restler;
|
|
/**
|
|
* @var string when format is not used as the extension this property is
|
|
* used to set the extension manually
|
|
*/
|
|
public $formatString = '';
|
|
protected $_models;
|
|
protected $_bodyParam;
|
|
/**
|
|
* @var bool|stdClass
|
|
*/
|
|
protected $_fullDataRequested = false;
|
|
protected $crud = array(
|
|
'POST' => 'create',
|
|
'GET' => 'retrieve',
|
|
'PUT' => 'update',
|
|
'DELETE' => 'delete',
|
|
'PATCH' => 'partial update'
|
|
);
|
|
protected static $prefixes = array(
|
|
'get' => 'retrieve',
|
|
'index' => 'list',
|
|
'post' => 'create',
|
|
'put' => 'update',
|
|
'patch' => 'modify',
|
|
'delete' => 'remove',
|
|
);
|
|
protected $_authenticated = false;
|
|
protected $cacheName = '';
|
|
|
|
public function __construct()
|
|
{
|
|
if (static::$useFormatAsExtension) {
|
|
$this->formatString = '.{format}';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This method will be called first for filter classes and api classes so
|
|
* that they can respond accordingly for filer method call and api method
|
|
* calls
|
|
*
|
|
*
|
|
* @param bool $isAuthenticated passes true when the authentication is
|
|
* done, false otherwise
|
|
*
|
|
* @return mixed
|
|
*/
|
|
public function __setAuthenticationStatus($isAuthenticated = false)
|
|
{
|
|
$this->_authenticated = $isAuthenticated;
|
|
}
|
|
|
|
/**
|
|
* pre call for get($id)
|
|
*
|
|
* if cache is present, use cache
|
|
*/
|
|
public function _pre_get_json($id)
|
|
{
|
|
$userClass = Defaults::$userIdentifierClass;
|
|
$this->cacheName = $userClass::getCacheIdentifier() . '_resources_' . $id;
|
|
if ($this->restler->getProductionMode()
|
|
&& !$this->restler->refreshCache
|
|
&& $this->restler->cache->isCached($this->cacheName)
|
|
) {
|
|
//by pass call, compose, postCall stages and directly send response
|
|
$this->restler->composeHeaders();
|
|
die($this->restler->cache->get($this->cacheName));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* post call for get($id)
|
|
*
|
|
* create cache if in production mode
|
|
*
|
|
* @param $responseData
|
|
*
|
|
* @internal param string $data composed json output
|
|
*
|
|
* @return string
|
|
*/
|
|
public function _post_get_json($responseData)
|
|
{
|
|
if ($this->restler->getProductionMode()) {
|
|
$this->restler->cache->set($this->cacheName, $responseData);
|
|
}
|
|
return $responseData;
|
|
}
|
|
|
|
/**
|
|
* @access hybrid
|
|
*
|
|
* @param string $id
|
|
*
|
|
* @throws RestException
|
|
* @return null|stdClass
|
|
*
|
|
* @url GET {id}
|
|
*/
|
|
public function get($id = '')
|
|
{
|
|
$version = $this->restler->getRequestedApiVersion();
|
|
if (empty($id)) {
|
|
//do nothing
|
|
} elseif (false !== ($pos = strpos($id, '-v'))) {
|
|
//$version = intval(substr($id, $pos + 2));
|
|
$id = substr($id, 0, $pos);
|
|
} elseif ($id{0} == 'v' && is_numeric($v = substr($id, 1))) {
|
|
$id = '';
|
|
//$version = $v;
|
|
} elseif ($id == 'root' || $id == 'index') {
|
|
$id = '';
|
|
}
|
|
$this->_models = new stdClass();
|
|
$r = null;
|
|
$count = 0;
|
|
|
|
$tSlash = !empty($id);
|
|
$target = empty($id) ? '' : $id;
|
|
$tLen = strlen($target);
|
|
|
|
$filter = array();
|
|
|
|
$routes
|
|
= Util::nestedValue(Routes::toArray(), "v$version")
|
|
? : array();
|
|
|
|
$prefix = Defaults::$useUrlBasedVersioning ? "/v$version" : '';
|
|
|
|
foreach ($routes as $value) {
|
|
foreach ($value as $httpMethod => $route) {
|
|
if (in_array($httpMethod, static::$excludedHttpMethods)) {
|
|
continue;
|
|
}
|
|
$fullPath = $route['url'];
|
|
if ($fullPath !== $target && !String::beginsWith($fullPath, $target)) {
|
|
continue;
|
|
}
|
|
$fLen = strlen($fullPath);
|
|
if ($tSlash) {
|
|
if ($fLen != $tLen && !String::beginsWith($fullPath, $target . '/'))
|
|
continue;
|
|
} elseif ($fLen > $tLen + 1 && $fullPath{$tLen + 1} != '{' && !String::beginsWith($fullPath, '{')) {
|
|
//when mapped to root exclude paths that have static parts
|
|
//they are listed else where under that static part name
|
|
continue;
|
|
}
|
|
|
|
if (!static::verifyAccess($route)) {
|
|
continue;
|
|
}
|
|
foreach (static::$excludedPaths as $exclude) {
|
|
if (empty($exclude)) {
|
|
if ($fullPath == $exclude)
|
|
continue 2;
|
|
} elseif (String::beginsWith($fullPath, $exclude)) {
|
|
continue 2;
|
|
}
|
|
}
|
|
$m = $route['metadata'];
|
|
if ($id == '' && $m['resourcePath'] != '') {
|
|
continue;
|
|
}
|
|
if (isset($filter[$httpMethod][$fullPath])) {
|
|
continue;
|
|
}
|
|
$filter[$httpMethod][$fullPath] = true;
|
|
// reset body params
|
|
$this->_bodyParam = array(
|
|
'required' => false,
|
|
'description' => array()
|
|
);
|
|
$count++;
|
|
$className = $this->_noNamespace($route['className']);
|
|
if (!$r) {
|
|
$resourcePath = '/'
|
|
. trim($m['resourcePath'], '/');
|
|
$r = $this->_operationListing($resourcePath);
|
|
}
|
|
$parts = explode('/', $fullPath);
|
|
$pos = count($parts) - 1;
|
|
if (count($parts) == 1 && $httpMethod == 'GET') {
|
|
} else {
|
|
for ($i = 0; $i < count($parts); $i++) {
|
|
if (strlen($parts[$i]) && $parts[$i]{0} == '{') {
|
|
$pos = $i - 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$nickname = $this->_nickname($route);
|
|
$index = static::$placeFormatExtensionBeforeDynamicParts && $pos > 0 ? $pos : 0;
|
|
if (!empty($parts[$index]))
|
|
$parts[$index] .= $this->formatString;
|
|
|
|
$fullPath = implode('/', $parts);
|
|
$description = isset(
|
|
$m['classDescription'])
|
|
? $m['classDescription']
|
|
: $className . ' API';
|
|
if (empty($m['description'])) {
|
|
$m['description'] = $this->restler->getProductionMode()
|
|
? ''
|
|
: 'routes to <mark>'
|
|
. $route['className']
|
|
. '::'
|
|
. $route['methodName'] . '();</mark>';
|
|
}
|
|
if (empty($m['longDescription'])) {
|
|
$m['longDescription'] = $this->restler->getProductionMode()
|
|
? ''
|
|
: 'Add PHPDoc long description to '
|
|
. "<mark>$className::"
|
|
. $route['methodName'] . '();</mark>'
|
|
. ' (the api method) to write here';
|
|
}
|
|
$operation = $this->_operation(
|
|
$route,
|
|
$nickname,
|
|
$httpMethod,
|
|
$m['description'],
|
|
$m['longDescription']
|
|
);
|
|
if (isset($m['throws'])) {
|
|
foreach ($m['throws'] as $exception) {
|
|
$operation->errorResponses[] = array(
|
|
'reason' => $exception['reason'],
|
|
'code' => $exception['code']);
|
|
}
|
|
}
|
|
if (isset($m['param'])) {
|
|
foreach ($m['param'] as $param) {
|
|
//combine body params as one
|
|
$p = $this->_parameter($param);
|
|
if ($p->paramType == 'body') {
|
|
$this->_appendToBody($p);
|
|
} else {
|
|
$operation->parameters[] = $p;
|
|
}
|
|
}
|
|
}
|
|
if (
|
|
count($this->_bodyParam['description']) ||
|
|
(
|
|
$this->_fullDataRequested &&
|
|
$httpMethod != 'GET' &&
|
|
$httpMethod != 'DELETE'
|
|
)
|
|
) {
|
|
$operation->parameters[] = $this->_getBody();
|
|
}
|
|
if (isset($m['return']['type'])) {
|
|
$responseClass = $m['return']['type'];
|
|
if (is_string($responseClass)) {
|
|
if (class_exists($responseClass)) {
|
|
$this->_model($responseClass);
|
|
$operation->responseClass
|
|
= $this->_noNamespace($responseClass);
|
|
} elseif (strtolower($responseClass) == 'array') {
|
|
$operation->responseClass = 'Array';
|
|
$rt = $m['return'];
|
|
if (isset(
|
|
$rt[CommentParser::$embeddedDataName]['type'])
|
|
) {
|
|
$rt = $rt[CommentParser::$embeddedDataName]
|
|
['type'];
|
|
if (class_exists($rt)) {
|
|
$this->_model($rt);
|
|
$operation->responseClass .= '[' .
|
|
$this->_noNamespace($rt) . ']';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
$api = false;
|
|
|
|
if (static::$groupOperations) {
|
|
foreach ($r->apis as $a) {
|
|
if ($a->path == "/$fullPath") {
|
|
$api = $a;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$api) {
|
|
$api = $this->_api("$prefix/$fullPath", $description);
|
|
$r->apis[] = $api;
|
|
}
|
|
|
|
$api->operations[] = $operation;
|
|
}
|
|
}
|
|
if (!$count) {
|
|
throw new RestException(404);
|
|
}
|
|
if (!is_null($r))
|
|
$r->models = $this->_models;
|
|
usort(
|
|
$r->apis,
|
|
function ($a, $b) {
|
|
$order = array(
|
|
'GET' => 1,
|
|
'POST' => 2,
|
|
'PUT' => 3,
|
|
'PATCH' => 4,
|
|
'DELETE' => 5
|
|
);
|
|
return
|
|
$a->operations[0]->httpMethod ==
|
|
$b->operations[0]->httpMethod
|
|
? $a->path > $b->path
|
|
: $order[$a->operations[0]->httpMethod] >
|
|
$order[$b->operations[0]->httpMethod];
|
|
|
|
}
|
|
);
|
|
return $r;
|
|
}
|
|
|
|
protected function _nickname(array $route)
|
|
{
|
|
static $hash = array();
|
|
$method = $route['methodName'];
|
|
if (isset(static::$prefixes[$method])) {
|
|
$method = static::$prefixes[$method];
|
|
} else {
|
|
$method = str_replace(
|
|
array_keys(static::$prefixes),
|
|
array_values(static::$prefixes),
|
|
$method
|
|
);
|
|
}
|
|
while (isset($hash[$method]) && $route['url'] != $hash[$method]) {
|
|
//create another one
|
|
$method .= '_';
|
|
}
|
|
$hash[$method] = $route['url'];
|
|
return $method;
|
|
}
|
|
|
|
protected function _noNamespace($className)
|
|
{
|
|
$className = explode('\\', $className);
|
|
return end($className);
|
|
}
|
|
|
|
protected function _operationListing($resourcePath = '/')
|
|
{
|
|
$r = $this->_resourceListing();
|
|
$r->resourcePath = $resourcePath;
|
|
$r->models = new stdClass();
|
|
return $r;
|
|
}
|
|
|
|
protected function _resourceListing()
|
|
{
|
|
$r = new stdClass();
|
|
$r->apiVersion = (string)$this->restler->_requestedApiVersion;
|
|
$r->swaggerVersion = "1.1";
|
|
$r->basePath = $this->restler->getBaseUrl();
|
|
$r->produces = $this->restler->getWritableMimeTypes();
|
|
$r->consumes = $this->restler->getReadableMimeTypes();
|
|
$r->apis = array();
|
|
return $r;
|
|
}
|
|
|
|
protected function _api($path, $description = '')
|
|
{
|
|
$r = new stdClass();
|
|
$r->path = $path;
|
|
$r->description =
|
|
empty($description) && $this->restler->getProductionMode()
|
|
? 'Use PHPDoc comment to describe here'
|
|
: $description;
|
|
$r->operations = array();
|
|
return $r;
|
|
}
|
|
|
|
protected function _operation(
|
|
$route,
|
|
$nickname,
|
|
$httpMethod = 'GET',
|
|
$summary = 'description',
|
|
$notes = 'long description',
|
|
$responseClass = 'void'
|
|
)
|
|
{
|
|
//reset body params
|
|
$this->_bodyParam = array(
|
|
'required' => false,
|
|
'description' => array()
|
|
);
|
|
|
|
$r = new stdClass();
|
|
$r->httpMethod = $httpMethod;
|
|
$r->nickname = $nickname;
|
|
$r->responseClass = $responseClass;
|
|
|
|
$r->parameters = array();
|
|
|
|
$r->summary = $summary . ($route['accessLevel'] > 2
|
|
? static::$apiDescriptionSuffixSymbols[2]
|
|
: static::$apiDescriptionSuffixSymbols[$route['accessLevel']]
|
|
);
|
|
$r->notes = $notes;
|
|
|
|
$r->errorResponses = array();
|
|
return $r;
|
|
}
|
|
|
|
protected function _parameter($param)
|
|
{
|
|
$r = new stdClass();
|
|
$r->name = $param['name'];
|
|
$r->description = !empty($param['description'])
|
|
? $param['description'] . '.'
|
|
: ($this->restler->getProductionMode()
|
|
? ''
|
|
: 'add <mark>@param {type} $' . $r->name
|
|
. ' {comment}</mark> to describe here');
|
|
//paramType can be path or query or body or header
|
|
$r->paramType = Util::nestedValue($param, CommentParser::$embeddedDataName, 'from') ? : 'query';
|
|
$r->required = isset($param['required']) && $param['required'];
|
|
if (isset($param['default'])) {
|
|
$r->defaultValue = $param['default'];
|
|
} elseif (isset($param[CommentParser::$embeddedDataName]['example'])) {
|
|
$r->defaultValue
|
|
= $param[CommentParser::$embeddedDataName]['example'];
|
|
}
|
|
$r->allowMultiple = false;
|
|
$type = 'string';
|
|
if (isset($param['type'])) {
|
|
$type = $param['type'];
|
|
if (is_array($type)) {
|
|
$type = array_shift($type);
|
|
}
|
|
if ($type == 'array') {
|
|
$contentType = Util::nestedValue(
|
|
$param,
|
|
CommentParser::$embeddedDataName,
|
|
'type'
|
|
);
|
|
if ($contentType) {
|
|
if ($contentType == 'indexed') {
|
|
$type = 'Array';
|
|
} elseif ($contentType == 'associative') {
|
|
$type = 'Object';
|
|
} else {
|
|
$type = "Array[$contentType]";
|
|
}
|
|
if (Util::isObjectOrArray($contentType)) {
|
|
$this->_model($contentType);
|
|
}
|
|
} elseif (isset(static::$dataTypeAlias[$type])) {
|
|
$type = static::$dataTypeAlias[$type];
|
|
}
|
|
} elseif (Util::isObjectOrArray($type)) {
|
|
$this->_model($type);
|
|
} elseif (isset(static::$dataTypeAlias[$type])) {
|
|
$type = static::$dataTypeAlias[$type];
|
|
}
|
|
}
|
|
$r->dataType = $type;
|
|
if (isset($param[CommentParser::$embeddedDataName])) {
|
|
$p = $param[CommentParser::$embeddedDataName];
|
|
if (isset($p['min']) && isset($p['max'])) {
|
|
$r->allowableValues = array(
|
|
'valueType' => 'RANGE',
|
|
'min' => $p['min'],
|
|
'max' => $p['max'],
|
|
);
|
|
} elseif (isset($p['choice'])) {
|
|
$r->allowableValues = array(
|
|
'valueType' => 'LIST',
|
|
'values' => $p['choice']
|
|
);
|
|
}
|
|
}
|
|
return $r;
|
|
}
|
|
|
|
protected function _appendToBody($p)
|
|
{
|
|
if ($p->name === Defaults::$fullRequestDataName) {
|
|
$this->_fullDataRequested = $p;
|
|
unset($this->_bodyParam['names'][Defaults::$fullRequestDataName]);
|
|
return;
|
|
}
|
|
$this->_bodyParam['description'][$p->name]
|
|
= "$p->name"
|
|
. ' : <tag>' . $p->dataType . '</tag> '
|
|
. ($p->required ? ' <i>(required)</i> - ' : ' - ')
|
|
. $p->description;
|
|
$this->_bodyParam['required'] = $p->required
|
|
|| $this->_bodyParam['required'];
|
|
$this->_bodyParam['names'][$p->name] = $p;
|
|
}
|
|
|
|
protected function _getBody()
|
|
{
|
|
$r = new stdClass();
|
|
$n = isset($this->_bodyParam['names'])
|
|
? array_values($this->_bodyParam['names'])
|
|
: array();
|
|
if (count($n) == 1) {
|
|
if (isset($this->_models->{$n[0]->dataType})) {
|
|
// ============ custom class ===================
|
|
$r = $n[0];
|
|
$c = $this->_models->{$r->dataType};
|
|
$a = $c->properties;
|
|
$r->description = "Paste JSON data here";
|
|
if (count($a)) {
|
|
$r->description .= " with the following"
|
|
. (count($a) > 1 ? ' properties.' : ' property.');
|
|
foreach ($a as $k => $v) {
|
|
$r->description .= "<hr/>$k : <tag>"
|
|
. $v['type'] . '</tag> '
|
|
. (isset($v['required']) ? '(required)' : '')
|
|
. ' - ' . $v['description'];
|
|
}
|
|
}
|
|
$r->defaultValue = "{\n \""
|
|
. implode("\": \"\",\n \"",
|
|
array_keys($c->properties))
|
|
. "\": \"\"\n}";
|
|
return $r;
|
|
} elseif (false !== ($p = strpos($n[0]->dataType, '['))) {
|
|
// ============ array of custom class ===============
|
|
$r = $n[0];
|
|
$t = substr($r->dataType, $p + 1, -1);
|
|
if ($c = Util::nestedValue($this->_models, $t)) {
|
|
$a = $c->properties;
|
|
$r->description = "Paste JSON data here";
|
|
if (count($a)) {
|
|
$r->description .= " with an array of objects with the following"
|
|
. (count($a) > 1 ? ' properties.' : ' property.');
|
|
foreach ($a as $k => $v) {
|
|
$r->description .= "<hr/>$k : <tag>"
|
|
. $v['type'] . '</tag> '
|
|
. (isset($v['required']) ? '(required)' : '')
|
|
. ' - ' . $v['description'];
|
|
}
|
|
}
|
|
$r->defaultValue = "[\n {\n \""
|
|
. implode("\": \"\",\n \"",
|
|
array_keys($c->properties))
|
|
. "\": \"\"\n }\n]";
|
|
return $r;
|
|
} else {
|
|
$r->description = "Paste JSON data here with an array of $t values.";
|
|
$r->defaultValue = "[ ]";
|
|
return $r;
|
|
}
|
|
} elseif ($n[0]->dataType == 'Array') {
|
|
// ============ array ===============================
|
|
$r = $n[0];
|
|
$r->description = "Paste JSON array data here"
|
|
. ($r->required ? ' (required) . ' : '. ')
|
|
. "<br/>$r->description";
|
|
$r->defaultValue = "[\n {\n \""
|
|
. "property\" : \"\"\n }\n]";
|
|
return $r;
|
|
} elseif ($n[0]->dataType == 'Object') {
|
|
// ============ object ==============================
|
|
$r = $n[0];
|
|
$r->description = "Paste JSON object data here"
|
|
. ($r->required ? ' (required) . ' : '. ')
|
|
. "<br/>$r->description";
|
|
$r->defaultValue = "{\n \""
|
|
. "property\" : \"\"\n}";
|
|
return $r;
|
|
}
|
|
}
|
|
$p = array_values($this->_bodyParam['description']);
|
|
$r->name = 'REQUEST_BODY';
|
|
$r->description = "Paste JSON data here";
|
|
if (count($p) == 0 && $this->_fullDataRequested) {
|
|
$r->required = $this->_fullDataRequested->required;
|
|
$r->defaultValue = "{\n \"property\" : \"\"\n}";
|
|
} else {
|
|
$r->description .= " with the following"
|
|
. (count($p) > 1 ? ' properties.' : ' property.')
|
|
. '<hr/>'
|
|
. implode("<hr/>", $p);
|
|
$r->required = $this->_bodyParam['required'];
|
|
// Create default object that includes parameters to be submitted
|
|
$defaultObject = new \StdClass();
|
|
foreach ($this->_bodyParam['names'] as $name => $values) {
|
|
if (!$values->required)
|
|
continue;
|
|
if (class_exists($values->dataType)) {
|
|
$myClassName = $values->dataType;
|
|
$defaultObject->$name = new $myClassName();
|
|
} else {
|
|
$defaultObject->$name = '';
|
|
}
|
|
}
|
|
$r->defaultValue = Scope::get('JsonFormat')->encode($defaultObject, true);
|
|
}
|
|
$r->paramType = 'body';
|
|
$r->allowMultiple = false;
|
|
$r->dataType = 'Object';
|
|
return $r;
|
|
}
|
|
|
|
protected function _model($className, $instance = null)
|
|
{
|
|
$id = $this->_noNamespace($className);
|
|
if (isset($this->_models->{$id})) {
|
|
return;
|
|
}
|
|
$properties = array();
|
|
if (!$instance) {
|
|
if (!class_exists($className))
|
|
return;
|
|
$instance = new $className();
|
|
}
|
|
$data = get_object_vars($instance);
|
|
$reflectionClass = new \ReflectionClass($className);
|
|
foreach ($data as $key => $value) {
|
|
|
|
$propertyMetaData = null;
|
|
|
|
try {
|
|
$property = $reflectionClass->getProperty($key);
|
|
if ($c = $property->getDocComment()) {
|
|
$propertyMetaData = Util::nestedValue(
|
|
CommentParser::parse($c),
|
|
'var'
|
|
);
|
|
}
|
|
} catch (\ReflectionException $e) {
|
|
}
|
|
|
|
if (is_null($propertyMetaData)) {
|
|
$type = $this->getType($value, true);
|
|
$description = '';
|
|
} else {
|
|
$type = Util::nestedValue(
|
|
$propertyMetaData,
|
|
'type'
|
|
) ? : $this->getType($value, true);
|
|
$description = Util::nestedValue(
|
|
$propertyMetaData,
|
|
'description'
|
|
) ? : '';
|
|
|
|
if (class_exists($type)) {
|
|
$this->_model($type);
|
|
$type = $this->_noNamespace($type);
|
|
}
|
|
}
|
|
|
|
if (isset(static::$dataTypeAlias[$type])) {
|
|
$type = static::$dataTypeAlias[$type];
|
|
}
|
|
$properties[$key] = array(
|
|
'type' => $type,
|
|
'description' => $description
|
|
);
|
|
if (Util::nestedValue(
|
|
$propertyMetaData,
|
|
CommentParser::$embeddedDataName,
|
|
'required'
|
|
)
|
|
) {
|
|
$properties[$key]['required'] = true;
|
|
}
|
|
if ($type == 'Array') {
|
|
$itemType = Util::nestedValue(
|
|
$propertyMetaData,
|
|
CommentParser::$embeddedDataName,
|
|
'type'
|
|
) ? :
|
|
(count($value)
|
|
? $this->getType(end($value), true)
|
|
: 'string');
|
|
if (class_exists($itemType)) {
|
|
$this->_model($itemType);
|
|
$itemType = $this->_noNamespace($itemType);
|
|
}
|
|
$properties[$key]['item'] = array(
|
|
'type' => $itemType,
|
|
/*'description' => '' */ //TODO: add description
|
|
);
|
|
} else if (preg_match('/^Array\[(.+)\]$/', $type, $matches)) {
|
|
$itemType = $matches[1];
|
|
$properties[$key]['type'] = 'Array';
|
|
$properties[$key]['item']['type'] = $itemType;
|
|
|
|
if (class_exists($itemType)) {
|
|
$this->_model($itemType);
|
|
}
|
|
}
|
|
}
|
|
if (!empty($properties)) {
|
|
$model = new stdClass();
|
|
$model->id = $id;
|
|
$model->properties = $properties;
|
|
$this->_models->{$id} = $model;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the data type of the given value.
|
|
*
|
|
*
|
|
* @param mixed $o given value for finding type
|
|
*
|
|
* @param bool $appendToModels if an object is found should we append to
|
|
* our models list?
|
|
*
|
|
* @return string
|
|
*
|
|
* @access private
|
|
*/
|
|
public function getType($o, $appendToModels = false)
|
|
{
|
|
if (is_object($o)) {
|
|
$oc = get_class($o);
|
|
if ($appendToModels) {
|
|
$this->_model($oc, $o);
|
|
}
|
|
return $this->_noNamespace($oc);
|
|
}
|
|
if (is_array($o)) {
|
|
if (count($o)) {
|
|
$child = end($o);
|
|
if (Util::isObjectOrArray($child)) {
|
|
$childType = $this->getType($child, $appendToModels);
|
|
return "Array[$childType]";
|
|
}
|
|
}
|
|
return 'array';
|
|
}
|
|
if (is_bool($o)) return 'boolean';
|
|
if (is_numeric($o)) return is_float($o) ? 'float' : 'int';
|
|
return 'string';
|
|
}
|
|
|
|
/**
|
|
* pre call for index()
|
|
*
|
|
* if cache is present, use cache
|
|
*/
|
|
public function _pre_index_json()
|
|
{
|
|
$userClass = Defaults::$userIdentifierClass;
|
|
$this->cacheName = $userClass::getCacheIdentifier()
|
|
. '_resources-v'
|
|
. $this->restler->_requestedApiVersion;
|
|
if ($this->restler->getProductionMode()
|
|
&& !$this->restler->refreshCache
|
|
&& $this->restler->cache->isCached($this->cacheName)
|
|
) {
|
|
//by pass call, compose, postCall stages and directly send response
|
|
$this->restler->composeHeaders();
|
|
die($this->restler->cache->get($this->cacheName));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* post call for index()
|
|
*
|
|
* create cache if in production mode
|
|
*
|
|
* @param $responseData
|
|
*
|
|
* @internal param string $data composed json output
|
|
*
|
|
* @return string
|
|
*/
|
|
public function _post_index_json($responseData)
|
|
{
|
|
if ($this->restler->getProductionMode()) {
|
|
$this->restler->cache->set($this->cacheName, $responseData);
|
|
}
|
|
return $responseData;
|
|
}
|
|
|
|
/**
|
|
* @access hybrid
|
|
* @return \stdClass
|
|
*/
|
|
public function index()
|
|
{
|
|
if (!static::$accessControlFunction && Defaults::$accessControlFunction)
|
|
static::$accessControlFunction = Defaults::$accessControlFunction;
|
|
$version = $this->restler->getRequestedApiVersion();
|
|
$allRoutes = Util::nestedValue(Routes::toArray(), "v$version");
|
|
$r = $this->_resourceListing();
|
|
$map = array();
|
|
if (isset($allRoutes['*'])) {
|
|
$this->_mapResources($allRoutes['*'], $map, $version);
|
|
unset($allRoutes['*']);
|
|
}
|
|
$this->_mapResources($allRoutes, $map, $version);
|
|
foreach ($map as $path => $description) {
|
|
if (!String::contains($path, '{')) {
|
|
//add id
|
|
$r->apis[] = array(
|
|
'path' => $path . $this->formatString,
|
|
'description' => $description
|
|
);
|
|
}
|
|
}
|
|
if (Defaults::$useUrlBasedVersioning && static::$listHigherVersions) {
|
|
$nextVersion = $version + 1;
|
|
if ($nextVersion <= $this->restler->getApiVersion()) {
|
|
list($status, $data) = $this->_loadResource("/v$nextVersion/resources.json");
|
|
if ($status == 200) {
|
|
$r->apis = array_merge($r->apis, $data->apis);
|
|
$r->apiVersion = $data->apiVersion;
|
|
}
|
|
}
|
|
|
|
}
|
|
return $r;
|
|
}
|
|
|
|
protected function _loadResource($url)
|
|
{
|
|
$ch = curl_init($this->restler->getBaseUrl() . $url
|
|
. (empty($_GET) ? '' : '?' . http_build_query($_GET)));
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
|
'Accept:application/json',
|
|
));
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
|
|
$result = json_decode(curl_exec($ch));
|
|
$http_status = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
return array($http_status, $result);
|
|
}
|
|
|
|
protected function _mapResources(array $allRoutes, array &$map, $version = 1)
|
|
{
|
|
foreach ($allRoutes as $fullPath => $routes) {
|
|
$path = explode('/', $fullPath);
|
|
$resource = isset($path[0]) ? $path[0] : '';
|
|
if ($resource == 'resources' || String::endsWith($resource, 'index'))
|
|
continue;
|
|
foreach ($routes as $httpMethod => $route) {
|
|
if (in_array($httpMethod, static::$excludedHttpMethods)) {
|
|
continue;
|
|
}
|
|
if (!static::verifyAccess($route)) {
|
|
continue;
|
|
}
|
|
|
|
foreach (static::$excludedPaths as $exclude) {
|
|
if (empty($exclude)) {
|
|
if ($fullPath == $exclude)
|
|
continue 2;
|
|
} elseif (String::beginsWith($fullPath, $exclude)) {
|
|
continue 2;
|
|
}
|
|
}
|
|
|
|
$res = $resource
|
|
? ($version == 1 ? "/resources/$resource" : "/v$version/resources/$resource-v$version")
|
|
: ($version == 1 ? "/resources/root" : "/v$version/resources/root-v$version");
|
|
|
|
if (empty($map[$res])) {
|
|
$map[$res] = isset(
|
|
$route['metadata']['classDescription'])
|
|
? $route['metadata']['classDescription'] : '';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maximum api version supported by the api class
|
|
* @return int
|
|
*/
|
|
public static function __getMaximumSupportedVersion()
|
|
{
|
|
return Scope::get('Restler')->getApiVersion();
|
|
}
|
|
|
|
/**
|
|
* Verifies that the requesting user is allowed to view the docs for this API
|
|
*
|
|
* @param $route
|
|
*
|
|
* @return boolean True if the user should be able to view this API's docs
|
|
*/
|
|
protected function verifyAccess($route)
|
|
{
|
|
if ($route['accessLevel'] < 2) {
|
|
return true;
|
|
}
|
|
if (
|
|
static::$hideProtected
|
|
&& !$this->_authenticated
|
|
&& $route['accessLevel'] > 1
|
|
) {
|
|
return false;
|
|
}
|
|
if ($this->_authenticated
|
|
&& static::$accessControlFunction
|
|
&& (!call_user_func(
|
|
static::$accessControlFunction, $route['metadata']))
|
|
) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
}
|