在禅道项目管理软件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

扫描二维码关注公众号,回复: 2715946 查看本文章

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工具。

https://www.getpostman.com/

安装好以后,按下图所示方法对指定的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[]变量后的正确提交姿势

也可以按需设置好把该任务指派给哪个用户:

猜你喜欢

转载自blog.csdn.net/watermelonbig/article/details/81203263