* @package common * @version $Id$ * @link https://www.zentao.net * @property router $app */ class commonModel extends model { /** * 网络请求客户端。 * HTTP client. * * @var object * @access public */ public static $httpClient; /** * 网络请求错误。 * Request errors. * * @var array * @access public */ public static $requestErrors = array(); /** * 缓存用户是否有某个模块、方法的访问权限。 * Cache the user's access rights to a module or method. * * @var array * @access public */ public static $userPrivs = array(); /** * 设置用户配置信息。 * Set config of user. * * @access public * @return void */ public function setUserConfig() { $this->sendHeader(); $this->setCompany(); $this->setUser(); $this->setApproval(); $this->loadConfigFromDB(); $this->loadCustomFromDB(); $this->initAuthorize(); if(!$this->checkIP()) return print($this->lang->ipLimited); } /** * 同步执行、项目、项目集的状态。 * Set the status of execution, project, and program to doing. * * @param int $objectID * @access public * @return void */ public function syncPPEStatus(int $objectID) { global $app; $rawModule = $app->rawModule; $rawMethod = strtolower($app->rawMethod); if($rawModule == 'marketresearch' and strpos($rawMethod, 'stage') !== false) $rawModule = 'execution'; if($rawModule == 'task' || $rawModule == 'effort' || $rawModule == 'researchtask') { $taskID = $objectID; $execution = $this->syncExecutionStatus($taskID); $project = $this->syncProjectStatus($execution); $this->syncProgramStatus($project); } if($rawModule == 'execution') { $executionID = $objectID; $execution = $this->dao->select('id, project, grade, parent, status, deleted')->from(TABLE_EXECUTION)->where('id')->eq($executionID)->fetch(); $this->syncExecutionByChild($execution); $project = $this->syncProjectStatus($execution); $this->syncProgramStatus($project); } if($rawModule == 'project') { $projectID = $objectID; $project = $this->dao->select('id, parent, path')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch(); $this->syncProgramStatus($project); } if($rawModule == 'program' and $this->config->systemMode == 'ALM') { $programID = $objectID; $program = $this->dao->select('id, parent, path')->from(TABLE_PROGRAM)->where('id')->eq($programID)->fetch(); $this->syncProgramStatus($program); } } /** * 项目开始时,设置项目所属的项目集的状态为进行中。 * Set the status of the program to which theproject is linked as Ongoing. * * @param object $project * @access public * @return void */ public function syncProgramStatus(object $project) { if($project->parent == 0) return; $parentPath = str_replace(",{$project->id},", '', $project->path); $parentPath = explode(',', trim($parentPath, ',')); $waitList = $this->dao->select('id')->from(TABLE_PROGRAM) ->where('id')->in($parentPath) ->andWhere('status')->eq('wait') ->orderBy('id_desc') ->fetchPairs(); $this->dao->update(TABLE_PROGRAM)->set('status')->eq('doing')->set('realBegan')->eq(helper::now())->where('id')->in($waitList)->exec(); $this->loadModel('action'); foreach($waitList as $programID) { $this->action->create('program', $programID, 'syncprogram'); } } /** * 执行开始时,设置执行所属的项目和项目所属的项目集的状态为进行中。 * Set the status of the project to which the execution is linked as Ongoing. * * @param object $execution * @access public * @return object $project */ public function syncProjectStatus(object $execution): object { $projectID = $execution->project; $project = $this->dao->select('*')->from(TABLE_PROJECT)->where('id')->eq($projectID)->fetch(); $today = helper::today(); if($project->status == 'wait') { $realBegan = helper::isZeroDate($project->realBegan) ? $this->dao->select('realBegan')->from(TABLE_PROJECT)->where('id')->eq($execution->id)->fetch('realBegan') : $project->realBegan; $this->dao->update(TABLE_PROJECT) ->set('status')->eq('doing') ->set('realBegan')->eq($realBegan) ->where('id')->eq($projectID) ->exec(); $this->loadModel('project')->recordFirstEnd($projectID); $actionType = $project->multiple ? 'syncproject' : 'syncmultipleproject'; $this->loadModel('action')->create('project', $projectID, $actionType); } return $project; } /** * 子阶段开始时,设置子阶段所属的父阶段和项目的状态为进行中。 * Set the status of the execution to which the sub execution is linked as Ongoing. * * @param object $execution * @access public * @return object|false $parentExecution */ public function syncExecutionByChild(object $execution): object|false { if($execution->grade == 1) return false; $today = helper::today(); $parentExecution = $this->dao->select('*')->from(TABLE_EXECUTION)->where('id')->eq($execution->parent)->fetch(); if($execution->deleted == '0' and $execution->status == 'doing' and in_array($parentExecution->status, array('wait', 'closed'))) { $this->dao->update(TABLE_EXECUTION) ->set('status')->eq('doing') ->beginIf(helper::isZeroDate($parentExecution->realBegan))->set('realBegan')->eq($today)->fi() ->where('id')->eq($execution->parent) ->exec(); $this->loadModel('action')->create('execution', $execution->parent, 'syncexecutionbychild'); } $project = $this->loadModel('project')->getByID($execution->project); if(strpos(',waterfall,waterfallplus,ipd,research,', ",$project->model,") !== false) $this->loadModel('programplan')->computeProgress($execution->id); return $parentExecution; } /** * 任务开始时,设置任务所属的执行、项目和项目所属的项目集的状态为进行中。 * Set the status of the execution to which the task is linked as Ongoing. * * @param int $taskID * @access public * @return object $execution */ public function syncExecutionStatus(int $taskID): object { $execution = $this->dao->select('t1.*')->from(TABLE_EXECUTION)->alias('t1') ->leftJoin(TABLE_TASK)->alias('t2')->on('t1.id=t2.execution') ->where('t2.id')->eq($taskID) ->fetch(); $today = helper::today(); if($execution->status == 'wait') { $this->dao->update(TABLE_EXECUTION)->set('status')->eq('doing')->set('realBegan')->eq($today)->where('id')->eq($execution->id)->exec(); $this->loadModel('project')->recordFirstEnd($execution->id); $this->loadModel('action')->create('execution', $execution->id, 'syncexecution'); if($execution->parent) { $execution = $this->dao->select('*')->from(TABLE_EXECUTION)->where('id')->eq($execution->id)->fetch(); // Get updated execution. $this->syncExecutionByChild($execution); } } return $execution; } /** * 设置HTTP标头。 * Set the header info. * * @access public * @return void */ public function sendHeader() { helper::header('Content-Type', "text/html; Language={$this->config->charset}"); helper::header('Cache-Control', 'private'); /* Send HTTP header. */ if($this->config->framework->sendXCTO) helper::header('X-Content-Type-Options', 'nosniff'); if($this->config->framework->sendXXP) helper::header('X-XSS-Protection', '1; mode=block'); if($this->config->framework->sendHSTS) helper::header('Strict-Transport-Security', 'max-age=3600; includeSubDomains'); if($this->config->framework->sendRP) helper::header('Referrer-Policy', 'no-referrer-when-downgrade'); if($this->config->framework->sendXPCDP) helper::header('X-Permitted-Cross-Domain-Policies', 'master-only'); if($this->config->framework->sendXDO) helper::header('X-Download-Options', 'noopen'); /* Set Content-Security-Policy header. */ if($this->config->CSPs) { foreach($this->config->CSPs as $CSP) helper::header('Content-Security-Policy', "$CSP;"); } if(!empty($this->config->xFrameOptions)) helper::header('X-Frame-Options', $this->config->xFrameOptions); if($this->loadModel('setting')->getItem('owner=system&module=sso&key=turnon')) { if(isset($_SERVER["HTTPS"]) and $_SERVER["HTTPS"] == 'on') { $session = $this->config->sessionVar . '=' . session_id(); helper::header('Set-Cookie', "$session; SameSite=None; Secure=true", false); } } } /** * 设置公司信息。 * Set the company. * * First, search company by the http host. If not found, search by the default domain. Last, use the first as the default. * After get the company, save it to session. * @access public * @return void */ public function setCompany() { if($this->session->company) { $this->app->company = $this->session->company; } else { $httpHost = $this->server->http_host; $company = $this->loadModel('company')->getFirst(); if(!$company) $this->app->triggerError(sprintf($this->lang->error->companyNotFound, $httpHost), __FILE__, __LINE__, true); $this->session->set('company', $company); $this->app->company = $company; } } /** * 设置用户信息。 * Set the user info. * * @access public * @return void */ public function setUser() { if($this->session->user) { $this->app->user = $this->session->user; } elseif($this->app->company->guest || (PHP_SAPI == 'cli' && (!isset($_SERVER['RR_MODE']) || $_SERVER['RR_MODE'] == 'jobs'))) { $user = new stdClass(); $user->id = 0; $user->account = 'guest'; $user->realname = 'guest'; $user->dept = 0; $user->avatar = ''; $user->role = 'guest'; $user->admin = false; $user->groups = array('group'); $user->visions = $this->config->vision; $this->session->set('user', $user); $this->app->user = $this->session->user; } } /** * 初始化用户权限和视图。 * Init user authorize. * * @access private * @return void */ private function initAuthorize() { if(isset($this->app->user)) { $user = $this->app->user; if(!$this->app->upgrading) $user->rights = $this->loadModel('user')->authorize($user->account); if(!$this->app->upgrading) $user->view = $this->user->grantUserView($user->account, $user->rights['acls']); $this->session->set('user', $user); $this->app->user = $this->session->user; } } /** * 设置审批配置。 * Set approval config. * * @access public * @return void */ public function setApproval() { $this->config->openedApproval = in_array($this->config->edition, array('biz', 'max', 'ipd')) && $this->config->vision != 'lite'; } /** * 获取表单的配置项。 * Obtain the config for the form. * * @param string $module * @param string $method * @param int $objectID * @static * @access public * @return array */ public static function formConfig(string $module, string $method, int $objectID = 0): array { global $config, $app; if($config->edition == 'open') return array(); $formConfig = array(); $common = new self(); $required = $common->dao->select('*')->from(TABLE_WORKFLOWRULE)->where('type')->eq('system')->andWhere('rule')->eq('notempty')->fetch(); $fields = $common->loadModel('flow')->getExtendFields($module, $method, $objectID); $type = 'string'; foreach($fields as $fieldObject) { if(strpos($fieldObject->type, 'int') !== false) $type = 'int'; if(strpos($fieldObject->type, 'date') !== false) $type = 'date'; if(in_array($fieldObject->type, array('float', 'decimal'))) $type = 'float'; $formConfig[$fieldObject->field] = array('type' => $type, 'default' => $fieldObject->default, 'control' => $fieldObject->control, 'rules' => $fieldObject->rules); $formConfig[$fieldObject->field]['required'] = strpos(",{$fieldObject->rules},", ",{$required->id},") !== false; if(in_array($fieldObject->control, array('multi-select', 'checkbox'))) $formConfig[$fieldObject->field]['filter'] = 'join'; } return $formConfig; } /** * 从数据库加载配置信息。 * Load configs from database and save it to config->system and config->personal. * * @access public * @return void */ public function loadConfigFromDB() { /* Get configs of system and current user. */ $account = isset($this->app->user->account) ? $this->app->user->account : ''; if($this->config->db->name) $config = $this->loadModel('setting')->getSysAndPersonalConfig($account); $this->config->systemDB = isset($config['system']) ? $config['system'] : array(); $this->config->personalDB = isset($config[$account]) ? $config[$account] : array(); /* Web root cannot be changed by api request. */ if(!$this->app->apiVersion) { $this->commonTao->updateDBWebRoot($this->config->systemDB); } /* Override the items defined in config/config.php and config/my.php. */ if(isset($this->config->systemDB->common)) $this->app->mergeConfig($this->config->systemDB->common, 'common'); if(isset($this->config->personalDB->common)) $this->app->mergeConfig($this->config->personalDB->common, 'common'); $this->config->disabledFeatures = $this->config->disabledFeatures . ',' . $this->config->closedFeatures; /* Sanplex hide features. */ $this->app->loadConfig('common'); if(!empty($this->config->hiddenFeature)) $this->config->disabledFeatures .= ',' . implode(',', $this->config->hiddenFeature); } /** * 从数据库加载自定义信息。 * Load custom lang from db. * * @access public * @return void */ public function loadCustomFromDB() { $this->loadModel('custom'); if($this->app->upgrading) return; if(!$this->config->db->name) return; $records = $this->custom->getAllLang(); if(!$records) return; $this->lang->db = new stdclass(); $this->lang->db->custom = $records; } /** * 判断哪些方法不需要鉴权。 * Judge a method of one module is open or not. * * @param string $module * @param string $method * @access public * @return bool */ public function isOpenMethod(string $module, string $method): bool { if(in_array("$module.$method", $this->config->openMethods)) return true; if($this->loadModel('user')->isLogon() or ($this->app->company->guest and $this->app->user->account == 'guest')) { if(in_array("$module.$method", $this->config->logonMethods)) return true; if(stripos($method, 'ajax') !== false) return true; if($module == 'block' && stripos(',dashboard,printblock,create,edit,delete,close,reset,layout,', ",{$method},") !== false) return true; if($module == 'index' and $method == 'app') return true; if($module == 'my' and $method == 'guidechangetheme') return true; if($module == 'product' and $method == 'showerrornone') return true; if($module == 'misc' and in_array($method, array('downloadclient', 'changelog'))) return true; if($module == 'tutorial' and in_array($method, array('start', 'index', 'quit', 'wizard'))) return true; } return false; } /** * 拒绝访问的页面。 * Deny access. * * @param string $module * @param string $method * @param bool $reload * @access public * @return mixed */ public function deny(string $module, string $method, bool $reload = true) { if($reload && $this->loadModel('user')->isLogon()) { /* Get authorize again. */ $user = $this->app->user; $user->rights = $this->user->authorize($user->account); $user->groups = $this->user->getGroups($user->account); $user->admin = strpos($this->app->company->admins, ",{$user->account},") !== false; $this->session->set('user', $user); $this->app->user = $this->session->user; if(commonModel::hasPriv($module, $method)) return true; } if($this->app->apiVersion) { header('HTTP/1.1 403 Forbidden', true, 403); header('Content-Type: application/json'); helper::end(json_encode(['status' => 'error', 'message' => 'Not allowed'])); } $vars = "module=$module&method=$method"; if(isset($this->server->http_referer)) { $referer = helper::safe64Encode($this->server->http_referer); $vars .= "&referer=$referer"; } $denyLink = helper::createLink('user', 'deny', $vars); if(helper::isAjaxRequest()) { $isModal = helper::isAjaxRequest('modal'); if($isModal) header("Location: $denyLink"); if(!$isModal) echo json_encode(array('load' => $denyLink)); } else { header("Location: $denyLink"); } helper::end(); } /** * 格式化日期,将日期格式化为YYYY-mm-dd,将日期时间格式化为YYYY-mm-dd HH:ii:ss。 * Format the date to YYYY-mm-dd, format the datetime to YYYY-mm-dd HH:ii:ss. * * @param string $date * @param string $type date|datetime|'' * @access public * @return string */ public function formatDate(string $date, string $type = '') { if(helper::isZeroDate($date)) { if($type == 'date') return '0000-00-00'; if($type == 'datetime') return '0000-00-00 00:00:00'; } $datePattern = '\w{4}(\/|-)\w{1,2}(\/|-)\w{1,2}'; $datetimePattern = $datePattern . ' \w{1,2}\:\w{1,2}\:\w{1,2}'; if(empty($type)) { if(!preg_match("/$datePattern/", $date) and !preg_match("/$datetimePattern/", $date)) return $date; if(preg_match("/$datePattern/", $date) === 1) $type = 'date'; if(preg_match("/$datetimePattern/", $date) === 1) $type = 'datetime'; } if($type == 'date') $format = 'Y-m-d'; if($type == 'datetime') $format = 'Y-m-d H:i:s'; return date($format, strtotime($date)); } /** * 检查字符串是否以给定的子字符串结尾。 * Checks if a string ends with a given substring. * * @param string $haystack * @param string $needle * @return bool */ public static function strEndsWith(string $haystack, string $needle): bool { $length = strlen($needle); if ($length === 0) return true; $position = strpos($haystack, $needle); return $position !== false && $position === strlen($haystack) - $length; } /** * 创建菜单项链接。 * Create menu item link * * @param object $menuItem * * @static * @access public * @return string */ public static function createMenuLink(object $menuItem): string { $link = $menuItem->link; if(is_array($menuItem->link)) { $vars = isset($menuItem->link['vars']) ? $menuItem->link['vars'] : ''; if(isset($menuItem->tutorial) and $menuItem->tutorial) { if(!empty($vars)) $vars = helper::safe64Encode($vars); $link = helper::createLink('tutorial', 'wizard', "module={$menuItem->link['module']}&method={$menuItem->link['method']}¶ms=$vars"); } else { $link = helper::createLink($menuItem->link['module'], $menuItem->link['method'], $vars); } } return $link; } /** * 获取左侧一级导航。 * Get main nav items list * * @param string $moduleName * @param bool $useDefault 是否使用语言项中的默认值 * * @static * @access public * @return array */ public static function getMainNavList(string $moduleName, bool $useDefault = false): array { global $lang, $app, $config; $app->loadLang('my'); if($config->edition != 'open' && $app->isServing()) $app->control->loadModel('common')->mergePrimaryFlows(); /* Ensure user has latest rights set. */ $app->user->rights = $app->control->loadModel('user')->authorize($app->user->account); $menuOrder = $lang->mainNav->menuOrder; $hasCustomMenu = false; if(isset($config->customMenu->nav) && !$useDefault && !commonModel::isTutorialMode()) { $customMenuOrder = array(); $items = json_decode($config->customMenu->nav); $hiddenItems = array(); foreach($items as $item) { if(!empty($item->hidden)) { $hiddenItems[] = $item->name; continue; } $customMenuOrder[$item->order] = $item->name; } $customMenuItems = array_values($customMenuOrder); foreach($menuOrder as $order => $name) { if(in_array($name, $customMenuItems) || in_array($name, $hiddenItems)) continue; while(isset($customMenuOrder[$order])) $order ++; $customMenuOrder[$order] = $name; } $menuOrder = $customMenuOrder; $hasCustomMenu = true; } ksort($menuOrder); $items = array(); $lastItem = end($menuOrder); $printDivider = false; $prev = ''; foreach($menuOrder as $key => $group) { // 如果有自定义菜单,则直接用自定义后的divider分隔符 if($hasCustomMenu && $group == 'divider' && $prev != 'divider') { $items[] = 'divider'; $prev = 'divider'; continue; } $prev = $group; if($group != 'my' && !$app->user->admin && !empty($app->user->rights['acls']['views']) && !isset($app->user->rights['acls']['views'][$group])) continue; // 后台权限分组中没有给导航视图 if(!isset($lang->mainNav->$group)) continue; $nav = $lang->mainNav->$group; list($title, $currentModule, $currentMethod, $vars) = explode('|', $nav); // 没有自定义过菜单,用默认语言项中的divider分隔符 if(!$hasCustomMenu) { /* When last divider is not used in mainNav, use it next menu. */ $printDivider = ($printDivider or ($lastItem != $key) and strpos($lang->dividerMenu, ",{$group},") !== false) ? true : false; if($printDivider and !empty($items)) { $items[] = 'divider'; $printDivider = false; } } $display = false; /* 1. 有权限则展示导航. */ if(common::hasPriv($currentModule, $currentMethod)) $display = true; /* 2. 如果没有资产库落地页的权限,则查看是否有资产库其他方法的权限. */ if($currentModule == 'assetlib' && !$display) list($display, $currentMethod) = commonTao::setAssetLibMenu($display, $currentModule, $currentMethod); /* 3. 可以个性化设置的导航,如果没有落地页的权限,则查看是否有其他落地页的权限。 */ $moduleLinkList = $currentModule . 'LinkList'; if(!$display and isset($lang->my->$moduleLinkList) and $config->vision != 'or') list($display, $currentMethod) = commonTao::setPreferenceMenu($display, $currentModule, $currentMethod); /* 4. 不可以个性化设置的导航,如果没有落地页的权限,则查看是否有对应app下其他方法的权限. */ if(!$display and isset($lang->$currentModule->menu) and !in_array($currentModule, array('program', 'product', 'project', 'execution', 'demandpool'))) list($display, $currentMethod) = commonTao::setOtherMenu($display, $currentModule, $currentMethod); /* 5. 如果以上权限都没有,则最后查看是否有该应用下任意一个顶部一级导航的权限。 */ if(!$display and isset($lang->$group->menu)) list($display, $currentModule, $currentMethod) = commonTao::setMenuByGroup($group, $display, $currentModule, $currentMethod); /* Check whether the homeMenu of this group have permissions. If yes, point to them. */ if($display == false and isset($lang->$group->homeMenu)) { foreach($lang->$group->homeMenu as $menu) { if(!isset($menu['link'])) continue; $linkPart = explode('|', $menu['link']); if(count($linkPart) < 3) continue; list($label, $module, $method) = $linkPart; if(common::hasPriv($module, $method)) { $display = true; $currentModule = $module; $currentMethod = $method; if(!isset($menu['target'])) break; } } } if(!$display) continue; /* Assign vars. */ $item = new stdClass(); $item->group = $group; $item->code = $group; $item->active = zget($lang->navGroup, $moduleName, '') == $group or $moduleName != 'program' and $moduleName == $group; $item->title = $title; $item->moduleName = $currentModule; $item->methodName = $currentMethod; $item->vars = $vars; $isTutorialMode = commonModel::isTutorialMode(); if($isTutorialMode and $currentModule == 'project') { if(!empty($vars)) $vars = helper::safe64Encode($vars); $item->url = helper::createLink('tutorial', 'wizard', "module={$currentModule}&method={$currentMethod}¶ms={$vars}", '', false, 0, 1); } else { $item->url = helper::createLink($currentModule, $currentMethod, $vars, '', false, 0, 1); } $items[] = $item; } // 如果最后一个是分割线,则删除 while(!empty($items) && end($items) === 'divider') { array_pop($items); } return $items; } /** * 获取高亮的顶部一级导航。 * Get active main menu. * * @static * @access public * @return string */ public static function getActiveMainMenu(): string { global $app; $isTutorialMode = commonModel::isTutorialMode(); $currentModule = $app->rawModule; $currentMethod = $app->rawMethod; if($isTutorialMode && defined('WIZARD_MODULE')) $currentModule = WIZARD_MODULE; if($isTutorialMode && defined('WIZARD_METHOD')) $currentMethod = WIZARD_METHOD; /* Print all main menus. */ $menu = customModel::getMainMenu(); $activeMenu = ''; foreach($menu as $menuItem) { if(isset($menuItem->hidden) and $menuItem->hidden and (!isset($menuItem->tutorial) or !$menuItem->tutorial)) continue; if(empty($menuItem->link)) continue; /* Init the these vars. */ $alias = isset($menuItem->alias) ? $menuItem->alias : ''; $subModule = isset($menuItem->subModule) ? explode(',', $menuItem->subModule) : array(); $exclude = isset($menuItem->exclude) ? $menuItem->exclude : ''; if($menuItem->name == $currentModule and strpos(",$exclude,", ",$currentModule-$currentMethod,") === false) $activeMenu = $menuItem->name; if($subModule and in_array($currentModule, $subModule) and strpos(",$exclude,", ",$currentModule-$currentMethod,") === false) $activeMenu = $menuItem->name; if($menuItem->link) { $module = ''; $method = ''; if(is_array($menuItem->link)) { if(isset($menuItem->link['module'])) $module = $menuItem->link['module']; if(isset($menuItem->link['method'])) $method = $menuItem->link['method']; } if($module == $currentModule and ($method == $currentMethod or strpos(",$alias,", ",$currentMethod,") !== false) and strpos(",$exclude,", ",$currentMethod,") === false) $activeMenu = $menuItem->name; /* Print drop menus. */ if(isset($menuItem->dropMenu)) { foreach($menuItem->dropMenu as $dropMenuName => $dropMenuItem) { if(empty($dropMenuItem)) continue; if(isset($dropMenuItem->hidden) and $dropMenuItem->hidden) continue; /* Parse drop menu link. */ if(!empty($dropMenuItem['links'])) { $dropMenuLink = common::getHasPrivLink($dropMenuItem); if(empty($dropMenuLink)) continue; list($subLabel, $subModule, $subMethod, $subParams) = $dropMenuLink; } else { $dropMenuLink = zget($dropMenuItem, 'link', $dropMenuItem); list($subLabel, $subModule, $subMethod, $subParams) = explode('|', $dropMenuLink); if(!common::hasPriv($subModule, $subMethod)) continue; } $activeMainMenu = false; if($currentModule == strtolower($subModule) and $currentMethod == strtolower($subMethod)) { $activeMainMenu = true; } else { $subModule = isset($dropMenuItem['subModule']) ? explode(',', $dropMenuItem['subModule']) : array(); $subExclude = isset($dropMenuItem['exclude']) ? $dropMenuItem['exclude'] : $exclude; $subAlias = zget($dropMenuItem, 'alias', ''); if($subModule and in_array($currentModule, $subModule) and strpos(",$subExclude,", ",$currentModule-$currentMethod,") === false) $activeMainMenu = true; if(strpos(",$subAlias,", ",$currentModule-$currentMethod,") !== false) $activeMainMenu = true; } if($activeMainMenu) $activeMenu = $dropMenuName; } } } } return $activeMenu; } /** * 获取有权限的链接。 * Get the authorized link. * * @param array $menu * @access public * @return array */ public static function getHasPrivLink(array $menu): array { if(empty($menu['link'])) return array(); list($label, $module, $method, $params) = explode('|', $menu['link']); if(common::hasPriv($module, $method)) return array($label, $module, $method, $params); if(empty($menu['links'])) return array(); $link = array(); foreach($menu['links'] as $menuLink) { list($module, $method, $params) = explode('|', $menuLink); if(common::hasPriv($module, $method)) { $link = array($label, $module, $method, $params); break; } } return $link; } /** * 当对象编辑后,比较前后差异。 * Create changes of one object. * * @param mixed $old the old object * @param mixed $new the new object * @param string $moduleName * @static * @access public * @return array */ public static function createChanges(mixed $old, mixed $new, string $moduleName = ''): array { global $app, $config, $dao; /** * 当主状态改变并且未设置子状态的值时把子状态的值设置为默认值并记录日志。 * Change sub status when status is changed and sub status is not set, and record the changes. */ if($config->edition != 'open') { $oldID = zget($old, 'id', ''); $oldStatus = zget($old, 'status', ''); $newStatus = zget($new, 'status', ''); $newSubStatus = zget($new, 'subStatus', ''); if(empty($moduleName)) $moduleName = $app->getModuleName(); if($oldID and $oldStatus and $newStatus and !$newSubStatus and $oldStatus != $newStatus) { $field = $dao->select('options')->from(TABLE_WORKFLOWFIELD)->where('`module`')->eq($moduleName)->andWhere('`field`')->eq('subStatus')->fetch(); if(!empty($field->options)) $field->options = json_decode($field->options, true); if(!empty($field->options[$newStatus]['default'])) { $flow = $dao->select('`table`')->from(TABLE_WORKFLOW)->where('`module`')->eq($moduleName)->fetch(); $default = $field->options[$newStatus]['default']; $common = new self(); $common->dao->update($flow->table)->set('subStatus')->eq($default)->where('id')->eq($oldID)->exec(); $new->subStatus = $default; } } $dateFields = array(); $rows = $dao->select('`field`')->from(TABLE_WORKFLOWFIELD)->where('`module`')->eq($moduleName)->andWhere('`control`')->in(array('data', 'datetime'))->fetchAll(); foreach($rows as $row) $dateFields[$row->field] = $row->field; } $changes = array(); foreach($new as $key => $value) { if($key == 'addedFiles') { foreach($value as $addedFile) $changes[] = array('field' => 'addDiff', 'old' => '', 'new' => '', 'diff' => $addedFile); } elseif($key == 'deleteFiles') { foreach($value as $deleteFile) $changes[] = array('field' => 'removeDiff', 'old' => '', 'new' => '', 'diff' => $deleteFile); } elseif($key == 'renameFiles') { foreach($value as $renameFile) $changes[] = array('field' => 'fileName', 'old' => $renameFile['old'], 'new' => $renameFile['new'], 'diff' => ''); } if(is_object($value) || is_array($value)) continue; $check = strtolower($key); if(in_array($check, array('lastediteddate', 'lasteditedby', 'assigneddate', 'editedby', 'editeddate', 'editingdate', 'uid', 'prevstatus', 'prevassignedto'))) continue; if(in_array($check, array('finisheddate', 'canceleddate', 'hangupeddate', 'lastcheckeddate', 'activateddate', 'closeddate', 'actualcloseddate')) && $value == '') continue; if(isset($old->$key) && !is_object($old->$key) && !is_array($old->$key)) { if($config->edition != 'open' && isset($dateFields[$key])) $old->$key = formatTime($old->$key); if($value != stripslashes((string)$old->$key)) { $diff = ''; $showDiff = (substr_count((string)$value, "\n") > 1 || substr_count((string)$old->$key, "\n") > 1 || strpos(',name,title,desc,spec,steps,content,digest,verify,report,definition,analysis,summary,prevention,resolution,outline,schedule,minutes,sql,interface,ui,langs,performance,privileges,search,actions,deploy,bi,safe,other,', ',' . strtolower($key) . ',') !== false); $hiddenDiff = array('bug' => 'resolution'); if($showDiff && (!isset($hiddenDiff[$moduleName]) || (isset($hiddenDiff[$moduleName]) && strpos(",$hiddenDiff[$moduleName],", ",$key,") === false))) { $diff = commonModel::diff((string)$old->$key, (string)$value); } $changes[] = array('field' => $key, 'old' => $old->$key, 'new' => $value, 'diff' => $diff); } } } return $changes; } /** * 比较两个字符串的差异。 * Diff two string. (see phpt) * * @param string $text1 * @param string $text2 * @static * @access public * @return string */ public static function diff(string $text1, string $text2): string { $text1 = str_replace(' ', '', trim($text1)); $text2 = str_replace(' ', '', trim($text2)); $w = explode("\n", $text1); $o = explode("\n", $text2); $w1 = array_diff_assoc($w,$o); $o1 = array_diff_assoc($o,$w); $w2 = array(); $o2 = array(); foreach($w1 as $idx => $val) $w2[sprintf("%03d<",$idx)] = sprintf("%03d- ", $idx+1) . "" . trim($val) . ""; foreach($o1 as $idx => $val) $o2[sprintf("%03d>",$idx)] = sprintf("%03d+ ", $idx+1) . "" . trim($val) . ""; $diff = array_merge($w2, $o2); ksort($diff); return implode("\n", $diff); } /** * 判断post数据大小是否超过Suhosin设置大小。 * Judge Suhosin Setting whether the actual size of post data is large than the setting size. * * @param int $countInputVars * @static * @access public * @return bool */ public static function judgeSuhosinSetting(int $countInputVars): bool { if(extension_loaded('suhosin')) { $maxPostVars = ini_get('suhosin.post.max_vars'); $maxRequestVars = ini_get('suhosin.request.max_vars'); if($countInputVars > $maxPostVars or $countInputVars > $maxRequestVars) return true; } else { $maxInputVars = ini_get('max_input_vars'); if($maxInputVars and $countInputVars > (int)$maxInputVars) return true; } return false; } /** * 获取详情页面上一页和下一页的对象。 * Get the previous and next object. * * @param string $type story|task|bug|case * @param int $objectID * @access public * @return object */ public function getPreAndNextObject(string $type, int $objectID): object { $queryCondition = $type . 'QueryCondition'; $queryCondition = $this->session->$queryCondition; $preAndNextObject = new stdClass(); $preAndNextObject->pre = ''; $preAndNextObject->next = ''; if(empty($queryCondition)) return $preAndNextObject; $sql = $this->commonTao->getPreAndNextSQL($type); $objectList = $this->commonTao->queryListForPreAndNext($type, $sql); $preAndNextObject = $this->commonTao->searchPreAndNextFromList($objectID, $objectList); $preAndNextObject = $this->commonTao->fetchPreAndNextObject($type, $objectID, $preAndNextObject); return $preAndNextObject; } /** * 保存列表页的查询条件到session中,用于其他页面返回、导出等操作。 * Save one executed query. * * @param string $sql * @param string $objectType story|task|bug|testcase * @param bool $onlyCondition * @access public * @return void */ public function saveQueryCondition(string $sql, string $objectType, bool $onlyCondition = true) { /* Set the query condition session. */ if($onlyCondition) { $queryCondition = substr($sql, strpos($sql, ' WHERE ') + 7); if($queryCondition) { if(strpos($queryCondition, ' ORDER BY') !== false) $queryCondition = substr($queryCondition, 0, strpos($queryCondition, ' ORDER BY ')); $queryCondition = str_replace('t1.', '', $queryCondition); } } else { $queryCondition = strpos($sql, ' ORDER BY') !== false ? substr($sql, 0, strpos($sql, ' ORDER BY ')) : $sql; } $queryCondition = trim($queryCondition); if(empty($queryCondition)) $queryCondition = "1=1"; $this->session->set($objectType . 'QueryCondition', $queryCondition, $this->app->tab); $this->session->set($objectType . 'OnlyCondition', $onlyCondition, $this->app->tab); /* Set the query condition session. */ $orderBy = explode(' ORDER BY ', $sql); $orderBy = isset($orderBy[1]) ? $orderBy[1] : ''; if($orderBy) { $orderBy = explode(' LIMIT ', $orderBy); $orderBy = $orderBy[0]; if($onlyCondition) $orderBy = str_replace('t1.', '', $orderBy); } $this->session->set($objectType . 'OrderBy', $orderBy, $this->app->tab); $this->session->set($objectType . 'BrowseList', array(), $this->app->tab); } /** * 追加排序字段。 * Append order by. * * @param string $orderBy * @param string $append * @access public * @return string */ public static function appendOrder(string $orderBy, string $append = 'id'): string { if(empty($orderBy)) return $append; list($firstOrder) = explode(',', $orderBy); $sort = strpos($firstOrder, '_') === false ? '_asc' : strstr($firstOrder, '_'); return strpos($orderBy, $append) === false ? $orderBy . ',' . $append . $sort : $orderBy; } /** * 检查字段是否存在。 * Check field exists * * @param string $table * @param string $field * @access public * @return bool */ public function checkField(string $table, string $field): bool { $fields = $this->dao->descTable($table); $hasField = false; foreach($fields as $fieldObj) { if($field == $fieldObj->field) { $hasField = true; break; } } return $hasField; } /** * 检查验证文件是否正确创建。 * Check safe file. * * @access public * @return string|false */ public function checkSafeFile() { if($this->config->inContainer) return false; if($this->app->hasValidSafeFile()) return false; if($this->app->getModuleName() == 'upgrade' and $this->session->upgrading) return false; if($this->app->getModuleName() == 'upgrade' && $this->app->getMethodName() == 'safedelete') return false; $statusFile = $this->app->getAppRoot() . 'www' . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'ok.txt'; return (!is_file($statusFile) or (time() - filemtime($statusFile)) > $this->config->safeFileTimeout) ? $statusFile : false; } /** * 检查系统是否处于维护状态,如果处于维护状态则给非管理员用户输出提示。 * Check whether system in maintenance status and show message for non-admin user. * * @return void */ public function checkMaintenance() { $maintenance = $this->loadModel('setting')->getItem('owner=system&module=system&key=maintenance'); if(empty($maintenance)) return true; $maintenance = json_decode($maintenance); if($this->app->moduleName == 'user' && $this->app->methodName == 'login') return true; if(!empty($this->app->user->admin)) return true; if(isset($maintenance->action) && in_array($maintenance->action, array('upgrade', 'downgrade', 'restore'))) helper::setStatus(503); $reason = sprintf($this->lang->maintainReason, isset($maintenance->reason) ? $maintenance->reason : $this->lang->unknown); $message = << {$this->lang->inMaintenance} {$reason}{$this->lang->systemMaintainer}
{$reason}{$this->lang->systemMaintainer}