* @package webhook * @version $Id$ * @link http://www.zentao.net */ class webhookModel extends model { /** * Get a webhook by id. * * @param int $id * @access public * @return object */ public function getByID($id) { $webhook = $this->dao->select('*')->from(TABLE_WEBHOOK)->where('id')->eq($id)->fetch(); return $webhook; } /** * Get a webhook by type. * * @param int $type * @access public * @return object */ public function getByType($type) { $webhook = $this->dao->select('*')->from(TABLE_WEBHOOK)->where('type')->eq($type)->andWhere('deleted')->eq('0')->fetch(); return $webhook; } /** * Get a webhook by type. * * @param int $type * @access public * @return object */ public function getBindAccount($webhookID, $webhookType, $openID) { $account = $this->dao->select('account')->from(TABLE_OAUTH)->where('providerID')->eq($webhookID)->andWhere('providerType')->eq($webhookType)->andWhere('openID')->eq($openID)->fetch('account'); return $account; } /** * Get webhook list. * * @param string $orderBy * @param object $pager * @param bool $decode * @access public * @return array */ public function getList($orderBy = 'id_desc', $pager = null, $decode = true) { $webhooks = $this->dao->select('*')->from(TABLE_WEBHOOK) ->where('deleted')->eq('0') ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); return $webhooks; } /** * Get log list of a webhook. * * @param int $id * @param string $orderBy * @param object $pager * @access public * @return array */ public function getLogList($id, $orderBy = 'date_desc', $pager = null) { $logs = $this->dao->select('*')->from(TABLE_LOG) ->where('objectType')->eq('webhook') ->andWhere('objectID')->eq($id) ->orderBy($orderBy) ->page($pager) ->fetchAll('id'); $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(!is_object($object)) { $object = new stdclass; $object->$field = ''; } $text = ''; if(isset($data->markdown->text)) { $text = substr($data->markdown->text, 0, strpos($data->markdown->text, '(http')); } elseif(isset($data->markdown->content)) { $text = substr($data->markdown->content, 0, strpos($data->markdown->content, '(http')); } elseif(isset($data->text->content)) { $text = substr($data->text->content, 0, strpos($data->text->content, '(http')); } elseif(isset($data->content)) { $text = $data->content->text; $text = substr($text, 0, strpos($text, '(http')) ? substr($text, 0, strpos($text, '(http')) : zget($users, $data->user, $this->app->user->realname) . $this->lang->action->label->{$action->action} . $this->lang->action->objectTypes[$action->objectType] . "[#{$action->objectID}::{$object->$field}]"; } else { $text = substr($data->text, 0, strpos($data->text, '(http')) ? substr($data->text, 0, strpos($data->text, '(http')) : zget($users, $data->user, $this->app->user->realname) . $this->lang->action->label->{$action->action} . $this->lang->action->objectTypes[$action->objectType] . "[#{$action->objectID}::{$object->$field}]"; } $log->action = $text; $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() { $dataList = $this->dao->select('*')->from(TABLE_NOTIFY)->where('status')->eq('wait')->andWhere('objectType')->eq('webhook')->orderBy('id')->fetchAll('id'); $dataList += $this->dao->select('*')->from(TABLE_NOTIFY)->where('status')->eq('senting')->andWhere('sendTime')->ge(date('Y-m-d H:i:s', time() - 3 * 3600))->andWhere('objectType')->eq('webhook')->orderBy('id')->fetchAll('id'); return $dataList; } /** * Get bind users. * * @param int $webhookID * @param array $users * @access public * @return array */ public function getBoundUsers($webhookID, $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'); } /** * Create a webhook. * * @access public * @return bool */ public function create() { $webhook = fixer::input('post') ->add('createdBy', $this->app->user->account) ->add('createdDate', helper::now()) ->join('products', ',') ->join('executions', ',') ->skipSpecial('url') ->trim('agentId,appKey,appSecret,wechatAgentId,wechatCorpId,wechatCorpSecret,feishuAppId,feishuAppSecret') ->remove('allParams, allActions') ->get(); $webhook->domain = trim($webhook->domain, '/'); $webhook->params = $this->post->params ? implode(',', $this->post->params) . ',text' : 'text'; if($webhook->type == 'dinguser') { $webhook->secret = array(); $webhook->secret['agentId'] = $webhook->agentId; $webhook->secret['appKey'] = $webhook->appKey; $webhook->secret['appSecret'] = $webhook->appSecret; if(empty($webhook->agentId)) dao::$errors['agentId'] = sprintf($this->lang->error->notempty, $this->lang->webhook->dingAgentId); if(empty($webhook->appKey)) dao::$errors['appKey'] = sprintf($this->lang->error->notempty, $this->lang->webhook->dingAppKey); if(empty($webhook->appSecret)) dao::$errors['appSecret'] = sprintf($this->lang->error->notempty, $this->lang->webhook->dingAppSecret); if(dao::isError()) return false; $webhook->secret = json_encode($webhook->secret); $webhook->url = $this->config->webhook->dingapiUrl; } elseif($webhook->type == 'wechatuser') { $webhook->secret = array(); $webhook->secret['agentId'] = $webhook->wechatAgentId; $webhook->secret['appKey'] = $webhook->wechatCorpId; $webhook->secret['appSecret'] = $webhook->wechatCorpSecret; if(empty($webhook->wechatCorpId)) dao::$errors['wechatCorpId'] = sprintf($this->lang->error->notempty, $this->lang->webhook->wechatCorpId); if(empty($webhook->wechatCorpSecret)) dao::$errors['wechatCorpSecret'] = sprintf($this->lang->error->notempty, $this->lang->webhook->wechatCorpSecret); if(empty($webhook->wechatAgentId)) dao::$errors['wechatAgentId'] = sprintf($this->lang->error->notempty, $this->lang->webhook->wechatAgentId); if(dao::isError()) return false; $webhook->secret = json_encode($webhook->secret); $webhook->url = $this->config->webhook->wechatApiUrl; } elseif($webhook->type == 'feishuuser') { $webhook->secret = array(); $webhook->secret['appId'] = $webhook->feishuAppId; $webhook->secret['appSecret'] = $webhook->feishuAppSecret; if(empty($webhook->feishuAppId)) dao::$errors['feishuAppId'] = sprintf($this->lang->error->notempty, $this->lang->webhook->feishuAppId); if(empty($webhook->feishuAppSecret)) dao::$errors['feishuAppSecret'] = sprintf($this->lang->error->notempty, $this->lang->webhook->feishuAppSecret); if(dao::isError()) return false; $webhook->secret = json_encode($webhook->secret); $webhook->url = $this->config->webhook->feishuApiUrl; } $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(); } /** * Update a webhook. * * @param int $id * @access public * @return bool */ public function update($id) { $webhook = fixer::input('post') ->add('editedBy', $this->app->user->account) ->add('editedDate', helper::now()) ->setDefault('products', '') ->setDefault('executions', '') ->join('products', ',') ->join('executions', ',') ->skipSpecial('url') ->trim('agentId,appKey,appSecret,wechatAgentId,wechatCorpId,wechatCorpSecret,feishuAppId,feishuAppSecret') ->remove('allParams, allActions') ->get(); $webhook->domain = trim($webhook->domain, '/'); $webhook->params = $this->post->params ? implode(',', $this->post->params) . ',text' : 'text'; if($webhook->type == 'dinguser') { $webhook->secret = array(); $webhook->secret['agentId'] = $webhook->agentId; $webhook->secret['appKey'] = $webhook->appKey; $webhook->secret['appSecret'] = $webhook->appSecret; if(empty($webhook->agentId)) dao::$errors['agentId'] = sprintf($this->lang->error->notempty, $this->lang->webhook->dingAgentId); if(empty($webhook->appKey)) dao::$errors['appKey'] = sprintf($this->lang->error->notempty, $this->lang->webhook->dingAppKey); if(empty($webhook->appSecret)) dao::$errors['appSecret'] = sprintf($this->lang->error->notempty, $this->lang->webhook->dingAppSecret); if(dao::isError()) return false; $webhook->secret = json_encode($webhook->secret); } elseif($webhook->type == 'wechatuser') { $webhook->secret = array(); $webhook->secret['agentId'] = $webhook->wechatAgentId; $webhook->secret['appKey'] = $webhook->wechatCorpId; $webhook->secret['appSecret'] = $webhook->wechatCorpSecret; if(empty($webhook->wechatCorpId)) dao::$errors['wechatCorpId'] = sprintf($this->lang->error->notempty, $this->lang->webhook->wechatCorpId); if(empty($webhook->wechatCorpSecret)) dao::$errors['wechatCorpSecret'] = sprintf($this->lang->error->notempty, $this->lang->webhook->wechatCorpSecret); if(empty($webhook->wechatAgentId)) dao::$errors['wechatAgentId'] = sprintf($this->lang->error->notempty, $this->lang->webhook->wechatAgentId); if(dao::isError()) return false; $webhook->secret = json_encode($webhook->secret); } elseif($webhook->type == 'feishuuser') { $webhook->secret = array(); $webhook->secret['appId'] = $webhook->feishuAppId; $webhook->secret['appSecret'] = $webhook->feishuAppSecret; if(empty($webhook->feishuAppId)) dao::$errors['feishuAppId'] = sprintf($this->lang->error->notempty, $this->lang->webhook->feishuAppId); if(empty($webhook->feishuAppSecret)) dao::$errors['feishuAppSecret'] = sprintf($this->lang->error->notempty, $this->lang->webhook->feishuAppSecret); if(dao::isError()) return false; $webhook->secret = json_encode($webhook->secret); } $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 ding openID. * * @param int $webhookID * @access public * @return bool */ public function bind($webhookID) { $data = fixer::input('post')->get(); $this->dao->delete()->from(TABLE_OAUTH) ->where('providerType')->eq('webhook') ->andWhere('providerID')->eq($webhookID) ->andWhere('account')->in(array_keys($data->userid)) ->exec(); foreach($data->userid 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 * @access public * @return bool */ public function send($objectType, $objectID, $actionType, $actionID, $actor = '') { static $webhooks = array(); if(!$webhooks) $webhooks = $this->getList(); if(!$webhooks) return true; foreach($webhooks as $id => $webhook) { $postData = $this->buildData($objectType, $objectID, $actionType, $actionID, $webhook); if(!$postData) continue; if($webhook->sendType == 'async') { if($webhook->type == 'dinguser') { $openIdList = $this->getOpenIdList($webhook->id, $actionID); if(empty($openIdList)) continue; } $this->saveData($id, $actionID, $postData, $actor); continue; } $result = $this->fetchHook($webhook, $postData, $actionID); if(!empty($result)) $this->saveLog($webhook, $actionID, $postData, $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($objectType, $objectID, $actionType, $actionID, $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($webhook->products) { $webhookProducts = explode(',', trim($webhook->products, ',')); $actionProduct = explode(',', trim($action->product, ',')); $intersect = array_intersect($webhookProducts, $actionProduct); if(!$intersect) return false; } if($webhook->executions) { if(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(); $field = $this->config->action->objectNameFields[$objectType]; $host = empty($webhook->domain) ? common::getSysURL() : $webhook->domain; $viewLink = $this->getViewLink($objectType == 'kanbancard' ? 'kanban' : $objectType, $objectType == 'kanbancard' ? $object->kanban : $objectID); $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 . ")"; $mobile = ''; $email = ''; if(in_array($objectType, $this->config->webhook->needAssignTypes) && !empty($object->assignedTo)) { foreach($users as $user) { if($user->account == $object->assignedTo) { $mobile = $user->mobile; $email = $user->email; break; } } } $action->text = $text; 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($title, $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($objectType, $objectID) { $oldOnlyBody = ''; $tab = ''; if(isset($_GET['onlybody']) and $_GET['onlybody'] == 'yes') { $oldOnlyBody = 'yes'; unset($_GET['onlybody']); } if($objectType == 'case') $objectType = 'testcase'; if($objectType == 'meeting') { $meeting = $this->dao->findById($objectID)->from(TABLE_MEETING)->fetch(); $tab = $meeting->project ? '#app=project' : '#app=my'; } $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($title, $text, $mobile) { 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; } /** * 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($text, $mobile, $email, $objectType, $objectID) { $data = new stdclass(); $data->text = $text; $data->markdown = 'true'; $data->user = $mobile ? $mobile : ($email ? $email : $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 $title * @param string $text * @param string $mobile * @access public * @return object */ public function getWeixinData($title, $text, $mobile = '') { $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($title, $text) { $data = new stdclass(); $data->msg_type = 'text'; $data->content['text'] = $text; return $data; } /** * Get openID list. * * @param int $actionID * @access public * @return string */ public function getOpenIdList($webhookID, $actionID) { 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, $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 = join(',', $openIdList); return $openIdList; } /** * Post hook data. * * @param object $webhook * @param string $sendData * @param int $actionID * @access public * @return int */ public function fetchHook($webhook, $sendData, $actionID = 0) { if(!extension_loaded('curl')) return print(helper::jsonEncode($this->lang->webhook->error->curl)); if($webhook->type == 'dinguser' || $webhook->type == 'wechatuser' || $webhook->type == 'feishuuser') { if(is_string($webhook->secret)) $webhook->secret = json_decode($webhook->secret); $openIdList = $this->getOpenIdList($webhook->id, $actionID); 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); } } $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; } /** * Save datas. * * @param int $webhookID * @param int $actionID * @param string $data * @access public * @return bool */ public function saveData($webhookID, $actionID, $data, $actor = '') { if(empty($actor)) $actor = $this->app->user->account; $webhookData = new stdclass(); $webhookData->objectType = 'webhook'; $webhookData->objectID = $webhookID; $webhookData->action = $actionID; $webhookData->data = $data; $webhookData->createdBy = $actor; $webhookData->createdDate = helper::now(); $this->dao->insert(TABLE_NOTIFY)->data($webhookData)->exec(); return !dao::isError(); } /** * Save log. * * @param object $webhook * @param int $actionID * @param string $data * @param string $result * @access public * @return bool */ public function saveLog($webhook, $actionID, $data, $result) { $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 = $result; $this->dao->insert(TABLE_LOG)->data($log)->exec(); return !dao::isError(); } /** * Set sent status. * * @param array $idList * @param string $status * @param string $time * @access public * @return void */ public function setSentStatus($idList, $status, $time = '') { if(empty($time)) $time = helper::now(); $this->dao->update(TABLE_NOTIFY)->set('status')->eq($status)->set('sendTime')->eq($time)->where('id')->in($idList)->exec(); } }