欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法

程序员文章站 2022-07-14 08:26:51
...

禅道是一款国产的不错的项目管理软件,有开源和商业版本,软件做得挺不错的,如果大家所在的公司不差这些小钱,建议使用禅道商业版,也算是对优秀软件的一点支持吧。

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,分别定义了任务类型和任务名称。

在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法

3、使用Postman调试api接口

我们需要使用Postman来模拟POST方式提交数据。从以下地址下载和安装postman工具。

https://www.getpostman.com/

安装好以后,按下图所示方法对指定的api接口地址进行测试。需要填写URL地址,选择提交方式,选择提交数据的编码格式,定义好必要的表单参数和数据。

在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法

点击“Send”按钮后,得到返回结果,"Body"数据显示结果是success的,"locate"则是一个页面重定向信息。

我们所要讨论的API接口bug,也就是在这里。因为上面操作是成功的,实际上禅道中指定项目-模块下并没有创建出我们的任务来。

如果不介意的话,上面Postman中的"Send"按钮,多点几次也无妨,因为每次都会返回一个"success"的结果回来。所有的提交尝试都是无效的。

与此同时,直接在禅道web页面上通过页面表单,仅填写type, name字段,是可以成功提交和创建出任务来的。

下面就依据分析和定准错误的过程做一下说明。

4、观察和分析创建禅道任务的表单参数

在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法

如上图所示,直接把创建任务的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上分析抓包文件,关键信息如下图所示

在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法

  • 先找到POST操作的数据包
  • 再检查MIME数据内容,可以分辨出通过表单提交的各个属性字段和取值
  • 重点观察下上图中展示出的name="assignedTo[]"

因为wireshark中能看到的数据包内容不是很友好,所以虽然抓包时已经看到了assignedTo[]的信息,但被上一步骤中的json数据参数误导了,以为上图中的意思是assignedTo参数,取值为[],一个空数组。

实际情况是:表单参数名是assignedTo[],取值为空。这个就是最终水落石出时才发现的了。

6、阅读和分析禅道php源码

大部分WEB开发框架,不论是什么编程语言开发的,都会遵循MVC的一个设计逻辑。所以我们可以看到禅道项目源码目录如下图所示,主要实现逻辑就都在model.php中了,控制逻辑在control.php中。

在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法

我们只截取一点关键部分的代码做分析。

下面是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的变量信息即可。

在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法

  • $tasksID = $this->task->create($projectID);这一行是调用model中的方法创建出禅道task,返回值中除了task id的信息,还会包括了方法执行结果(created/existed)信息;
  • var_dump($tasksID); 我们把这个变量打印出来,分别观察下通过页面表单提交时这个$tasksID的值,通过api提交数据时的$tasksID的值。

1)通过web页面表单创建task

在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法

  • 在表单中我们只填写了type和name两个字段;
  • 提交后,成功创建出了一个task任务,页面弹出的窗口中展示出了$tasksID变量信息和返回的响应结果。
  • 从打印出来的信息可以看到,创建task的方法执行结果为"status"="created",task id为44. 

2)通过禅道api创建一个task

在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法

 

  • 上图中的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[]变量后的正确提交姿势

在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法

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

 

在禅道项目管理软件v9.8.3的一个API调用bug的排错中使用到的一些工具和分析方法