znt/framework/api/router.class.php

840 lines
24 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 = '')
{
$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) : '';
parent::__construct($appName, $appRoot);
$this->viewType = 'json';
$this->httpMethod = strtolower((string) $_SERVER['REQUEST_METHOD']);
$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 string
*/
public function parseRouteV2($routes)
{
$methodName = '';
list($info, $paramValues) = $this->matchRoutes($routes);
if($info)
{
if(isset($info['method'])) $methodName = $info['method'];
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['method'])) $methodName = $info['method'];
}
if(isset($info['response']) && $this->responseExtractor == '*') $this->responseExtractor = $info['response'];
}
foreach($paramValues as $key => $value)
{
if(is_numeric($key)) continue;
$_GET[$key] = $value;
}
return $methodName;
}
/**
* API2.0 路由
* API2.0 routing.
*
* @param array $routes
* @access private
* @return array
*/
public function routeV2($routes)
{
$this->action = strtolower((string) $_SERVER['REQUEST_METHOD']);
$methodName = '';
if($this->action == 'get') $methodName = $this->parseRouteV2($routes);
$pathItems = explode('/', trim($this->path, '/'));
$moduleName = $this->singular($pathItems[0]);
$actionToMethod = array(
'get' => 'browse',
'post' => 'create',
'put' => 'edit',
'delete' => 'delete'
);
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];
if(!$methodName) $methodName = $actionToMethod[$this->action];
/* File is special. */
if($moduleName == 'file' && $this->action == 'post')
{
$methodName = 'ajaxUpload';
$_GET['field'] = 'file';
$_GET['objectType'] = zget($_POST, 'objectType', '');
$_GET['objectID'] = zget($_POST, 'objectID', '');
}
$this->setModuleName($moduleName);
$this->setMethodName($methodName);
$this->setControlFile();
/* Set default params and post data to delete.*/
if($this->action == 'delete')
{
$defaultParams = $this->getDefaultParams();
if(isset($defaultParams['confirm'])) $_GET['confirm'] = 'yes';
}
}
/**
* 将路由路径参数转化为正则
*
* 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);
}
}
/**
* 检查传入的对象是否有权限访问
*
* Check object priv.
*
* @param object $object
* @param string $table
* @access public
* @return bool
*/
public function checkObjectPriv(object $object, string $table): bool
{
if($this->user->admin) return true;
$userView = $this->user->view;
switch($table)
{
case TABLE_STORY:
case TABLE_BUG:
case TABLE_CASE:
case TABLE_TICKET:
case TABLE_FEEDBACK:
case TABLE_PRODUCTPLAN:
return (!$object->product || strpos(",{$userView->products},", ",$object->product,") !== false);
case TABLE_PRODUCT:
return (!$object->id || strpos(",{$userView->products},", ",$object->id,") !== false);
case TABLE_PROJECT: // project,execution,program
$projects = ",{$userView->sprints},{$userView->projects},{$userView->programs},";
return (!$object->id || strpos($projects, ",$object->id,") !== false);
case TABLE_BUILD:
case TABLE_TASK:
return (!$object->execution || strpos(",{$userView->sprints},", ",$object->execution,") !== false);
default:
return true;
}
return false;
}
/**
* 检查传入的对象是否可以访问
*
* Check access.
*
* @access public
* @return void
*/
public function checkAccess()
{
$objectMap = [
'program' => TABLE_PROJECT,
'programID' => TABLE_PROJECT,
'product' => TABLE_PRODUCT,
'products' => TABLE_PRODUCT,
'productID' => TABLE_PRODUCT,
'project' => TABLE_PROJECT,
'projectID' => TABLE_PROJECT,
'productplan' => TABLE_PRODUCTPLAN,
'productplanID' => TABLE_PRODUCTPLAN,
'plan' => TABLE_PRODUCTPLAN,
'planID' => TABLE_PRODUCTPLAN,
'execution' => TABLE_PROJECT,
'executionID' => TABLE_PROJECT,
'story' => TABLE_STORY,
'storyID' => TABLE_STORY,
'epic' => TABLE_STORY,
'epicID' => TABLE_STORY,
'requirement' => TABLE_STORY,
'requirementID' => TABLE_STORY,
'task' => TABLE_TASK,
'taskID' => TABLE_TASK,
'bug' => TABLE_BUG,
'bugID' => TABLE_BUG,
'feedback' => TABLE_FEEDBACK,
'feedbackID' => TABLE_FEEDBACK,
'build' => TABLE_BUILD,
'buildID' => TABLE_BUILD,
'case' => TABLE_CASE,
'caseID' => TABLE_CASE,
'testcase' => TABLE_CASE,
'testcaseID' => TABLE_CASE,
'user' => TABLE_USER,
'userID' => TABLE_USER,
'ticket' => TABLE_TICKET,
'ticketID' => TABLE_TICKET,
'dept' => TABLE_DEPT,
'deptID' => TABLE_DEPT,
];
/* Check assignedTo. */
if(isset($_POST['assignedTo']) && $_POST['assignedTo'])
{
$user = $this->dao->select('*')->from(TABLE_USER)
->where('account')->eq($_POST['assignedTo'])
->fetch();
if(!$user) return $this->control->sendError('User does not exist.');
}
$params = array_merge($this->params, $_POST);
foreach($params as $key => $value)
{
if(!isset($objectMap[$key]) || !$value) continue;
$table = $objectMap[$key];
$result = $this->checkObjectExists($table, $value);
if($result === false) return $this->control->sendError(ucfirst(str_replace('ID', '', $key)) . ' does not exist.');
foreach($result as $object)
{
if(!$this->checkObjectPriv($object, $table)) return $this->control->sendError(ucfirst(str_replace('ID', '', $key)) . ' is not allowed.');
}
}
}
/**
* 检查对象是否存在
*
* Check object exists.
*
* @param string $table
* @param int $objectID
* @access public
* @return array|false
*/
public function checkObjectExists($table, $objectIDList)
{
if(!is_array($objectIDList)) $objectIDList = [$objectIDList];
$objects = [];
foreach($objectIDList as $objectID)
{
$object = $this->dao->select('*')->from($table)
->where('id')->eq($objectID)
->beginIF(!in_array($table, [TABLE_DEPT]))->andWhere('deleted')->eq('0')->fi()
->fetch();
if(!$object) return false;
$objects[] = $object;
}
return $objects;
}
/**
* 执行对应模块
*
* 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', 'delete')))
{
$this->setFormData();
}
else
{
$this->checkAccess();
}
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");
$_POST = json_decode($requestBody, true);
/* Avoid empty post body. */
if(in_array($this->control->moduleName, ['feedback', 'ticket']))
{
$_POST['uid'] = '1';
}
else
{
$_POST['verifyPassword'] = '1';
}
/* 以POST的值为准。 Set GET value from POST data. */
foreach($_POST as $key => $value)
{
if(isset($this->params[$key])) $this->params[$key] = $value;
}
$this->checkAccess();
/* 其他方法不需要从GET页面获取post data。Other request directly. */
if(!in_array($this->methodName, ['create', 'edit', 'change'])) return;
/* 更新操作的表单需要拼接原始的值。 Merge original values. */
/* Get form data by get request. */
$postData = $_POST;
$_POST = array();
$this->control->viewType = 'html';
$this->control->getFormData = true;
$zen = $this->control->moduleName . 'Zen';
if(isset($this->control->$zen)) $this->control->$zen->getFormData = true;
$control = $this->control; // fetch method will change control.
$method = $this->control->methodName;
call_user_func_array(array($this->control, $method), $this->params);
/* Clean the output in get method. */
ob_clean();
$this->control->getFormData = false;
$this->control->viewType = 'json';
$this->control = $control;
$_POST = $postData;
foreach($this->control->formData as $key => $value)
{
if(!isset($_POST[$key])) $_POST[$key] = $value;
}
if(isset($this->control->$zen))
{
$this->control->$zen->getFormData = false;
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
{
/* Browse all items in api mode defaultly. */
$this->params[$key] = in_array($key, ['browseType', 'status']) ? 'all' : $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;
}
/**
* 设置超级变量。
* Set the super vars.
*
* @access public
* @return void
*/
public function setSuperVars()
{
$this->config->framework->filterCSRF = false;
parent::setSuperVars();
}
}