Files
EasySoft-ZenTaoPMS/framework/api/router.class.php
T

631 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();
/* 以POST的值为准。 Set GET value from POST data. */
foreach($_POST as $key => $value)
{
if(isset($this->params[$key])) $this->params[$key] = $value;
}
$this->control->viewType = 'html';
$this->control->getFormData = true;
$zen = $this->control->moduleName . 'Zen';
$this->control->$zen->getFormData = true;
$method = $this->control->methodName;
call_user_func_array(array($this->control, $method), $this->params);
$this->control->getFormData = false;
$this->control->$zen->getFormData = false;
$this->control->viewType = 'json';
$_POST = $postData;
foreach($this->control->formData as $key => $value)
{
if(!isset($_POST[$key])) $_POST[$key] = $value;
}
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;
}
}