在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法
禅道是一款国产的不错的项目管理软件,有开源和商业版本,软件做得挺不错的,如果大家所在的公司不差这些小钱,建议使用禅道商业版,也算是对优秀软件的一点支持吧。
1、为什么会需要调用禅道的API接口?
主要目的是为了解决一些运维工作自动化方面的问题。我希望把包括zabbix监控系统在内的各种监控报警事件,做成自动根据事件信息生成禅道上特定项目下的任务工单,同时指派给指定的值班人员。在过往的工作中,经常发生监控报警事件得不到处理,或处理后未更新事件状态的事情,有时也存在角色、职责分工不明,一个报警事件同时发给了十个人,却没有一个主要负责人,反而产生的事件响应处理上的延误。所以准备做一个自动将报警事件转换为待处理的工单的功能,需要使用到禅道API接口。
2、禅道创建新任务的API接口的说明
针对禅道的API使用方面的知识,会另外写一篇文章。这里只谈与bug有关的部分。
这个接口bug是存在于创建新任务的api中,产生的原因是禅道的版本迭代、功能演变,改变了一个表单参数的数据类型,而在源码的处理逻辑中忽视了这一点所带来的影响。
创建一个任务的API接口使用信息如下表所示:
GET/POST /zentao/task-create-[projectID]-[storyID]-[moduleID]-[taskID]-[todoID].json |
||
Create a task. |
||
参数列表 |
类型 |
描述 |
projectID |
int |
|
storyID |
int |
|
moduleID |
int |
|
taskID |
int |
|
todoID |
int |
|
注:实际上参数projectID是个必选参数,其它参数可选。
下面是我的测试环境中的接口地址,向projectID=1, storyID=1, modeuleID=3的项目模块中创建一个task:
http://192.168.81.7/zentao/task-create-1-1-3.json?zentaosid=mfn6e7p8ptan851aum5l7fcao0&t=json
一点说明:
- zentaosid,是事先获取到的session id,保存在本地cookie中,每次调用接口时需要带上
- t=json,使用json格式进行数据交互
- 使用POST方式提交
- 使用form-data格式同步提交表单数据,从创建新任务的web表单页面上可以看到只有两个属性是必须的,即type, name,分别定义了任务类型和任务名称。
3、使用Postman调试api接口
我们需要使用Postman来模拟POST方式提交数据。从以下地址下载和安装postman工具。
安装好以后,按下图所示方法对指定的api接口地址进行测试。需要填写URL地址,选择提交方式,选择提交数据的编码格式,定义好必要的表单参数和数据。
点击“Send”按钮后,得到返回结果,"Body"数据显示结果是success的,"locate"则是一个页面重定向信息。
我们所要讨论的API接口bug,也就是在这里。因为上面操作是成功的,实际上禅道中指定项目-模块下并没有创建出我们的任务来。
如果不介意的话,上面Postman中的"Send"按钮,多点几次也无妨,因为每次都会返回一个"success"的结果回来。所有的提交尝试都是无效的。
与此同时,直接在禅道web页面上通过页面表单,仅填写type, name字段,是可以成功提交和创建出任务来的。
下面就依据分析和定准错误的过程做一下说明。
4、观察和分析创建禅道任务的表单参数
如上图所示,直接把创建任务的URL地址从html改为json后缀,就可以得到json格式的数据了。
主要包括了project,story,task,users等几部分数据。我们主要关注task这一段,如下所示:
"task":{"module":3,"assignedTo":"","name":"","story":1,"type":"","pri":"","estimate":"","desc":"","estStarted":"","deadline":"","mailto":"","color":""}
猜测,可能是需要填写和提交的表单参数不全?
验证办法:逐个把上面task涉及的所有参数,添加到Postman的form-data中去,并测试提交。
初步结论:显然是提交再多的表单参数也不管用
实际原因:从上面json数据得到的表单字段参数assignedTo名称有误,这个后面会逐步谈到。
5、使用wireshark抓包分析数据包内容
1)登录到运行这套禅道软件的测试机上,执行抓包命令:
# tcpdump -i eth0 -w /tmp/20180724001.pcap
2)在本地PC机上打开浏览器,登录到禅道网站并创建一个指定项目、模块下的禅道任务
3)在本地PC上分析抓包文件,关键信息如下图所示
- 先找到POST操作的数据包
- 再检查MIME数据内容,可以分辨出通过表单提交的各个属性字段和取值
- 重点观察下上图中展示出的name="assignedTo[]"
因为wireshark中能看到的数据包内容不是很友好,所以虽然抓包时已经看到了assignedTo[]的信息,但被上一步骤中的json数据参数误导了,以为上图中的意思是assignedTo参数,取值为[],一个空数组。
实际情况是:表单参数名是assignedTo[],取值为空。这个就是最终水落石出时才发现的了。
6、阅读和分析禅道php源码
大部分WEB开发框架,不论是什么编程语言开发的,都会遵循MVC的一个设计逻辑。所以我们可以看到禅道项目源码目录如下图所示,主要实现逻辑就都在model.php中了,控制逻辑在control.php中。
我们只截取一点关键部分的代码做分析。
下面是control.php文件中的create方法的一部分:
/**
* Create a task.
*
* @param int $projectID
* @param int $storyID
* @param int $moduleID
* @param int $taskID
* @param int $todoID
* @access public
* @return void
*/
public function create($projectID = 0, $storyID = 0, $moduleID = 0, $taskID = 0, $todoID = 0)
{
$task = new stdClass();
$task->module = $moduleID;
$task->assignedTo = '';
$task->name = '';
$task->story = $storyID;
$task->type = '';
$task->pri = '';
$task->estimate = '';
$task->desc = '';
$task->estStarted = '';
$task->deadline = '';
$task->mailto = '';
$task->color = '';
if($taskID > 0)
{
$task = $this->task->getByID($taskID);
$projectID = $task->project;
}
if($todoID > 0)
{
$todo = $this->loadModel('todo')->getById($todoID);
$task->name = $todo->name;
$task->pri = $todo->pri;
$task->desc = $todo->desc;
}
$project = $this->project->getById($projectID);
$taskLink = $this->createLink('project', 'browse', "projectID=$projectID&tab=task");
$storyLink = $this->session->storyList ? $this->session->storyList : $this->createLink('project', 'story', "projectID=$projectID");
/* Set menu. */
$this->project->setMenu($this->project->getPairs(), $project->id);
if(!empty($_POST))
{
$response['result'] = 'success';
$response['message'] = '';
$tasksID = $this->task->create($projectID);
if(dao::isError())
{
$response['result'] = 'fail';
$response['message'] = dao::getError();
$this->send($response);
}
- 从上面代码可以看出我们在创建一个禅道任务时,是分成多个子步骤执行的,先创建了一个空的$task对象,然后逐步填充内容;
- 根据$tasksID = $this->task->create($projectID);这行代码可以看到它是在调用model中的create($projectID)方法完成创建一个task的部分工作;
下面是model.php文件中create($projectID)方法的一部分:
class taskModel extends model
{
/**
* Create a task.
*
* @param int $projectID
* @access public
* @return void
*/
public function create($projectID)
{
$taskIdList = array();
$taskFiles = array();
$this->loadModel('file');
$task = fixer::input('post')
->add('project', (int)$projectID)
->setDefault('estimate, left, story', 0)
->setDefault('status', 'wait')
->setIF($this->post->estimate != false, 'left', $this->post->estimate)
->setIF($this->post->story != false, 'storyVersion', $this->loadModel('story')->getVersion($this->post->story))
->setDefault('estStarted', '0000-00-00')
->setDefault('deadline', '0000-00-00')
->setIF(strpos($this->config->task->create->requiredFields, 'estStarted') !== false, 'estStarted', $this->post->estStarted)
->setIF(strpos($this->config->task->create->requiredFields, 'deadline') !== false, 'deadline', $this->post->deadline)
->setDefault('openedBy', $this->app->user->account)
->setDefault('openedDate', helper::now())
->stripTags($this->config->task->editor->create['id'], $this->config->allowedTags)
->join('mailto', ',')
->remove('after,files,labels,assignedTo,uid,storyEstimate,storyDesc,storyPri,team,teamEstimate,teamMember,multiple,teams')
->get();
foreach($this->post->assignedTo as $assignedTo)
{
/* When type is affair and has assigned then ignore none. */
if($task->type == 'affair' and count($this->post->assignedTo) > 1 and empty($assignedTo)) continue;
$task->assignedTo = $assignedTo;
if($assignedTo) $task->assignedDate = helper::now();
/* Check duplicate task. */
if($task->type != 'affair')
{
$result = $this->loadModel('common')->removeDuplicate('task', $task, "project=$projectID and story=$task->story");
if($result['stop'])
{
$taskIdList[$assignedTo] = array('status' => 'exists', 'id' => $result['duplicate']);
continue;
}
}
$task = $this->file->processImgURL($task, $this->config->task->editor->create['id'], $this->post->uid);
$this->dao->insert(TABLE_TASK)->data($task)
->autoCheck()
->batchCheck($this->config->task->create->requiredFields, 'notempty')
->checkIF($task->estimate != '', 'estimate', 'float')
->checkIF($task->deadline != '0000-00-00', 'deadline', 'ge', $task->estStarted)
->exec();
if(dao::isError()) return false;
$taskID = $this->dao->lastInsertID();
- 该方法中先是对$task的一些字段进行了初始化,设置默认值等;
- 然后没有经历任何判定条件就执行了foreach($this->post->assignedTo as $assignedTo)的命令,对一个指定的数组做循环处理;
- 我们看一下这个数组是谁:$this->post->assignedTo
- 要知道的是,创建task的写库表的操作都是在这个数组循环中完成的,如果进入不了这个循环,则是完全没有机会能创建出禅道的项目task的;
- 到这里,问题基本上明确了,必须要在post中提交的表单参数,除了type, name外,还有一个assignedTo的参数,而它需要是一个数组变量!
- 在Postman中提交一个数组变量时,需要将变更名称定义为"assignedTo[]"的形式,至于该变量的取值倒不重要,可以保持为空,也可以指定为一个或多个禅道用户名;
7、使用var_dump()方法调试php
在上面步骤中主要是讲的结论,实际上整个结论都是在使用var_dump()反复调试禅道API接口后才得到的。经历了几十次的对比正常的页面表单提交和异常的api数据提交过程,所打印的变更信息。
var_dump()方法可以直接将php变量在保持原数据结构的条件下,打印在console上。
只需要在control.php文件的下面位置打印一下$tasksID的变量信息即可。
- $tasksID = $this->task->create($projectID);这一行是调用model中的方法创建出禅道task,返回值中除了task id的信息,还会包括了方法执行结果(created/existed)信息;
- var_dump($tasksID); 我们把这个变量打印出来,分别观察下通过页面表单提交时这个$tasksID的值,通过api提交数据时的$tasksID的值。
1)通过web页面表单创建task
- 在表单中我们只填写了type和name两个字段;
- 提交后,成功创建出了一个task任务,页面弹出的窗口中展示出了$tasksID变量信息和返回的响应结果。
- 从打印出来的信息可以看到,创建task的方法执行结果为"status"="created",task id为44.
2)通过禅道api创建一个task
- 上图中的array(0) { }就是我们所得到的$tasksID变量的取值,虽然返回的响应结果说result=success,但显然在调用model中的create方法时,没创建和返回有效的对象。
- 而上面创建task失败的原因就在于,model中create方法的foreach($this->post->assignedTo as $assignedTo)的处理逻辑上存在设计漏洞。必须跟表单同步提交的参数只有type, name,但在该方法中又在没做任何初始赋值处理或判定的条件下,直接假定$this->post->assignedTo是一个有效的数组变量来使用。这就直接导致我们在使用api提交数据时,如果没有提供assignedTo[]参数,或者是误把该参数理解为了是一个string类型的参数变量,则都会得到上图所示的显示已经成功提交,但却是无效的。
3)补充上正确的assignedTo[]变量后的正确提交姿势
也可以按需设置好把该任务指派给哪个用户:
上一篇: jira集群部署
下一篇: C项目(文件链表实现管理系统)