| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840 |
- <?php /**
- * 禅道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
- * @param string $appName
- * @param string $appRoot
- */
- public function __construct($appName = 'api', $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($routes)
- {
- foreach($routes as $route => $target)
- {
- $patternAsRegex = preg_replace_callback(
- '#:([\w]+)\+?#',
- \Closure::fromCallable([$this, 'matchesCallback']),
- str_replace(')', ')?', $route)
- );
- if(substr_compare($route, '/', -strlen('/')) === 0) $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]+)\+?#',
- \Closure::fromCallable([$this, 'matchesCallback']),
- str_replace(')', ')?', $route)
- );
- if(substr_compare($route, '/', -strlen('/')) === 0) $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($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, $table)
- {
- 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($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($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();
- }
- }
|