get('related_to'); $commentType = (string)$request->get('commenttype'); $isPrivate = (int)$request->get('is_private') ? 1 : 0; $message = (string)$request->getRaw('commentcontent'); $createdTime = null; $rct = $adb->pquery('SELECT createdtime FROM vtiger_crmentity WHERE crmid = ?', array($commentId)); if ($rct && $adb->num_rows($rct) > 0) { $createdTime = (string)$adb->query_result($rct, 0, 'createdtime'); } $parent = self::getParentInfo($relatedTo); $contactId = isset($parent['contact_id']) ? (int)$parent['contact_id'] : 0; $contact = $contactId > 0 ? self::getContactInfo($contactId) : null; $attachments = self::getAttachmentsForComment($commentId); $payloadBase = array( 'event' => 'crm_comment_created', 'ts' => date('c'), 'comment_id' => $commentId, 'comment_createdtime' => $createdTime, 'channel' => $commentType, 'message' => $message, 'is_private' => $isPrivate, 'author_user_id' => $authorUserId, 'parent' => $parent, 'contact_id' => $contactId, 'contact' => $contact, 'attachments' => $attachments, ); if (empty($attachments)) { self::enqueueJob($payloadBase); } else { foreach ($attachments as $attachment) { $payload = $payloadBase; $payload['attachment'] = $attachment; $filePath = isset($attachment['local_path']) ? $attachment['local_path'] : null; $fileName = (isset($attachment['storedname']) && $attachment['storedname'] !== '') ? $attachment['storedname'] : (isset($attachment['name']) ? $attachment['name'] : null); $mimeType = isset($attachment['type']) ? $attachment['type'] : null; if ($filePath && is_file($filePath) && is_readable($filePath)) { $size = @filesize($filePath); if ($size !== false && $size <= self::MAX_BINARY_BYTES) { self::enqueueJob($payload, $filePath, $fileName, $mimeType); continue; } } self::enqueueJob($payload); } } self::registerShutdown(); } private static function enqueueJob($payload, $filePath = null, $fileName = null, $mimeType = null) { self::$queue[] = array( 'url' => self::WEBHOOK_URL, 'payload' => $payload, 'file_path' => $filePath, 'file_name' => $fileName, 'mime_type' => $mimeType, ); } private static function registerShutdown() { if (self::$shutdownRegistered) return; self::$shutdownRegistered = true; register_shutdown_function(array(__CLASS__, 'flushQueue')); } public static function flushQueue() { if (empty(self::$queue)) return; if (function_exists('fastcgi_finish_request')) { @fastcgi_finish_request(); } foreach (self::$queue as $job) { $res = self::sendWebhook($job['url'], $job['payload'], $job['file_path'], $job['file_name'], $job['mime_type']); self::log('Webhook result: ' . json_encode($res, JSON_UNESCAPED_UNICODE)); } } private static function sendWebhook($url, $payload, $filePath = null, $fileName = null, $mimeType = null) { $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE); if ($payloadJson === false) { return array('ok' => false, 'error' => 'json_encode_failed'); } $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::CONNECT_TIMEOUT_SEC); curl_setopt($ch, CURLOPT_TIMEOUT, self::TIMEOUT_SEC); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2); $isMultipart = $filePath && is_file($filePath) && is_readable($filePath); if ($isMultipart) { $postFields = array( 'payload' => $payloadJson, 'file' => new CURLFile($filePath, $mimeType ? $mimeType : 'application/octet-stream', $fileName ? $fileName : basename($filePath)), ); curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields); } else { curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json; charset=utf-8')); curl_setopt($ch, CURLOPT_POSTFIELDS, $payloadJson); } $resp = curl_exec($ch); $errno = curl_errno($ch); $error = $errno ? curl_error($ch) : null; $code = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); return array( 'ok' => ($errno === 0) && ($code >= 200 && $code < 300), 'http_code' => $code, 'curl_errno' => $errno, 'curl_error' => $error, 'multipart' => $isMultipart, 'resp_len' => is_string($resp) ? strlen($resp) : 0, ); } private static function getParentInfo($parentId) { global $adb; $parentId = (int)$parentId; if ($parentId <= 0) return array('id' => 0, 'module' => null, 'contact_id' => 0); $module = null; $r = $adb->pquery('SELECT setype FROM vtiger_crmentity WHERE crmid = ? AND deleted = 0', array($parentId)); if ($r && $adb->num_rows($r) > 0) { $module = (string)$adb->query_result($r, 0, 'setype'); } $contactId = 0; $ticketId = 0; $projectId = 0; if ($module === 'Contacts') { $contactId = $parentId; } elseif ($module === 'HelpDesk') { $ticketId = $parentId; $r2 = $adb->pquery('SELECT contact_id FROM vtiger_troubletickets WHERE ticketid = ?', array($parentId)); if ($r2 && $adb->num_rows($r2) > 0) { $contactId = (int)$adb->query_result($r2, 0, 'contact_id'); } } elseif ($module === 'Project') { $projectId = $parentId; $r2 = $adb->pquery('SELECT linktoaccountscontacts FROM vtiger_project WHERE projectid = ?', array($parentId)); if ($r2 && $adb->num_rows($r2) > 0) { $contactId = (int)$adb->query_result($r2, 0, 'linktoaccountscontacts'); } } return array( 'id' => $parentId, 'module' => $module, 'contact_id' => (int)$contactId, 'ticket_id' => (int)$ticketId, 'project_id' => (int)$projectId, ); } private static function getContactInfo($contactId) { global $adb; $contactId = (int)$contactId; if ($contactId <= 0) return null; $r = $adb->pquery( "SELECT c.mobile, cf.cf_2616\n" . "FROM vtiger_contactdetails c\n" . "INNER JOIN vtiger_crmentity e ON e.crmid = c.contactid AND e.deleted = 0\n" . "LEFT JOIN vtiger_contactscf cf ON cf.contactid = c.contactid\n" . "WHERE c.contactid = ?", array($contactId) ); if (!$r || $adb->num_rows($r) === 0) { self::log('Contact info not found or query failed: contactid=' . $contactId); return array('id' => $contactId, 'mobile' => null, 'cf_2616' => null); } return array( 'id' => $contactId, 'mobile' => (string)$adb->query_result($r, 0, 'mobile'), 'cf_2616' => $adb->query_result($r, 0, 'cf_2616'), ); } private static function getAttachmentsForComment($commentId) { global $adb; $commentId = (int)$commentId; if ($commentId <= 0) return array(); $siteUrl = null; if (isset($GLOBALS['site_URL']) && $GLOBALS['site_URL']) { $siteUrl = rtrim((string)$GLOBALS['site_URL'], '/'); } $out = array(); $r = $adb->pquery( "SELECT a.attachmentsid, a.name, a.type, a.path, a.storedname\n" . "FROM vtiger_seattachmentsrel r\n" . "INNER JOIN vtiger_attachments a ON a.attachmentsid = r.attachmentsid\n" . "WHERE r.crmid = ?", array($commentId) ); if (!$r) return array(); $rows = $adb->num_rows($r); for ($i = 0; $i < $rows; $i++) { $aid = (int)$adb->query_result($r, $i, 'attachmentsid'); $name = (string)$adb->query_result($r, $i, 'name'); $type = (string)$adb->query_result($r, $i, 'type'); $path = (string)$adb->query_result($r, $i, 'path'); $stored = (string)$adb->query_result($r, $i, 'storedname'); $download = 'index.php?module=ModComments&action=DownloadFile&record=' . $commentId . '&fileid=' . $aid; $downloadAbs = $siteUrl ? ($siteUrl . '/' . $download) : null; $localPath = null; if ($path && preg_match('#^https?://#i', $path)) { $localPath = null; } else { $candidate1 = $path . $aid . '_' . ($stored ? $stored : $name); $candidate2 = $path . $aid . '_' . $name; if (@is_file($candidate1)) { $localPath = $candidate1; } elseif (@is_file($candidate2)) { $localPath = $candidate2; } } $out[] = array( 'attachmentsid' => $aid, 'name' => $name, 'type' => $type, 'path' => $path, 'storedname' => $stored, 'download_url' => $download, 'download_url_abs' => $downloadAbs, 'local_path' => $localPath, ); } return $out; } private static function log($msg) { $line = date('Y-m-d H:i:s') . ' ' . $msg . PHP_EOL; @file_put_contents(self::LOG_FILE, $line, FILE_APPEND); } }