* @package webhook * @version $Id$ * @link https://www.zentao.net */ class webhookModel extends model { /** * 获取单个webhook。 * Get a webhook by id. * * @param int $id * @access public * @return object */ public function getByID(int $id) { return $this->dao->select('*')->from(TABLE_WEBHOOK)->where('id')->eq($id)->fetch(); } /** * 获取某种类型的webhook。 * Get a webhook by type. * * @param string $type * @param int $id * @access public * @return object */ public function getByType(string $type, int $id = 0) { return $this->dao->select('*')->from(TABLE_WEBHOOK)->where('type')->eq($type) ->andWhere('deleted')->eq('0') ->beginIF($id)->andWhere('id')->eq($id)->fi() ->fetch(); } /** * 获取已经绑定的用户。 * Get a webhook by type. * * @param int $webhookID * @param string $webhookType * @param string $openID * @access public * @return object */ public function getBindAccount(int $webhookID, string $webhookType, string $openID) { return $this->dao->select('account')->from(TABLE_OAUTH) ->where('providerID')->eq($webhookID) ->andWhere('providerType')->eq($webhookType) ->andWhere('openID')->eq($openID) ->fetch('account'); } /** * 获取webhook列表。 * Get webhook list. * * @param string $orderBy * @param object $pager * @access public * @return array */ public function getList(string $orderBy = 'id_desc', ?object $pager = null): array { return $this->dao->select('*,products,executions')->from(TABLE_WEBHOOK) ->where('deleted')->eq('0') ->orderBy($orderBy) ->page($pager) ->fetchAll('id', false); } /** * 获取日志列表。 * Get log list of a webhook. * * @param int $id * @param string $orderBy * @param object $pager * @access public * @return array */ public function getLogList(int $id, string $orderBy = 'date_desc', ?object $pager = null): array { $logs = $this->dao->select('*')->from(TABLE_LOG) ->where('objectType')->eq('webhook') ->andWhere('objectID')->eq($id) ->orderBy($orderBy) ->page($pager) ->fetchAll('id', false); $actions = array(); foreach($logs as $log) $actions[] = $log->action; $this->loadModel('action'); $actions = $this->dao->select('*')->from(TABLE_ACTION)->where('id')->in($actions)->fetchAll('id'); $users = $this->loadModel('user')->getPairs('noletter'); foreach($logs as $log) { if(!isset($actions[$log->action])) { $log->action = ''; continue; } $action = $actions[$log->action]; $data = json_decode($log->data); $object = $this->dao->select('*')->from($this->config->objectTables[$action->objectType])->where('id')->eq($action->objectID)->fetch(); $field = zget($this->config->action->objectNameFields, $action->objectType, $action->objectType); if(empty($data)) continue; if(!is_object($object)) { $object = new stdclass; $object->$field = ''; } $log->action = $this->webhookTao->getActionText($data, $action, $object, $users); $log->actionURL = $this->getViewLink($action->objectType, $action->objectID); $log->module = $action->objectType; $log->moduleID = $action->objectID; $log->dialog = $action->objectType == 'todo' ? 1 : 0; } return $logs; } /** * 获取通知信息。 * Get saved data list. * * @access public * @return array */ public function getDataList() { $now = helper::now(); $dataList = $this->dao->select('*')->from(TABLE_NOTIFY) ->where('status')->eq('wait') ->andWhere('objectType')->eq('webhook') ->andWhere('(sendTime IS NULL OR sendTime <= "' . $now . '")') ->orderBy('id') ->fetchAll('id', false); $threeHoursAgo = date('Y-m-d H:i:s', time() - 3 * 3600); $dataList += $this->dao->select('*')->from(TABLE_NOTIFY) ->where('status')->eq('senting') ->andWhere('objectType')->eq('webhook') ->andWhere('(sendTime IS NULL OR (sendTime <= "' . $now . '" AND sendTime >= "' . $threeHoursAgo . '"))') ->orderBy('id') ->fetchAll('id', false); return $dataList; } /** * 获取绑定用户。 * Get bind users. * * @param int $webhookID * @param array|string $users * @access public * @return array */ public function getBoundUsers(int $webhookID, array|string $users = array()) { return $this->dao->select('*')->from(TABLE_OAUTH)->where('providerType')->eq('webhook') ->andWhere('providerID')->eq($webhookID) ->beginIF($users)->andWhere('account')->in($users)->fi() ->fetchPairs('account', 'openID'); } /** * 创建webhook。 * Create a webhook. * * @param object $webhook * * @access public * @return int|false */ public function create(object $webhook): int|false { $webhook->createdBy = $this->app->user->account; $webhook->createdDate = helper::now(); $webhook->domain = trim($webhook->domain, '/'); $webhook->params = $this->post->params ? implode(',', $this->post->params) . ',text' : 'text'; if($webhook->type == 'dinguser') { $webhook = $this->webhookTao->getDingdingSecret($webhook); } elseif($webhook->type == 'wechatuser') { $webhook = $this->webhookTao->getWeixinSecret($webhook); } elseif($webhook->type == 'feishuuser') { $webhook = $this->webhookTao->getFeishuSecret($webhook); } if(!empty($webhook->url) && !preg_match('/^http(s)?:\/\//', $webhook->url)) dao::$errors['url'] = $this->lang->webhook->error->url; if(dao::isError()) return false; $this->dao->insert(TABLE_WEBHOOK)->data($webhook, 'agentId,appKey,appSecret,wechatCorpId,wechatCorpSecret,wechatAgentId,feishuAppId,feishuAppSecret') ->batchCheck($this->config->webhook->create->requiredFields, 'notempty') ->autoCheck() ->exec(); return $this->dao->lastInsertID(); } /** * 编辑webhook。 * Update a webhook. * * @param int $id * @param object $webhook * @access public * @return bool */ public function update(int $id, object $webhook): bool { $webhook->editedBy = $this->app->user->account; $webhook->editedDate = helper::now(); $webhook->domain = trim($webhook->domain, '/'); $webhook->params = $this->post->params ? implode(',', $this->post->params) : 'text'; if(!str_contains($webhook->params, 'text')) $webhook->params .= ',text'; if($webhook->type == 'dinguser') { $webhook = $this->webhookTao->getDingdingSecret($webhook); } elseif($webhook->type == 'wechatuser') { $webhook = $this->webhookTao->getWeixinSecret($webhook); } elseif($webhook->type == 'feishuuser') { $webhook = $this->webhookTao->getFeishuSecret($webhook); } if(!empty($webhook->url) && !preg_match('/^http(s)?:\/\//', $webhook->url)) dao::$errors['url'] = $this->lang->webhook->error->url; if(dao::isError()) return false; $this->dao->update(TABLE_WEBHOOK)->data($webhook, 'agentId,appKey,appSecret,wechatCorpId,wechatCorpSecret,wechatAgentId,feishuAppId,feishuAppSecret') ->batchCheck($this->config->webhook->edit->requiredFields, 'notempty') ->autoCheck() ->where('id')->eq($id) ->exec(); return !dao::isError(); } /** * 绑定钉钉、飞书、企业微信的用户到禅道。 * Bind users of dingding, feishu, weixin to zentao. * * @param int $webhookID * @access public * @return bool */ public function bind(int $webhookID): bool { $userList = $this->post->userid; if(!$userList) return false; $this->dao->delete()->from(TABLE_OAUTH) ->where('providerType')->eq('webhook') ->andWhere('providerID')->eq($webhookID) ->andWhere('account')->in(array_keys($userList)) ->exec(); foreach($userList as $account => $userid) { if(empty($userid)) continue; $oauth = new stdclass(); $oauth->account = $account; $oauth->openID = $userid; $oauth->providerType = 'webhook'; $oauth->providerID = $webhookID; $this->dao->insert(TABLE_OAUTH)->data($oauth)->exec(); } return !dao::isError(); } /** * 发送数据。 * Send data. * * @param string $objectType * @param int $objectID * @param string $actionType * @param int $actionID * @param string $actor * @access public * @return bool */ public function send(string $objectType, int $objectID, string $actionType, int $actionID, string $actor = ''): bool { static $webhooks = array(); if(!$webhooks) $webhooks = $this->getList(); if(!$webhooks) return true; /* 如果对象类型是瀑布,动作是提交审计或者审计,那么对象类型就是审批。*/ if($objectType == 'waterfall' && strpos(',toaudit,audited,', ",{$actionType},") !== false) $objectType = 'review'; $aitaskObject = null; if($objectType == 'aitask' && in_array($actionType, array('finished', 'failed'))) { $aitaskObject = $this->dao->select('*')->from(TABLE_AI_TASK)->where('id')->eq($objectID)->fetch(); } foreach($webhooks as $id => $webhook) { $postData = $this->buildData($objectType, $objectID, $actionType, $actionID, $webhook); if(!$postData) continue; $needDelay = false; if($aitaskObject && !empty($aitaskObject->noticeTime) && $aitaskObject->noticeTime != '1') { $needDelay = true; } if($webhook->sendType == 'async' || $needDelay) { if($webhook->type == 'dinguser') { $openIdList = $this->getOpenIdList($webhook->id, $actionID); if(empty($openIdList)) continue; } $this->saveData($id, $actionID, $postData, $actor, $aitaskObject); continue; } $result = $this->fetchHook($webhook, $postData, $actionID); if(!empty($result)) $this->saveLog($webhook, $actionID, $postData, (string)$result); } return !dao::isError(); } /** * Build data. * * @param string $objectType * @param int $objectID * @param string $actionType * @param int $actionID * @param object $webhook * @access public * @return string */ public function buildData(string $objectType, int $objectID, string $actionType, int $actionID, object $webhook) { /* Validate data. */ if(!isset($this->lang->action->label)) $this->loadModel('action'); if(!isset($this->lang->action->label->$actionType)) return false; if(empty($this->config->objectTables[$objectType])) return false; $action = $this->dao->select('*')->from(TABLE_ACTION)->where('id')->eq($actionID)->fetch(); if(!$action) return false; if($webhook->products) { $webhookProducts = explode(',', trim($webhook->products, ',')); $actionProduct = explode(',', trim($action->product, ',')); $intersect = array_intersect($webhookProducts, $actionProduct); if(!$intersect) return false; } if($webhook->executions && strpos(",$webhook->executions,", ",$action->execution,") === false) return false; static $users = array(); if(empty($users)) $users = $this->loadModel('user')->getList(); $object = $this->dao->select('*')->from($this->config->objectTables[$objectType])->where('id')->eq($objectID)->fetch(); if(!$object) return false; $host = empty($webhook->domain) ? common::getSysURL() : $webhook->domain; $viewLink = $this->getViewLink($objectType == 'kanbancard' ? 'kanban' : $objectType, $objectType == 'kanbancard' ? $object->kanban : $objectID); if($objectType == 'aitask' && in_array($actionType, array('finished', 'failed'))) { $text = $this->loadModel('aitask')->getNotificationText($object, $objectID, $actionType, 'markdown', '', $viewLink, $host); $title = $text; } else { $field = $this->config->action->objectNameFields[$objectType]; $objectTypeName = ($objectType == 'story' and $object->type == 'requirement') ? $this->lang->action->objectTypes['requirement'] : $this->lang->action->objectTypes[$objectType]; $title = $this->app->user->realname . $this->lang->action->label->$actionType . $objectTypeName; $host = (defined('RUN_MODE') and RUN_MODE == 'api') ? '' : $host; $text = $title . ' ' . "[#{$objectID}::{$object->$field}](" . $host . $viewLink . ")"; } $action->text = $text; $mobile = ''; $email = ''; if(in_array($objectType, $this->config->webhook->needAssignTypes) && !empty($object->assignedTo)) { $assignedTo = $this->loadModel('user')->getById($object->assignedTo); if($assignedTo) { $mobile = $assignedTo->mobile; $email = $assignedTo->email; } } return $this->getDataByType($webhook, $action, $title, $text, $mobile, $email, $objectType, $objectID); } /** * 根据webhook类型获取数据。 * Get data by type. * * @param object $webhook * @param object $action * @param string $title * @param string $text * @param string $mobile * @param string $email * @param string $objectType * @param int $objectID * @access public * @return string */ public function getDataByType(object $webhook, object $action, string $title, string $text, string $mobile, string $email, string $objectType, int $objectID): string { if($webhook->type == 'dinggroup' or $webhook->type == 'dinguser') { $data = $this->getDingdingData($title, $text, $webhook->type == 'dinguser' ? '' : $mobile); } elseif($webhook->type == 'bearychat') { $data = $this->getBearychatData($text, $mobile, $email, $objectType, $objectID); } elseif($webhook->type == 'wechatgroup' or $webhook->type == 'wechatuser') { $data = $this->getWeixinData($text, $mobile); } elseif($webhook->type == 'feishuuser' or $webhook->type == 'feishugroup') { $data = $this->getFeishuData($title, $text); } else { $data = new stdclass(); foreach(explode(',', $webhook->params) as $param) $data->$param = $action->$param; } return json_encode($data); } /** * 获取对象详情链接。 * Get view link. * * @param string $objectType * @param int $objectID * @access public * @return string */ public function getViewLink(string $objectType, int $objectID): string { $oldOnlyBody = ''; $tab = ''; if(isset($_GET['onlybody']) and $_GET['onlybody'] == 'yes') { $oldOnlyBody = 'yes'; unset($_GET['onlybody']); } if($objectType == 'case') $objectType = 'testcase'; if($objectType == 'kanbancard') { $objectType = 'kanban'; $objectID = $this->dao->select('kanban')->from(TABLE_KANBANCARD)->where('id')->eq($objectID)->fetch('kanban'); } if($objectType == 'meeting') { $meeting = $this->dao->findById($objectID)->from(TABLE_MEETING)->fetch(); $tab = $meeting->project ? '#app=project' : '#app=my'; } if($objectType == 'aitask') { $viewLink = helper::createLink('aitask', 'view', "taskID={$objectID}", 'html') . $tab; if($oldOnlyBody) $_GET['onlybody'] = $oldOnlyBody; return $viewLink; } $viewLink = helper::createLink($objectType, 'view', "id=$objectID", 'html') . $tab; if($oldOnlyBody) $_GET['onlybody'] = $oldOnlyBody; return $viewLink; } /** * 获取发送钉钉的数据。 * Get hook data for dingding. * * @param string $title * @param string $text * @param string $mobile * @access public * @return object */ public function getDingdingData(string $title, string $text, string $mobile): object { if($mobile) $text .= " @{$mobile}"; $markdown = new stdclass(); $markdown->title = $title; $markdown->text = $text; $data = new stdclass(); $data->msgtype = 'markdown'; $data->markdown = $markdown; if($mobile) { $at = new stdclass(); $at->atMobiles = array($mobile); $at->isAtAll = false; $data->at = $at; } return $data; } /** * 获取发送bearychat的数据。 * Get hook data for bearychat. * * @param string $text * @param string $mobile * @param string $email * @param string $objectType * @param int $objectID * @access public * @return object */ public function getBearychatData(string $text, string $mobile, string $email, string $objectType, int $objectID): object { $data = new stdclass(); $data->text = $text; $data->markdown = 'true'; if($mobile) { $data->user = $mobile; } elseif($email) { $data->user = $email; } else { $data->user = $this->app->user->account; } if(!empty($_FILES['files']['name'][0])) { $this->loadModel('file'); $files = $this->dao->select('*')->from(TABLE_FILE)->where('objectType')->eq($objectType)->andWhere('objectID')->eq($objectID)->orderBy('addedDate_desc,id_desc')->limit(count($_FILES['files']['name']))->fetchAll(); if($files) { foreach($files as $file) { $attachment = array(); $attachment['title'] = $file->title; $attachment['images'][]['url'] = common::getSysURL() . $this->file->webPath . $file->pathname; $data->attachments[] = $attachment; } } } return $data; } /** * 获取发送微信的数据。 * Get weixin send data. * * @param string $text * @param string $mobile * @access public * @return object */ public function getWeixinData(string $text, string $mobile = ''): object { $data = new stdclass(); $data->msgtype = 'markdown'; $markdown = new stdclass(); $markdown->content = $text; if($mobile) { $data->msgtype = 'text'; $markdown->mentioned_mobile_list = array($mobile); } $data->{$data->msgtype} = $markdown; return $data; } /** * 获取发送飞书的数据。 * Get feishu send data. * * @param string $title * @param string $text * @access public * @return object */ public function getFeishuData(string $title, string $text): object { $data = new stdclass(); $data->msg_type = 'interactive'; $data->card = array(); $data->card['header'] = array(); $data->card['elements'] = array(); $data->card['elements'][] = array('tag' => 'markdown', 'content' => $text); $data->card['header']['title'] = array('tag' => 'plain_text', 'content' => (string)$title); $data->card['header']['template'] = 'blue'; return $data; } /** * 获取openID列表。 * Get openID list. * * @param int $webhookID * @param int $actionID * @param string|array $toList * @access public * @return string */ public function getOpenIdList(int $webhookID, int $actionID, string|array $toList = ''): string { if($toList) { $openIdList = $this->getBoundUsers($webhookID, $toList); $openIdList = implode(',', $openIdList); return $openIdList; } if(empty($actionID)) return false; $action = $this->dao->select('*')->from(TABLE_ACTION)->where('id')->eq($actionID)->fetch(); $table = zget($this->config->objectTables, $action->objectType, ''); if(empty($table)) return false; $object = $this->dao->select('*')->from($table)->where('id')->eq($action->objectID)->fetch(); $toList = $this->loadModel('message')->getToList((object)$object, $action->objectType, $actionID); if(!empty($object->mailto)) $toList .= ',' . $object->mailto; if(empty($toList)) return false; /* Remove duplicate people. */ if($action->objectType == 'release') { $toList = array_unique(explode(',', $toList)); $toList = implode(',', $toList); } $toList = str_replace(",{$this->app->user->account},", ',', ",$toList,"); $openIdList = $this->getBoundUsers($webhookID, $toList); $openIdList = implode(',', $openIdList); return $openIdList; } /** * 请求发送数据的接口。 * Post hook data. * * @param object $webhook * @param string $sendData * @param int $actionID * @param string|array $appendUser * @access public * @return int */ public function fetchHook(object $webhook, string $sendData, int $actionID = 0, string|array $appendUser = '') { if(!extension_loaded('curl')) return print(helper::jsonEncode($this->lang->webhook->error->curl)); if(in_array($webhook->type, array('dinguser', 'wechatuser', 'feishuuser'))) return $this->sendToUser($webhook, $sendData, $actionID, $appendUser); $contentType = "Content-Type: {$webhook->contentType};charset=utf-8"; if($webhook->type == 'dinggroup' or $webhook->type == 'wechatgroup' or $webhook->type == 'feishugroup') $contentType = "Content-Type: application/json"; $header[] = $contentType; $url = $webhook->url; if($webhook->type == 'dinggroup' and $webhook->secret) { $timestamp = time() * 1000; $sign = $timestamp . "\n" . $webhook->secret; $sign = urlencode(base64_encode(hash_hmac('sha256', $sign, $webhook->secret, true))); $url .= "×tamp={$timestamp}&sign={$sign}"; } if($webhook->type == 'feishugroup' and $webhook->secret) { $timestamp = time(); $sign = $timestamp . "\n" . $webhook->secret; $sign = base64_encode(hash_hmac('sha256', '', $sign, true)); $content = json_decode($sendData); $content->timestamp = $timestamp; $content->sign = $sign; $sendData = json_encode($content); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30); curl_setopt($ch, CURLOPT_TIMEOUT, 30); curl_setopt($ch, CURLOPT_NOSIGNAL, 1); curl_setopt($ch, CURLOPT_HTTPHEADER, $header); curl_setopt($ch, CURLOPT_POSTFIELDS, $sendData); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); $error = curl_error($ch); curl_close($ch); if($error) return $error; if($result) return $result; return $httpCode; } /** * 发送消息到钉钉、飞书、企业微信的用户。 * Send to dingding, feishu, weixin users. * * @param object $webhook * @param string $sendData * @param int $actionID * @param string|array $appendUser * @access public * @return string|false */ public function sendToUser(object $webhook, string $sendData, int $actionID, string|array $appendUser): string|false { if(is_string($webhook->secret)) $webhook->secret = json_decode($webhook->secret); $openIdList = $this->getOpenIdList($webhook->id, $actionID, $appendUser); if(empty($openIdList)) return false; if($webhook->type == 'dinguser') { $this->app->loadClass('dingapi', true); $dingapi = new dingapi($webhook->secret->appKey, $webhook->secret->appSecret, $webhook->secret->agentId); $result = $dingapi->send($openIdList, $sendData); return json_encode($result); } elseif($webhook->type == 'wechatuser') { $this->app->loadClass('wechatapi', true); $wechatapi = new wechatapi($webhook->secret->appKey, $webhook->secret->appSecret, $webhook->secret->agentId); $result = $wechatapi->send($openIdList, $sendData); return json_encode($result); } elseif($webhook->type == 'feishuuser') { $this->app->loadClass('feishuapi', true); $feishuapi = new feishuapi($webhook->secret->appId, $webhook->secret->appSecret); $result = $feishuapi->send($openIdList, $sendData); return json_encode($result); } } /** * 保存发送的通知。 * Save datas. * * @param int $webhookID * @param int $actionID * @param string $data * @param string $actor * @access public * @return bool */ public function saveData(int $webhookID, int $actionID, string $data, string $actor = '', object $aitaskObject = null): bool { if(empty($actor)) $actor = $this->app->user->account; $sendTime = helper::now(); $object = $aitaskObject; if(!$object) { $action = $this->loadModel('action')->getById($actionID); if($action && $action->objectType == 'aitask' && in_array($action->action, array('finished', 'failed'))) { $object = $this->dao->select('*')->from(TABLE_AI_TASK)->where('id')->eq($action->objectID)->fetch(); } } if($object && !empty($object->noticeTime) && $object->noticeTime != '1') { $today = date('Y-m-d'); $targetTime = $today . ' ' . $object->noticeTime . ':00'; $targetTimestamp = strtotime($targetTime); $nowTimestamp = time(); $sendTime = $targetTimestamp < $nowTimestamp ? helper::now() : $targetTime; } $webhookData = new stdclass(); $webhookData->objectType = 'webhook'; $webhookData->objectID = $webhookID; $webhookData->action = $actionID; $webhookData->data = $data; $webhookData->status = 'wait'; $webhookData->createdBy = $actor; $webhookData->createdDate = helper::now(); $webhookData->sendTime = $sendTime; $this->dao->insert(TABLE_NOTIFY)->data($webhookData)->exec(); return !dao::isError(); } /** * 保存发送日志。 * Save log. * * @param object $webhook * @param int $actionID * @param string $data * @param string|int $result * @access public * @return bool */ public function saveLog(object $webhook, int $actionID, string $data, string|int $result): bool { $log = new stdclass(); $log->objectType = 'webhook'; $log->objectID = $webhook->id; $log->action = $actionID; $log->date = helper::now(); $log->url = $webhook->url; $log->contentType = $webhook->contentType; $log->data = $data; $log->result = (string)$result; $this->dao->insert(TABLE_LOG)->data($log)->exec(); return !dao::isError(); } /** * 设置消息发送状态。 * Set sent status. * * @param array|int|string $idList * @param string $status * @param string $time * @access public * @return void */ public function setSentStatus(array|int|string $idList, string $status, string $time = '') { $this->dao->update(TABLE_NOTIFY) ->set('status')->eq($status) ->beginIF(!empty($time))->set('sendTime')->eq($time)->fi() ->where('id')->in($idList) ->exec(); } }