622 lines
17 KiB
PHP
622 lines
17 KiB
PHP
<?php declare(strict_types=1);
|
|
/**
|
|
* 禅道API的api类。
|
|
* The api class file of ZenTao API.
|
|
*
|
|
* The author disclaims copyright to this source code. In place of
|
|
* a legal notice, here is a blessing:
|
|
*
|
|
* May you do good and not evil.
|
|
* May you find forgiveness for yourself and forgive others.
|
|
* May you share freely, never taking more than you give.
|
|
*/
|
|
include dirname(__FILE__, 2) . '/router.class.php';
|
|
class api extends router
|
|
{
|
|
/**
|
|
* 请求API的路径
|
|
* The requested path of api.
|
|
*
|
|
* @var string
|
|
* @access public
|
|
*/
|
|
public $path;
|
|
|
|
/**
|
|
* 请求API的参数,包括键值
|
|
* The requested params of api: key and value.
|
|
*
|
|
* @var array
|
|
* @access public
|
|
*/
|
|
public $params = array();
|
|
|
|
/**
|
|
* 请求API的参数名
|
|
* The requested param names of api.
|
|
*
|
|
* @var array
|
|
* @access public
|
|
*/
|
|
public $paramNames = array();
|
|
|
|
/**
|
|
* 请求的资源名称
|
|
* The requested entry point
|
|
*
|
|
* @var string
|
|
* @access public
|
|
*/
|
|
public $entry;
|
|
|
|
/**
|
|
* API资源的执行方法: get post put delete
|
|
* The action of entry point: get post put delete
|
|
*
|
|
* @var string
|
|
* @access public
|
|
*/
|
|
public $action;
|
|
|
|
/**
|
|
* 选择性输出json数据
|
|
* Extract json data
|
|
*
|
|
* @var string
|
|
* @access public
|
|
*/
|
|
public $responseExtractor = '*';
|
|
|
|
/**
|
|
* 构造方法, 设置请求路径,版本等
|
|
*
|
|
* The construct function.
|
|
* Prepare all the paths, version and so on.
|
|
*
|
|
* @access public
|
|
* @return void
|
|
*/
|
|
public function __construct(string $appName = 'api', string $appRoot = '')
|
|
{
|
|
parent::__construct($appName, $appRoot);
|
|
|
|
$this->viewType = 'json';
|
|
$this->httpMethod = strtolower((string) $_SERVER['REQUEST_METHOD']);
|
|
|
|
$this->path = trim(substr((string) $_SERVER['REQUEST_URI'], strpos((string) $_SERVER['REQUEST_URI'], 'api.php') + 7), '/');
|
|
if(strpos($this->path, '?') > 0) $this->path = strstr($this->path, '?', true);
|
|
|
|
$subPos = $this->path ? strpos($this->path, '/') : false;
|
|
|
|
$this->apiVersion = $subPos !== false ? substr($this->path, 0, $subPos) : '';
|
|
$this->path = $subPos !== false ? substr($this->path, $subPos) : '';
|
|
|
|
$this->loadApiLang();
|
|
}
|
|
|
|
/**
|
|
* 解析请求路径,找到处理方法
|
|
*
|
|
* Parse request path, find entry and action.
|
|
*
|
|
* @param array $routes
|
|
* @access public
|
|
* @return void
|
|
*/
|
|
public function route(array $routes)
|
|
{
|
|
foreach($routes as $route => $target)
|
|
{
|
|
$patternAsRegex = preg_replace_callback(
|
|
'#:([\w]+)\+?#',
|
|
$this->matchesCallback(...),
|
|
str_replace(')', ')?', $route)
|
|
);
|
|
if(str_ends_with($route, '/')) $patternAsRegex .= '?';
|
|
|
|
/* Cache URL params' names and values if this route matches the current HTTP request. */
|
|
if(!preg_match('#^' . $patternAsRegex . '$#', $this->path, $paramValues)) continue;
|
|
|
|
/* Set module and action */
|
|
$this->entry = $target;
|
|
$this->action = strtolower((string) $_SERVER['REQUEST_METHOD']);
|
|
|
|
/* Set params */
|
|
foreach($this->paramNames as $name)
|
|
{
|
|
if(!isset($paramValues[$name])) continue;
|
|
|
|
$this->params[$name] = urldecode($paramValues[$name]);
|
|
}
|
|
return;
|
|
}
|
|
|
|
$this->entry = 'error';
|
|
$this->action = 'notFound';
|
|
}
|
|
|
|
/**
|
|
* 复数转单数
|
|
* Converting plural nouns to singular.
|
|
*
|
|
* @param string $word
|
|
* @access public
|
|
* @return string
|
|
*/
|
|
public function singular($word)
|
|
{
|
|
/* 特殊词处理 */
|
|
$irregular = array(
|
|
'children' => 'child',
|
|
'men' => 'man',
|
|
'women' => 'woman',
|
|
'people' => 'person',
|
|
'feet' => 'foot',
|
|
'teeth' => 'tooth',
|
|
'mice' => 'mouse',
|
|
'geese' => 'goose',
|
|
'oxen' => 'ox',
|
|
'cacti' => 'cactus',
|
|
'foci' => 'focus',
|
|
'nuclei' => 'nucleus',
|
|
'syllabi' => 'syllabus',
|
|
'radii' => 'radius',
|
|
'phenomena' => 'phenomenon',
|
|
'criteria' => 'criterion',
|
|
'data' => 'datum',
|
|
'media' => 'medium',
|
|
'lice' => 'louse',
|
|
'selves' => 'self',
|
|
'loaves' => 'loaf',
|
|
'leaves' => 'leaf',
|
|
'lives' => 'life',
|
|
'wives' => 'wife',
|
|
'knives' => 'knife',
|
|
'wolves' => 'wolf',
|
|
'elves' => 'elf',
|
|
'halves' => 'half',
|
|
'scarves' => 'scarf',
|
|
'hooves' => 'hoof',
|
|
'veterans' => 'veteran', // 特殊情况示例
|
|
);
|
|
|
|
if(isset($irregular[strtolower($word)])) {
|
|
$lowerWord = strtolower($word);
|
|
$singular = $irregular[$lowerWord];
|
|
if (ctype_upper($word[0])) {
|
|
$singular = ucfirst($singular);
|
|
}
|
|
return $singular;
|
|
}
|
|
|
|
$rules = [
|
|
'/sses$/i' => 'ss',
|
|
'/ies$/i' => 'y',
|
|
'/ves$/i' => 'f',
|
|
'/zes$/i' => 'z',
|
|
'/ches$/i' => 'ch',
|
|
'/shes$/i' => 'sh',
|
|
'/men$/i' => 'man',
|
|
'/s$/i' => '',
|
|
];
|
|
|
|
foreach($rules as $pattern => $replacement)
|
|
{
|
|
if(preg_match($pattern, $word)) return preg_replace($pattern, $replacement, $word, 1);
|
|
}
|
|
|
|
return $word;
|
|
}
|
|
|
|
/**
|
|
* 路由正则匹配
|
|
* Match routes.
|
|
*
|
|
* @param array $routes
|
|
* @access private
|
|
* @return array
|
|
*/
|
|
private function matchRoutes($routes)
|
|
{
|
|
foreach($routes as $route => $info)
|
|
{
|
|
$patternAsRegex = preg_replace_callback(
|
|
'#:([\w]+)\+?#',
|
|
$this->matchesCallback(...),
|
|
str_replace(')', ')?', $route)
|
|
);
|
|
if(str_ends_with($route, '/')) $patternAsRegex .= '?';
|
|
|
|
/* Cache URL params' names and values if this route matches the current HTTP request. */
|
|
if(!preg_match('#^' . $patternAsRegex . '$#', $this->path, $paramValues)) continue;
|
|
|
|
return array($info, $paramValues);
|
|
}
|
|
|
|
return array(null, array());
|
|
}
|
|
|
|
/**
|
|
* API2.0 根据路由表设置path和params
|
|
* API2.0 Set path, params by routes.
|
|
*
|
|
* @param array $routes
|
|
* @access private
|
|
* @return void
|
|
*/
|
|
public function parseRouteV2($routes)
|
|
{
|
|
list($info, $paramValues) = $this->matchRoutes($routes);
|
|
|
|
if($info)
|
|
{
|
|
if(isset($info['redirect']))
|
|
{
|
|
foreach($paramValues as $key => $value)
|
|
{
|
|
if(is_numeric($key)) continue;
|
|
|
|
$_GET[$key] = $value;
|
|
$info['redirect'] = str_replace(':'.$key, $value, $info['redirect']);
|
|
}
|
|
if(isset($info['response'])) $this->responseExtractor = $info['response'];
|
|
|
|
$url = parse_url($info['redirect']);
|
|
$this->path = $url['path'];
|
|
|
|
if(isset($url['query']))
|
|
{
|
|
parse_str($url['query'], $params);
|
|
foreach($params as $key => $value) $_GET[$key] = $value;
|
|
}
|
|
|
|
list($info, $paramValues) = $this->matchRoutes($routes);
|
|
}
|
|
|
|
if(isset($info['response'])) $this->responseExtractor = $info['response'];
|
|
}
|
|
|
|
foreach($paramValues as $key => $value)
|
|
{
|
|
if(is_numeric($key)) continue;
|
|
$_GET[$key] = $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* API2.0 路由
|
|
* API2.0 routing.
|
|
*
|
|
* @param array $routes
|
|
* @access private
|
|
* @return array
|
|
*/
|
|
public function routeV2($routes)
|
|
{
|
|
$this->action = strtolower((string) $_SERVER['REQUEST_METHOD']);
|
|
|
|
if($this->action == 'get') $this->parseRouteV2($routes);
|
|
|
|
$pathItems = explode('/', trim($this->path, '/'));
|
|
$moduleName = $this->singular($pathItems[0]);
|
|
|
|
$actionToMethod = array(
|
|
'get' => 'browse',
|
|
'post' => 'create',
|
|
'put' => 'edit',
|
|
'delete' => 'delete'
|
|
);
|
|
|
|
$methodName = $actionToMethod[$this->action];
|
|
|
|
if(isset($pathItems[1]))
|
|
{
|
|
if(is_numeric($pathItems[1]))
|
|
{
|
|
if($this->action == 'get')
|
|
{
|
|
$methodName = 'view';
|
|
}
|
|
else
|
|
{
|
|
$_GET[$moduleName.'ID'] = $pathItems[1];
|
|
}
|
|
}
|
|
else
|
|
{
|
|
$methodName = $pathItems[1];
|
|
}
|
|
}
|
|
|
|
if(isset($pathItems[2]))
|
|
{
|
|
$methodName = $pathItems[2];
|
|
}
|
|
|
|
$this->setModuleName($moduleName);
|
|
$this->setMethodName($methodName);
|
|
$this->setControlFile();
|
|
}
|
|
|
|
/**
|
|
* 将路由路径参数转化为正则
|
|
*
|
|
* Parse params of route to regular expression.
|
|
*
|
|
* @param array $m
|
|
* @access protected
|
|
* @return string
|
|
*/
|
|
protected function matchesCallback(array $m)
|
|
{
|
|
$this->paramNames[] = $m[1];
|
|
return '(?P<' . $m[1] . '>[^/]+)';
|
|
}
|
|
|
|
/**
|
|
* 解析访问请求
|
|
*
|
|
* Parse request.
|
|
*
|
|
* @access public
|
|
* @return void
|
|
*/
|
|
public function parseRequest()
|
|
{
|
|
/* If version of api don't exists, call parent method. */
|
|
if(!$this->apiVersion) return parent::parseRequest();
|
|
|
|
global $routes;
|
|
if($this->apiVersion == 'v1')
|
|
{
|
|
include $this->appRoot . "config/apiv1.php";
|
|
if(isset($this->config->routes)) $routes = array_merge($routes, $this->config->routes);
|
|
$this->route($routes);
|
|
}
|
|
else
|
|
{
|
|
include $this->appRoot . "config/apiv2.php";
|
|
$this->routeV2($routes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 执行对应模块
|
|
*
|
|
* Load the running module.
|
|
*
|
|
* @access public
|
|
* @return void
|
|
*/
|
|
public function loadModule()
|
|
{
|
|
try
|
|
{
|
|
/* If the version of api don't exists, call parent method. */
|
|
if($this->apiVersion == 'v2')
|
|
{
|
|
$this->setParams();
|
|
if(in_array($this->action, array('post', 'put')))
|
|
{
|
|
$this->setFormData();
|
|
}
|
|
return parent::loadModule();
|
|
}
|
|
elseif(!$this->apiVersion)
|
|
{
|
|
parent::setParams();
|
|
return parent::loadModule();
|
|
}
|
|
|
|
/* api v1. */
|
|
$entry = strtolower($this->entry);
|
|
$filename = $this->appRoot . "api/$this->apiVersion/entries/$entry.php";
|
|
|
|
if(file_exists($filename)) include($filename);
|
|
|
|
$entryName = $this->entry . 'Entry';
|
|
|
|
if($entry == 'error' && !class_exists($entryName)) include($this->appRoot . "api/v1/entries/$entry.php");
|
|
|
|
$entry = new $entryName();
|
|
|
|
if($this->action == 'options') throw EndResponseException::create($entry->send(204));
|
|
|
|
echo call_user_func_array(array($entry, $this->action), array_values($this->params));
|
|
|
|
$this->outputXhprof();
|
|
}
|
|
catch(EndResponseException $endResponseException)
|
|
{
|
|
echo $endResponseException->getContent();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置form data。
|
|
* Set form data.
|
|
*
|
|
* @access public
|
|
* @return void
|
|
*/
|
|
public function setFormData()
|
|
{
|
|
$requestBody = file_get_contents("php://input");
|
|
if(!$requestBody) return;
|
|
|
|
$_POST = json_decode($requestBody, true);
|
|
|
|
/* 更新操作的表单需要拼接原始的值。 Merge original values. */
|
|
/* Get form data by get request. */
|
|
$postData = $_POST;
|
|
$_POST = array();
|
|
|
|
$this->control->viewType = 'html';
|
|
$this->control->getFormData = true;
|
|
|
|
$method = $this->control->methodName;
|
|
call_user_func_array(array($this->control, $method), $_GET);
|
|
|
|
$this->control->getFormData = false;
|
|
$this->control->viewType = 'json';
|
|
|
|
$_POST = $postData;
|
|
foreach($this->control->formData as $key => $value)
|
|
{
|
|
if(!isset($_POST[$key])) $_POST[$key] = $value;
|
|
}
|
|
|
|
$zen = $this->control->moduleName . 'Zen';
|
|
foreach($this->control->$zen->formData as $key => $value)
|
|
{
|
|
if(!isset($_POST[$key])) $_POST[$key] = $value;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 设置要被调用方法的参数。
|
|
* Set the params of method calling.
|
|
*
|
|
* @access public
|
|
* @return void
|
|
*/
|
|
public function setParams()
|
|
{
|
|
$defaultParams = $this->getDefaultParams();
|
|
|
|
$this->params = array();
|
|
|
|
/* POST/PUT/DELETE methods have no correct param name, use index. */
|
|
if($this->action != 'get')
|
|
{
|
|
$values = array_values($_GET);
|
|
$index = 0;
|
|
foreach($defaultParams as $key => $defaultItem)
|
|
{
|
|
if(isset($values[$index]))
|
|
{
|
|
$value = $values[$index];
|
|
settype($value, $defaultItem['type']);
|
|
$_GET[$key] = $value;
|
|
}
|
|
$index++;
|
|
}
|
|
}
|
|
|
|
foreach($defaultParams as $key => $defaultItem)
|
|
{
|
|
if(isset($_GET[$key]))
|
|
{
|
|
$this->params[$key] = helper::convertType(strip_tags((string) $_GET[$key]), $defaultItem['type']);
|
|
}
|
|
else
|
|
{
|
|
$this->params[$key] = $defaultItem['default'];
|
|
}
|
|
}
|
|
|
|
if($this->config->framework->filterParam == 2)
|
|
{
|
|
$_GET = validater::filterParam($_GET, 'get');
|
|
$_COOKIE = validater::filterParam($_COOKIE, 'cookie');
|
|
}
|
|
|
|
$this->rawParams = $this->params;
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* 加载配置文件
|
|
*
|
|
* Load config file of api.
|
|
*
|
|
* @param string $configPath
|
|
* @access public
|
|
* @return void
|
|
*/
|
|
public function loadApiConfig(string $configPath)
|
|
{
|
|
global $config;
|
|
include($this->appRoot . "api/$this->apiVersion/config/$configPath.php");
|
|
}
|
|
|
|
/**
|
|
* 加载语言文件
|
|
*
|
|
* Load lang file of api.
|
|
*
|
|
* @access public
|
|
* @return void
|
|
*/
|
|
public function loadApiLang()
|
|
{
|
|
global $lang;
|
|
$filename = $this->appRoot . "api/$this->apiVersion/lang/$this->clientLang.php";
|
|
if($this->apiVersion && file_exists($filename)) include($filename);
|
|
}
|
|
|
|
/**
|
|
* 格式化旧版本API响应数据
|
|
*
|
|
* Format old version data.
|
|
*
|
|
* @param string
|
|
* @access public
|
|
* @return string
|
|
*/
|
|
public function formatData(string $output)
|
|
{
|
|
/* If the version exists, return output directly. */
|
|
if($this->apiVersion) return $output;
|
|
|
|
$output = json_decode((string) $output);
|
|
|
|
$data = new stdClass();
|
|
$data->status = $output->status ?? $output->result;
|
|
if(isset($output->message)) $data->message = $output->message;
|
|
if(isset($output->data)) $data->data = json_decode((string) $output->data);
|
|
if(isset($output->id)) $data->id = $output->id;
|
|
$output = json_encode($data);
|
|
|
|
unset($_SESSION['ENTRY_CODE']);
|
|
unset($_SESSION['VALID_ENTRY']);
|
|
|
|
return $output;
|
|
}
|
|
|
|
/**
|
|
* 设置vision。
|
|
* set Debug.
|
|
*
|
|
* @access public
|
|
* @return void
|
|
*/
|
|
public function setVision()
|
|
{
|
|
$account = isset($_SESSION['user']) ? $_SESSION['user']->account : '';
|
|
if(empty($account) and isset($_POST['account'])) $account = $_POST['account'];
|
|
if(empty($account) and isset($_GET['account'])) $account = $_GET['account'];
|
|
|
|
$vision = 'rnd';
|
|
if($this->config->installed and validater::checkAccount($account))
|
|
{
|
|
$sql = new sql();
|
|
$account = $sql->quote($account);
|
|
|
|
$user = $this->dbh->query("SELECT * FROM " . TABLE_USER . " WHERE account = $account AND deleted = '0' LIMIT 1")->fetch();
|
|
if(!empty($user->visions))
|
|
{
|
|
$userVisions = explode(',', $user->visions);
|
|
if(!in_array($vision, $userVisions)) $vision = '';
|
|
if(empty($vision)) list($vision) = $userVisions;
|
|
}
|
|
}
|
|
|
|
list($defaultVision) = explode(',', trim($this->config->visions, ','));
|
|
if($vision and strpos($this->config->visions, ",{$vision},") === false) $vision = $defaultVision;
|
|
|
|
$this->config->vision = $vision ? $vision : $defaultVision;
|
|
}
|
|
}
|