* @package convert * @version $Id: model.php 4129 2013-01-18 01:58:14Z wwccss $ * @link https://www.zentao.net */ ?> config->db; $params->name = $dbName; $dbh = new dbh($params); $dbh->exec("SET NAMES {$params->encoding}"); $dbh->setAttribute(PDO::ATTR_CASE , PDO::CASE_LOWER); $dbh->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); $dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->sourceDBH = $dbh; return $dbh; } catch (PDOException $exception) { return $exception->getMessage(); } } /** * 检查数据库是否存在。 * Check database exits or not. * * @param string $dbName * @access public * @return object|false */ public function dbExists(string $dbName = ''): object|false { if(!$this->checkDBName($dbName)) return false; return $this->dbh->dbExists($dbName); } /** * 检查数据表是否存在。 * Check table exits or not. * * @param string $table * @access public * @return object|false */ public function tableExists(string $table): object|false { $sql = "SHOW tables like '$table'"; return $this->dbh->query($sql)->fetch(); } /** * 检查jira数据库表是否存在。 * Check table of jira databases exits or not. * * @param string $dbName * @param string $table * @access public * @return object|false */ public function tableExistsOfJira(string $dbName, string $table): object|false { $this->connectDB($dbName); $sql = "SHOW tables like '$table'"; return $this->dao->dbh($this->sourceDBH)->query($sql)->fetch(); } /** * 保存每个表的最大ID。 * Save the max id of every table. Thus when we convert again, when can delete id larger then the saved max id. * * @access public * @return void */ public function saveState(): void { /* Get user defined tables. */ $constants = get_defined_constants(true); $userConstants = $constants['user']; /* These tables needn't save. */ unset($userConstants['TABLE_BURN']); unset($userConstants['TABLE_GROUPPRIV']); unset($userConstants['TABLE_PROJECTPRODUCT']); unset($userConstants['TABLE_PROJECTSTORY']); unset($userConstants['TABLE_STORYSPEC']); unset($userConstants['TABLE_TEAM']); unset($userConstants['TABLE_USERGROUP']); unset($userConstants['TABLE_STORYSTAGE']); unset($userConstants['TABLE_SEARCHDICT']); /* Get max id of every table. */ foreach($userConstants as $key => $value) { if(strpos($key, 'TABLE') === false) continue; if($key == 'TABLE_COMPANY') continue; $state[$value] = (int)$this->dao->select('MAX(id) AS id')->from($value)->fetch('id'); } $this->session->set('state', $state); } /** * 获取jira的数据。 * Get jira data. * * @param int $method * @param string $module * @param int $lastID * @param int $limit * @access public * @return array */ public function getJiraData(string $method, string $module, int $lastID = 0, int $limit = 0): array { if($method == 'db') { $originDBH = $this->dbh; $this->connectDB($this->session->jiraDB); $result = $this->getJiraDataFromDB($module, $lastID, $limit); $this->dao->dbh($originDBH); return $result; } elseif($method == 'file') { return $this->getJiraDataFromFile($module, $lastID, $limit); } else { return $this->getJiraDataFromAPI($module, $lastID, $limit); } } /** * 从接口获取jira数据。 * Get jira data from jiraAPI. * * @param string $module * @param int $lastID * @param int $limit * @access public * @return array */ public function getJiraDataFromAPI(string $module = '', int $lastID = 0, int $limit = 0): array { if(empty($_SESSION['jiraApi'])) return array(); $jiraApi = json_decode($this->session->jiraApi, true); if(empty($jiraApi['domain'])) return array(); $this->app->loadClass('jira', true); $jiraApi = new jira($jiraApi['domain'], $jiraApi['admin'], $jiraApi['token']); $functionMap = array( 'issue' => 'getIssues', 'issuelinktype' => 'getIssueLinkTypes', 'issuetype' => 'getIssueTypes', 'user' => 'getUsers', 'project' => 'getProjects', 'resolution' => 'getResolutions', 'priority' => 'getPriority', 'status' => 'getStatus', 'customfield' => 'getCustomFields', 'customfieldoption' => 'getCustomFieldOption', 'workflow' => 'getWorkflowActions' ); if(isset($functionMap[$module])) { $function = $functionMap[$module]; $dataList = $jiraApi->$function($lastID, $limit); } else { return array(); } if(in_array($module, array_keys($this->config->convert->objectTables)) && $module != 'workflow') { if($module == 'customfield') { foreach($dataList as $issueType => $datas) { foreach($datas as $key => $data) { $buildfunction = 'build' . ucfirst($module) . 'data'; $dataList[$issueType][$key] = $this->$buildfunction($data); } } } else { foreach($dataList as $key => $data) { $buildfunction = 'build' . ucfirst($module) . 'data'; $dataList[$key] = $this->$buildfunction($data); } } } return $dataList; } /** * 从数据库获取jira数据。 * Get jira data from db. * * @param string $module * @param int $lastID * @param int $limit * @access public * @return array */ public function getJiraDataFromDB(string $module = '', int $lastID = 0, int $limit = 0): array { $dataList = array(); $table = zget($this->config->convert->objectTables, $module, ''); if($module == 'user') { $dataList = $this->dao->dbh($this->sourceDBH)->select('t1.`ID`, t1.`lower_user_name` as account, t1.`lower_display_name` as realname, t1.`lower_email_address` as email, t1.created_date as `join`, t2.user_key as userCode')->from(JIRA_USERINFO)->alias('t1') ->leftJoin(JIRA_USER)->alias('t2')->on('t1.`lower_user_name` = t2.`lower_user_name`') ->where('1 = 1') ->beginIF($lastID)->andWhere('t1.ID')->gt($lastID)->fi() ->orderBy('t1.ID asc')->limit($limit) ->fetchAll('id'); } elseif($module == 'nodeassociation') { $dataList = $this->dao->dbh($this->sourceDBH)->select('*')->from($table)->limit($lastID, $limit)->fetchAll(); } elseif($module == 'fixversion' || $module == 'affectsversion') { $dataList = array(); } elseif(!empty($table)) { $dataList = $this->dao->dbh($this->sourceDBH)->select('*')->from($table) ->where('1 = 1') ->beginIF($lastID)->andWhere('ID')->gt($lastID)->fi() ->orderBy('ID asc')->limit($limit) ->fetchAll('id', false); } return $dataList; } /** * 从文件中获取jira数据。 * Get jira data from file. * * @param string $module * @param int $lastID * @param int $limit * @access public * @return array */ public function getJiraDataFromFile(string $module, int $lastID = 0, int $limit = 0): array { $fileName = $module; if($module == 'build') $fileName = 'version'; if($module == 'file') $fileName = 'fileattachment'; $filePath = $this->app->getTmpRoot() . 'jirafile/' . $fileName . '.xml'; if(!file_exists($filePath)) return array(); $xmlContent = file_get_contents($filePath); $xmlContent = preg_replace ('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u', ' ', $xmlContent); $reader = new XMLReader(); $dom = new DOMDocument(); $reader->XML($xmlContent); $reader->read(); $domNode = $reader->expand($dom); $reader->close(); $parsedXML = simplexml_import_dom($domNode); $parsedXML = $this->object2Array($parsedXML); $dataList = array(); foreach($parsedXML as $key => $xmlArray) { if(strtolower($key) != strtolower($fileName)) continue; foreach($xmlArray as $key => $attributes) { if(is_array($attributes) && isset($attributes['status']) && $attributes['status'] == 'deleted') break; if(is_numeric($key)) { $desc = isset($attributes['description']) ? $attributes['description'] : ''; $summary = isset($attributes['summary']) ? $attributes['summary'] : ''; $body = isset($attributes['body']) ? $attributes['body'] : ''; $data = array(); $dataID = 0; foreach($attributes as $k => $value) { if(empty($value)) continue; if(is_array($value)) { if(isset($value['status']) && $value['status'] == 'deleted') break; if(!empty($desc)) $value['description'] = $desc; if(!empty($summary)) $value['summary'] = $summary; if(!empty($body)) $value['body'] = $body; $dataID = !empty($value['id']) ? $value['id'] : ($key + 1); $data = array_merge($data, $value); } else { $data = array_merge($data, array($k => $value)); } } if(!empty($dataID)) $dataList[$dataID] = $data; } else { if(is_array($attributes)) { if(isset($attributes['status']) && $attributes['status'] == 'deleted') continue; $dataID = !empty($attributes['id']) ? $attributes['id'] : $key; $dataList[$dataID] = $attributes; } else { if(!empty($dataID)) $dataList[$dataID][$key] = $attributes; } } } } if($limit) { $dataList = array_slice($dataList, $lastID, $limit, true); if(empty($dataList)) return array(); } if(in_array($module, array_keys($this->config->convert->objectTables))) { foreach($dataList as $key => $data) { $buildFunction = 'build' . ucfirst($module) . 'Data'; $dataList[$key] = $this->$buildFunction($data); } } return $dataList; } /** * 分割jira文件。 * Split jira file. * * @access public * @return void */ public function splitFile(): void { $filePath = $this->app->getTmpRoot() . 'jirafile/'; $fileName = 'entities.xml'; $file = $filePath . $fileName; $handle = fopen($file, "r"); $tagList = array(' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' '', ' ''); while(!feof($handle)) { $itemStr = fgets($handle); if(empty($itemStr)) continue; foreach($tagList as $startName => $endName) { $startName .= ' '; if(strpos($itemStr, $startName) === false) continue; if(strpos($itemStr, '/>') === false) { while(true) { $followItemStr = fgets($handle); $itemStr .= $followItemStr; if(strpos($itemStr, $endName) !== false) break; } } $object = str_replace('<', '', $startName); $object = trim(strtolower($object)); $data = preg_replace ('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u', ' ', $itemStr); if(!file_exists($filePath . $object . '.xml')) $data = "\n \n" . $data; file_put_contents($filePath . $object . '.xml', $data, FILE_APPEND); } } foreach($tagList as $startName => $endName) { $object = str_replace('<', '', $startName); $object = strtolower($object); $filename = $filePath . $object . '.xml'; if(file_exists($filename)) file_put_contents($filename, '', FILE_APPEND); } fclose($handle); } /** * 创建jira数据表。 * Create tmp table for import jira. * * @access public * @return void */ public function createTmpTable4Jira(): void { $sql = <<dbh->exec($sql); } catch(Exception $e){} } /** * 使用接口导入Jira数据。 * Import jira from REST API. * * @param string $type user|project|issue|build|issuelink|action|file * @param int $lastID * @param bool $createTable * @access public * @return array */ public function importJiraData(string $type = '', int $lastID = 0, bool $createTable = false): array { if($createTable) $this->createTmpTable4Jira(); if($this->session->jiraMethod == 'db') $this->connectDB($this->session->jiraDB); $limit = 1000; $nextObject = false; if(empty($type)) $type = key($this->lang->convert->jira->objectList); foreach(array_keys($this->lang->convert->jira->objectList) as $module) { if($module != $type && !$nextObject) continue; if($module == $type) $nextObject = true; $this->convertTao->sourceDBH = $this->sourceDBH; while(true) { $dataList = $this->getJiraData($this->session->jiraMethod, $module, $lastID, $limit); if(empty($dataList)) { $lastID = 0; break; } if($module == 'user') $this->convertTao->importJiraUser($dataList); if($module == 'project') $this->convertTao->importJiraProject($dataList); if($module == 'issue') $this->convertTao->importJiraIssue($dataList); if($module == 'build') $this->convertTao->importJiraBuild($dataList); if($module == 'issuelink') $this->convertTao->importJiraIssueLink($dataList); if($module == 'worklog') $this->convertTao->importJiraWorkLog($dataList); if($module == 'action') $this->convertTao->importJiraAction($dataList); if($module == 'changeitem') $this->convertTao->importJiraChangeItem($dataList); if($module == 'file') $this->convertTao->importJiraFile($dataList); $offset = $lastID + $limit; return array('type' => $module, 'count' => count($dataList), 'lastID' => $this->session->jiraMethod == 'db' ? max(array_keys($dataList)) : $offset); } } if($this->session->jiraMethod == 'file') $this->deleteJiraFile(); /* 更新各项目的统计数据。 */ $projectList = $this->dao->dbh($this->dbh)->select('BID')->from(JIRA_TMPRELATION)->where('BType')->in('zproject,zexecution')->fetchPairs(); $this->loadModel('program')->updateStats($projectList); unset($_SESSION['jiraDB']); unset($_SESSION['jiraMethod']); unset($_SESSION['jiraRelation']); unset($_SESSION['stepStatus']); unset($_SESSION['jiraUser']); return array('finished' => true); } /** * 删除jira文件。 * Delete jira backip file. * * @access public * @return void */ public function deleteJiraFile(): void { $fileList = array('action', 'project', 'status', 'resolution', 'user', 'issue', 'changegroup', 'changeitem', 'issuelink', 'issuelinktype', 'fileattachment', 'version', 'issuetype', 'nodeassociation', 'applicationuser', 'fieldscreenlayoutitem', 'workflow', 'workflowscheme', 'fieldconfigscheme', 'fieldconfigschemeissuetype', 'customfield', 'customfieldoption', 'customfieldvalue', 'ospropertyentry', 'worklog', 'auditlog', 'group', 'membership', 'projectroleactor', 'priority', 'configurationcontext', 'optionconfiguration', 'fixversion', 'affectsversion'); foreach($fileList as $fileName) { $filePath = $this->app->getTmpRoot() . 'jirafile/' . $fileName . '.xml'; if(file_exists($filePath)) @unlink($filePath); } } /** * 检查数据库名称。 * Check dbName is valide. * * @param string $dbName * @access public * @return bool */ public function checkDBName(string $dbName): bool { if(preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $dbName)) return true; return false; } /** * 获取禅道对象列表。 * Get zentao object list. * * @access public * @return array */ public function getZentaoObjectList(): array { $objectList = $this->lang->convert->jira->zentaoObjectList; if(!$this->config->enableER) unset($objectList['epic']); if(!$this->config->URAndSR) unset($objectList['requirement']); return $objectList; } /** * 获取禅道关联关系列表。 * Get zentao relation list. * * @access public * @return array */ public function getZentaoRelationList(): array { return $this->lang->convert->jira->zentaoLinkTypeList; } /** * 获取禅道字段列表。 * Get zentao fields. * * @param string $module * @access public * @return array */ public function getZentaoFields(string $module): array { $this->app->loadLang($module); $fields = array(); if(isset($this->config->convert->objectFields[$module])) { foreach($this->config->convert->objectFields[$module] as $field) { $fields[$field] = $this->lang->{$module}->$field; } } return $fields; } /** * 获取禅道状态列表。 * Get zentao status. * * @param string $module * @access public * @return array */ public function getZentaoStatus(string $module): array { $this->loadModel($module); $statusList = $this->lang->{$module}->statusList; if($module == 'testcase') $statusList = array_merge($statusList, array('add_case_status' => $this->lang->convert->add)); return $statusList; } /** * 获取导入jira的步骤列表。 * Get import jira data steps. * * @param array $jiraData * @param array $issueTypeList * @access public * @return array */ public function getJiraStepList(array $jiraData, array $issueTypeList = array()): array { $stepList = array(); $objectSteps = array(); if(empty($issueTypeList)) $issueTypeList = $this->getJiraData($this->session->jiraMethod, 'issuetype'); if(!empty($jiraData['jiraObject'])) { foreach($jiraData['jiraObject'] as $objectID) { if($jiraData['zentaoObject'][$objectID] == 'add_custom') continue; if($objectID) $objectSteps[$objectID] = zget($issueTypeList[$objectID], 'pname', '') . $this->lang->convert->jira->steps['objectData']; } } foreach($this->lang->convert->jira->steps as $currentStep => $stepLabel) { if($currentStep == 'objectData') { if(!empty($objectSteps)) { $stepList = $stepList + $objectSteps; continue; } } $stepList[$currentStep] = $stepLabel; } return $stepList; } /** * 校验导入jira数据时配置信息完整性。 * Check import jira. * * @param string $step * @access public * @return bool */ public function checkImportJira(string $step): bool { $data = fixer::input('post')->get(); if($step == 'object') { $jiraObject = $data->jiraObject; $zentaoObject = $data->zentaoObject; foreach($jiraObject as $typeID) { if(empty($zentaoObject[$typeID])) dao::$errors['message'] = sprintf($this->lang->error->notempty, $this->lang->convert->jira->zentaoObject); } } return !dao::isError(); } /** * 获取导入Jira时禅道的对象属性默认值。 * Get object default value. * * @param string $step * @access public * @return array */ public function getObjectDefaultValue(string $step): array { if($step == 'object') return $this->config->convert->importDeafaultValue; $jiraRelation = $this->session->jiraRelation; $jiraRelation = $jiraRelation ? json_decode($jiraRelation, true) : array(); if(empty($jiraRelation['zentaoObject'][$step])) return array(); $zentaoObject = $jiraRelation['zentaoObject'][$step]; $defaultValue = array(); $importDeafaultValue = $this->config->convert->importDeafaultValue; $defaultValue["zentaoField$step"] = !empty($importDeafaultValue[$zentaoObject]['field']) ? $importDeafaultValue[$zentaoObject]['field'] : array(); $defaultValue["zentaoStatus$step"] = !empty($importDeafaultValue[$zentaoObject]['status']) ? $importDeafaultValue[$zentaoObject]['status'] : array(); $defaultValue["zentaoAction$step"] = !empty($importDeafaultValue[$zentaoObject]['action']) ? $importDeafaultValue[$zentaoObject]['action'] : array(); $defaultValue["zentaoResolution$step"] = !empty($importDeafaultValue[$zentaoObject]['resolution']) ? $importDeafaultValue[$zentaoObject]['resolution'] : array(); $defaultValue["zentaoReason$step"] = !empty($importDeafaultValue[$zentaoObject]['reason']) ? $importDeafaultValue[$zentaoObject]['reason'] : array(); return $defaultValue; } /** * 获取Jira事务类型列表。 * Get jira type list. * * @access public * @return array */ public function getJiraTypeList(): array { return $this->getJiraData($this->session->jiraMethod, 'issuetype'); } /** * 获取Jira状态列表。 * Get jira status list. * * @param string $step * @access public * @return array */ public function getJiraStatusList($step = ''): array { if($this->session->jiraMethod == 'api') { $jql = 'created<=' . date('Y-m-d', strtotime('+1 day')); if($step) $jql .= " AND issuetype = {$step}"; $issues = $this->callJiraAPI('/rest/api/3/search/jql?jql=' . urlencode($jql) . '&fields=status,issuetype&maxResults=5000'); foreach($issues as $issue) { if(!empty($issue->fields)) { foreach($issue->fields as $field => $value) $issue->$field = $value; } if(!empty($issue->status->id)) $issue->issuestatus = $issue->status->id; if(!empty($issue->issuetype->id)) $issue->issuetype = $issue->issuetype->id; } } else { $issues = $this->getJiraData($this->session->jiraMethod, 'issue'); } $statusList = $this->getJiraData($this->session->jiraMethod, 'status'); $jiraStatusList = array(); foreach($issues as $issue) { if(empty($issue->issuestatus)) continue; if(empty($statusList[$issue->issuestatus])) continue; $status = $statusList[$issue->issuestatus]; $jiraStatusList[$issue->issuetype][$issue->issuestatus] = $status->pname; } return $step ? zget($jiraStatusList, $step, array()) : $jiraStatusList; } /** * 获取Jira自定义字段列表。 * Get jira custom fields. * * @access public * @return array */ public function getJiraCustomField(): array { if($this->config->edition == 'open') return array(); $jiraFields = array(); $fields = $this->getJiraData($this->session->jiraMethod, 'customfield'); if($this->session->jiraMethod == 'api') { foreach($fields as $issueType => $fieldList) { foreach($fieldList as $fieldID => $field) $fields[$issueType][$fieldID] = $field->cfname; } return $fields; } $issues = $this->getJiraData($this->session->jiraMethod, 'issue'); $fieldValue = $this->getJiraData($this->session->jiraMethod, 'customfieldvalue'); foreach($fieldValue as $value) { if(empty($issues[$value->issue]) || empty($fields[$value->customfield])) continue; $issue = $issues[$value->issue]; $field = $fields[$value->customfield]; if(in_array($field->customfieldtypekey, array('com.pyxis.greenhopper.jira:gh-sprint', 'com.pyxis.greenhopper.jira:gh-epic-label', 'com.pyxis.greenhopper.jira:gh-epic-status', 'com.pyxis.greenhopper.jira:gh-epic-color'))) continue; $jiraFields[$issue->issuetype][$value->customfield] = $field->cfname; } return $jiraFields; } /** * 获取Jira按照项目分组的自定义字段。 * Get jira custom fields group by project. * * @param array $relations * @access public * @return array */ public function getJiraFieldGroupByProject($relations): array { $fieldList = $this->dao->dbh($this->dbh)->select('AID, extra, BID AS field')->from(JIRA_TMPRELATION)->where('AType')->eq('jcustomfield')->andWhere('BType')->eq('zworkflowfield')->fetchGroup('AID', 'extra'); if($this->session->jiraMethod == 'api') { $projectList = $this->callJiraAPI('/rest/api/3/issue/createmeta?expand=projects.issuetypes.fields&maxResults=1000'); $jiraFields = array(); foreach($projectList as $project) { if(empty($project->issuetypes)) continue; foreach($project->issuetypes as $issueType) { if(empty($issueType->fields)) continue; $zentaoObject = $relations['zentaoObject'][$issueType->id]; foreach($issueType->fields as $key => $field) { if(strpos($key, 'customfield_') === false) continue; $fieldKey = str_replace('customfield_', '', $key); if(empty($fieldList[$fieldKey])) continue; $zentaoField = $fieldList[$fieldKey][$zentaoObject]; $jiraFields[$project->id][$zentaoObject][$zentaoField->field] = $zentaoField; } } } return $jiraFields; } $issues = $this->getJiraData($this->session->jiraMethod, 'issue'); $fieldValue = $this->getJiraData($this->session->jiraMethod, 'customfieldvalue'); $jiraFields = array(); foreach($fieldValue as $value) { if(empty($issues[$value->issue])) continue; $issue = $issues[$value->issue]; $zentaoObject = $relations['zentaoObject'][$issue->issuetype]; if(!empty($fieldList[$value->customfield][$zentaoObject])) { $field = $fieldList[$value->customfield][$zentaoObject]; $jiraFields[$issue->project][$zentaoObject][$field->field] = $field; } } return $jiraFields; } /** * 获取Jira工作流里的动作列表。 * Get jira workflow actions. * * @access public * @return array */ public function getJiraWorkflowActions(): array { if($this->config->edition == 'open') return array(); if($this->session->jiraMethod == 'api') { $schemeList = $this->callJiraAPI('/rest/api/3/issuetypescheme?expand=projects,issuetypes&maxResults=1000'); $workflowList = $this->callJiraAPI('/rest/api/3/workflow/search?expand=transitions,projects&maxResults=1000'); $projectGroup = array(); foreach($schemeList as $scheme) { if(empty($scheme->issueTypes->values) || empty($scheme->projects->values)) continue; foreach($scheme->issueTypes->values as $issueType) { foreach($scheme->projects->values as $project) $projectGroup[$issueType->id][$project->id] = $project->id; } } $workflowActions = array(); foreach($workflowList as $workflow) { if(empty($workflow->transitions) || empty($workflow->projects)) continue; foreach($workflow->projects as $project) { foreach($projectGroup as $issueType => $projects) { if(!in_array($project->id, $projects)) continue; foreach($workflow->transitions as $action) $workflowActions[$issueType]['actions'][$action->id] = (array)$action; } } } return $workflowActions; } $workflows = $this->getJiraData($this->session->jiraMethod, 'workflow'); $workflowActions = array(); $actionNameList = array(); foreach($workflows as $workflowID => $workflow) { $descriptor = simplexml_load_string($workflow->descriptor); $descriptor = $this->object2Array($descriptor); foreach($descriptor as $id => $actions) { if(!empty($actions['action'])) { $actionInfo = array(); foreach($actions['action'] as $key => $actionList) { if(is_numeric($key)) { foreach($actionList as $k => $action) { $actionInfo = array_merge($actionInfo, $k == '@attributes' ? $action : array($k => $action)); if(empty($actionNameList[$actionInfo['name']]) && strpos($actionInfo['name'], 'Issue') === false) $workflowActions['actions'][] = $actionInfo; $actionNameList[$actionInfo['name']] = $actionInfo['name']; } } else { $actionInfo = array_merge($actionInfo, $key == '@attributes' ? $actionList : array($key => $actionList)); if(empty($actionNameList[$actionInfo['name']]) && strpos($actionInfo['name'], 'Issue') === false) $workflowActions['actions'][] = $actionInfo; $actionNameList[$actionInfo['name']] = $actionInfo['name']; } } } elseif(!empty($actions['step'])) { foreach($actions['step'] as $step) { if(empty($step['meta'])) continue; $workflowActions['steps'][$step['@attributes']['id']] = $step['meta']; } } } } return $workflowActions; } /** * 验证Jira api接口能否访问。 * Check jira api. * * @access public * @return bool */ public function checkJiraApi(): bool { $jiraApi = json_decode($this->session->jiraApi, true); if(empty($jiraApi['domain'])) return false; $token = base64_encode("{$jiraApi['admin']}:{$jiraApi['token']}"); $url = $jiraApi['domain'] . '/rest/agile/1.0/board'; $boardList = json_decode(commonModel::http($url, array(), array(), array("Authorization: Basic $token"), 'json', 'GET', 10)); if(empty($boardList->values)) { dao::$errors['message'] = $this->lang->convert->jira->apiError; return false; } return true; } /** * 调用Jira api接口。 * Call Jira API. * * @param string $url * @param int $start * @param bool $rawData * @access public * @return array */ public function callJiraAPI($url, $start = 0, $rawData = false) { if(empty($_SESSION['jiraApi'])) return array(); $jiraApi = json_decode($this->session->jiraApi, true); if(empty($jiraApi['domain'])) return array(); if(strpos($url, $jiraApi['domain']) === 0) $url = str_replace($jiraApi['domain'], '', $url); $token = base64_encode("{$jiraApi['admin']}:{$jiraApi['token']}"); $httpURL = $jiraApi['domain'] . $url . "&startAt=$start"; $result = json_decode(commonModel::http($httpURL, array(), array(), array("Authorization: Basic $token"), 'json', 'GET', 10)); if(!empty($result->values) || !empty($result->issues)) { $dataList = !empty($result->values) ? $result->values : $result->issues; if(empty($result->isLast)) { if(!empty($result->nextPageToken)) { $dataList = array_merge($dataList, $this->callJiraAPI($url . "&nextPageToken={$result->nextPageToken}")); } else { $dataList = array_merge($dataList, $this->callJiraAPI($url, $start + $result->maxResults)); } } return $dataList; } if(!empty($result->projects)) return $result->projects; return $rawData ? $result : array(); } /** * 获取jira用户。 * Get jira account. * * @param string $userKey * @access public * @return string */ public function getJiraAccount(string $userKey): string { if(empty($userKey)) return ''; $users = $this->getJiraUser(); return zget($users, $userKey, ''); } /** * 处理Jira用户名为禅道格式。 * Process jira user. * * @param string $jiraAccount * @param string $jiraEmail * @access public * @return string */ public function processJiraUser(string $jiraAccount, string $jiraEmail): string { $userConfig = $this->session->jiraUser; $account = substr($jiraAccount, 0, 30); if($userConfig['mode'] == 'email' && $jiraEmail) { if(strpos($jiraEmail, '@') !== false) { $account = substr(substr($jiraEmail, 0, strpos($jiraEmail, '@')), 0, 30); } else { $account = substr($jiraEmail, 0, 30); } } return preg_replace("/[^a-zA-Z0-9]/", "", $account); } /** * 从jira文件中获取版本信息。 * Get version group from jira file. * * @access public * @return array */ public function getVersionGroup(): array { $filePath = $this->app->getTmpRoot() . 'jirafile/nodeassociation.xml'; if(!file_exists($filePath)) return array(); $xmlContent = file_get_contents($this->app->getTmpRoot() . 'jirafile/nodeassociation.xml'); $xmlContent = preg_replace ('/[^\x{0009}\x{000a}\x{000d}\x{0020}-\x{D7FF}\x{E000}-\x{FFFD}]+/u', ' ', $xmlContent); $parsedXML = simplexml_load_string($xmlContent, 'SimpleXMLElement', LIBXML_NOCDATA); if(empty($parsedXML)) return array(); $dataList = array(); $parsedXML = $this->object2Array($parsedXML); foreach($parsedXML as $key => $xmlArray) { if(strtolower($key) != 'nodeassociation') continue; foreach($xmlArray as $key => $attributes) { foreach($attributes as $value) { if(!is_array($value)) continue; if($value['sinkNodeEntity'] != 'Version') continue; $data = new stdclass(); $data->versionid = $value['sinkNodeId']; $data->issueid = $value['sourceNodeId']; $data->relation = $value['associationType']; $dataList[$value['sinkNodeId']][] = $data; } } } return $dataList; } /** * 将对象转换为数组。 * Convert object to array. * * @param object|array $parsedXML * @access public * @return array */ public function object2Array(object|array $parsedXML): array { if(is_object($parsedXML)) { $parsedXML = (array)$parsedXML; } if(is_array($parsedXML)) { foreach($parsedXML as $key => $value) { if(is_object($value) && empty($value)) { $parsedXML[$key] = (string)$value; continue; } if(!is_object($value) && !is_array($value)) continue; $parsedXML[$key] = $this->object2Array($value); } } return $parsedXML; } /** * 转换需求阶段。 * Convert stage. * * @param string $jiraStatus * @param string $issueType * @param array $relations * @access public * @return string */ public function convertStage(string $jiraStatus, string $issueType, array $relations = array()): string { if(empty($relations)) { $jiraRelation = $this->session->jiraRelation; $relations = $jiraRelation ? json_decode($jiraRelation, true) : array(); } $stage = 'wait'; if(!empty($relations["zentaoStage$issueType"][$jiraStatus])) $stage = $relations["zentaoStage$issueType"][$jiraStatus]; return $stage; } /** * 转换状态。 * Convert jira status. * * @param string $objectType * @param string $jiraStatus * @param string $issueType * @param array $relations * @access public * @return string */ public function convertStatus(string $objectType, string $jiraStatus, string $issueType, array $relations = array()): string { if(empty($relations)) { $jiraRelation = $this->session->jiraRelation; $relations = $jiraRelation ? json_decode($jiraRelation, true) : array(); } if(!empty($relations["zentaoStatus{$issueType}"][$jiraStatus])) return (string)$relations["zentaoStatus{$issueType}"][$jiraStatus]; if($objectType == 'testcase' && empty($this->config->testcase->needReview)) return 'normal'; if($objectType == 'feedback' && empty($this->config->feedback->needReview)) return 'normal'; if($objectType == 'ticket' && empty($this->config->feedback->ticket)) return 'normal'; return in_array($objectType, array('task', 'testcase', 'feedback', 'ticket', 'flow')) ? 'wait' : 'active'; } /** * 获取Jira的所有冲刺。 * Get jira sprint. * * @param array $projectList * @access public * @return array */ public function getJiraSprint(array $projectList): array { $sprintGroup = array(); if($this->session->jiraMethod == 'file' || $this->session->jiraMethod == 'api') { foreach($projectList as $projectID) { $boardList = $this->callJiraAPI("/rest/agile/1.0/board?projectKeyOrId={$projectID}&maxResults=1000"); foreach($boardList as $board) { $sprintList = $this->callJiraAPI("/rest/agile/1.0/board/$board->id/sprint?maxResults=1000"); foreach($sprintList as $sprint) { $sprintGroup[$projectID][$sprint->id] = $sprint; } } } } else { $sprintGroup = $this->dao->dbh($this->sourceDBH)->select('project.id as pid, sprint.*')->from('ao_60db71_sprint')->alias('sprint') ->leftJoin('ao_60db71_rapidview')->alias('rapview')->on('rapview.id=sprint.RAPID_VIEW_ID') ->leftJoin('searchrequest')->alias('search')->on('search.ID=rapview.SAVED_FILTER_ID') ->leftJoin('project')->on("search.reqcontent like concat('%project = ', project.pkey, ' O%')") ->where('project.id')->notNULL() ->fetchGroup('pid', 'id'); } return $sprintGroup; } /** * 获取Jira issue的所属sprint。 * Get jira sprint issue. * * @access public * @return array */ public function getJiraSprintIssue(): array { $issueGroup = array(); $sprintRelation = $this->dao->dbh($this->dbh)->select('AID,BID')->from(JIRA_TMPRELATION)->where('AType')->eq('jsprint')->andWhere('BType')->eq('zexecution')->fetchPairs(); if($this->session->jiraMethod != 'db') { foreach($sprintRelation as $sprintID => $executionID) { $issueList = $this->callJiraAPI("/rest/agile/1.0/sprint/{$sprintID}/issue?maxResults=1000"); foreach($issueList as $issue) { $issueGroup[$issue->id] = $executionID; if(!empty($issue->fields->subtasks)) { foreach($issue->fields->subtasks as $subtask) $issueGroup[$subtask->id] = $executionID; } } } } return $issueGroup; } /** * 获取Jira已归档的项目。 * Get jira archived project. * * @param array $dataList * @access public * @return array */ public function getJiraArchivedProject($dataList): array { if($this->session->jiraMethod == 'api') return array(); $archivedProject = array(); $auditLog = $this->getJiraData($this->session->jiraMethod, 'auditlog'); if($auditLog) { foreach($auditLog as $log) { if($log->summary == 'Project archived' && $log->object_type == 'project') $archivedProject[$log->object_id] = $log->object_id; } } if($this->session->jiraMethod == 'file') { if(empty($_SESSION['jiraApi'])) return $archivedProject; $jiraApi = json_decode($this->session->jiraApi, true); if(empty($jiraApi['domain'])) return $archivedProject; $token = base64_encode("{$jiraApi['admin']}:{$jiraApi['token']}"); $url = $jiraApi['domain'] . '/rest/api/2/project/'; $result = json_decode(commonModel::http($url, array(), array(), array("Authorization: Basic $token"), 'json', 'GET', 10)); $projectList = array(); if(!empty($result)) { foreach($result as $project) { if(!empty($project->id) && empty($project->archived)) $projectList[$project->id] = $project->id; // 没有被归档的项目。 } } /* 过滤掉没被归档的项目,剩下的都是被归档的项目。 */ foreach($dataList as $project) { if(empty($projectList[$project->id])) $archivedProject[$project->id] = $project->id; } } else { $sql = "SHOW tables like 'ao_c77861_audit_entity';"; if($this->dao->dbh($this->sourceDBH)->query($sql)->fetch()) { $auditEntity = $this->dao->dbh($this->sourceDBH)->select('PRIMARY_RESOURCE_ID')->from('ao_c77861_audit_entity') ->where('ACTION_T_KEY')->eq('jira.auditing.project.archived') ->andWhere('PRIMARY_RESOURCE_TYPE')->eq('PROJECT') ->fetchPairs(); $archivedProject = array_merge($archivedProject, $auditEntity); } } return $archivedProject; } /** * 获取Jira项目角色与成员。 * Get jira project role actor. * * @param array $projectList * @access public * @return array */ public function getJiraProjectRoleActor(array $projectList): array { if($this->session->jiraMethod == 'api') { $projectMember = array(); foreach($projectList as $project) { $projectRoles = $this->callJiraAPI("/rest/api/2/project/{$project->id}/role?maxResults=1000", 0, true); foreach($projectRoles as $role) { if(!is_string($role)) continue; $users = $this->callJiraAPI($role . '?maxResults=1000', 0 , true); if(empty($users->actors)) continue; foreach($users->actors as $user) $projectMember[$project->id][$user->actorUser->accountId] = $user->actorUser->accountId; } } return $projectMember; } $projectRoleActor = $this->getJiraData($this->session->jiraMethod, 'projectroleactor'); $memberShip = $this->getJiraData($this->session->jiraMethod, 'membership'); $projectMember = array(); foreach($projectRoleActor as $role) { if(empty($role->pid)) continue; if($role->roletype == 'atlassian-user-role-actor') { $projectMember[$role->pid][$role->roletypeparameter] = $role->roletypeparameter; } if($role->roletype == 'atlassian-group-role-actor') { foreach($memberShip as $member) { if($member->parent_name == $role->roletypeparameter) $projectMember[$role->pid]["JIRAUSER{$member->child_id}"] = 'JIRAUSER' . $member->child_id; } } } return $projectMember; } /** * 获取按照项目分组的Jira事务类型。 * get Jira issue type group by project. * * @param array $relations * @access public * @return array */ public function getIssueTypeList(array $relations): array { if($this->session->jiraMethod == 'api') { $projectList = $this->callJiraAPI('/rest/api/3/issue/createmeta?expand=projects.issuetypes&maxResults=1000'); $projectGroup = array(); foreach($projectList as $project) { if(empty($project->issuetypes)) continue; foreach($project->issuetypes as $issueType) { $projectGroup[$project->id][] = $relations['zentaoObject'][$issueType->id]; } } return $projectGroup; } $schemeproject = $this->getJiraData($this->session->jiraMethod, 'configurationcontext'); $schemeissuetype = $this->getJiraData($this->session->jiraMethod, 'optionconfiguration'); $projectIssueTypeList = array(); foreach($schemeproject as $projectRelation) { if(!empty($projectRelation->project) && $projectRelation->customfield == 'issuetype') { foreach($schemeissuetype as $issueTypeRelation) { if($issueTypeRelation->fieldconfig == $projectRelation->fieldconfigscheme && $issueTypeRelation->fieldid == 'issuetype' && !empty($issueTypeRelation->optionid)) { if(!empty($relations['zentaoObject'][$issueTypeRelation->optionid])) $projectIssueTypeList[$projectRelation->project][] = $relations['zentaoObject'][$issueTypeRelation->optionid]; } } } } return $projectIssueTypeList; } /** * 快速导入jira数据。 * Quick import Jira data. * * @access public * @return void */ public function quickImportJiraData() { $jiraApi = array(); $jiraApi['domain'] = $this->post->domain; $jiraApi['admin'] = $this->post->account; $jiraApi['token'] = $this->post->token; $this->session->set('jiraApi', json_encode($jiraApi)); $this->session->set('jiraDB', ''); $this->session->set('jiraMethod', 'api'); $this->checkJiraApi(); if(dao::isError()) return false; $issueTypeList = $this->getJiraTypeList(); $defaultValue = $this->getObjectDefaultValue('object'); $jiraFields = $this->getJiraCustomField(); $jiraStatus = $this->getJiraStatusList(); $jiraActions = $this->getJiraWorkflowActions(); $jiraResolution = $this->getJiraData('api', 'resolution'); $jiraLinkTypes = $this->getJiraData('api', 'issuelinktype'); $jiraRelation = array(); foreach($issueTypeList as $issueID => $issueType) { $jiraRelation['jiraObject'][] = $issueID; $jiraRelation['zentaoObject'][$issueID] = !empty($defaultValue['zentaoObject'][$issueType->pname]) ? $defaultValue['zentaoObject'][$issueType->pname] : 'add_custom'; if($jiraRelation['zentaoObject'][$issueID] == 'add_custom') continue; if(!empty($jiraFields[$issueID])) { foreach($jiraFields[$issueID] as $fieldID => $field) { $jiraRelation["jiraField{$issueID}"][] = $fieldID; $jiraRelation["zentaoField{$issueID}"][$fieldID] = 'add_field'; } } if(!empty($jiraStatus[$issueID])) { foreach($jiraStatus[$issueID] as $statusID => $status) { $jiraRelation["jiraStatus{$issueID}"][] = $statusID; $jiraRelation["zentaoStatus{$issueID}"][$statusID] = !empty($this->config->convert->importDeafaultValue[$jiraRelation['zentaoObject'][$issueID]]['status'][$status]) ? $this->config->convert->importDeafaultValue[$jiraRelation['zentaoObject'][$issueID]]['status'][$status] : ''; if(in_array($jiraRelation['zentaoObject'][$issueID], array('requirement', 'story', 'epic'))) $jiraRelation["zentaoStage{$issueID}"][$statusID] = ''; } } if(!empty($jiraActions[$issueID])) { foreach($jiraActions[$issueID]['actions'] as $actionID => $action) { $jiraRelation["jiraAction{$issueID}"][] = $actionID; $jiraRelation["zentaoAction{$issueID}"][$actionID] = !empty($this->config->convert->importDeafaultValue[$jiraRelation['zentaoObject'][$issueID]]['action'][$action['name']]) ? $this->config->convert->importDeafaultValue[$jiraRelation['zentaoObject'][$issueID]]['action'][$action['name']] : 'add_action'; } } if(!empty($jiraResolution)) { foreach($jiraResolution as $resolutionID => $resolution) { $jiraRelation["jiraResolution{$issueID}"][] = $resolutionID; if($jiraRelation['zentaoObject'][$issueID] == 'bug') { $jiraRelation["zentaoResolution{$issueID}"][$resolutionID] = !empty($this->config->convert->importDeafaultValue[$jiraRelation['zentaoObject'][$issueID]]['resolution'][$resolution->pname]) ? $this->config->convert->importDeafaultValue[$jiraRelation['zentaoObject'][$issueID]]['resolution'][$resolution->pname] : ''; } else { $jiraRelation["zentaoReason{$issueID}"][$resolutionID] = !empty($this->config->convert->importDeafaultValue[$jiraRelation['zentaoObject'][$issueID]]['reason'][$resolution->pname]) ? $this->config->convert->importDeafaultValue[$jiraRelation['zentaoObject'][$issueID]]['reason'][$resolution->pname] : ''; } } } } if(!empty($jiraLinkTypes)) { foreach($jiraLinkTypes as $linkID => $linkType) { $jiraRelation['jiraLinkType'][] = $linkID; $jiraRelation['zentaoLinkType'][$linkID] = 'add_relation'; } } $jiraUser['password'] = md5('123456'); $jiraUser['group'] = ''; $jiraUser['mode'] = 'email'; $this->session->set('jiraUser', $jiraUser); $this->session->set('jiraRelation', json_encode($jiraRelation)); $this->batchImportJiraData(); return true; } /** * 分批次的导入Jira数据。 * Batch import Jira data. * * @param string $type * @param int $lastID * @param bool $createTable * @access public * @return void */ public function batchImportJiraData(string $type = '', int $lastID = 0, bool $createTable = true): bool { $result = $this->importJiraData($type, $lastID, $createTable); if(!empty($result['finished'])) return true; return $this->batchImportJiraData($result['type'], $result['lastID'], false); } /** * 将日期转成对应时区的日期。 * Format date. * * @param string $date * @access public * @return string */ public function formatDate(string $date): string { if($this->session->jiraMethod == 'api') return $date; $dateUTC = new DateTime($date, new DateTimeZone('UTC')); $dateUTC->setTimezone(new DateTimeZone($this->config->timezone)); return $dateUTC->format('Y-m-d'); } /** * 将日期转成对应时区的日期。 * Format date. * * @param string $date * @access public * @return string */ public function formatDatetime(string $datetime): string { if($this->session->jiraMethod == 'api') return $datetime; $dateUTC = new DateTime($datetime, new DateTimeZone('UTC')); $dateUTC->setTimezone(new DateTimeZone($this->config->timezone)); return $dateUTC->format('Y-m-d H:i:s'); } }