diff --git a/api/BusinessLogic/Emails/MailgunEmailSender.php b/api/BusinessLogic/Emails/MailgunEmailSender.php index e46289e1..6231c26c 100644 --- a/api/BusinessLogic/Emails/MailgunEmailSender.php +++ b/api/BusinessLogic/Emails/MailgunEmailSender.php @@ -18,11 +18,11 @@ class MailgunEmailSender extends \BaseClass implements EmailSender { $mailgunArray['to'] = implode(',', $emailBuilder->to); - if ($emailBuilder->cc !== null) { + if ($emailBuilder->cc !== null && count($emailBuilder->cc) > 0) { $mailgunArray['cc'] = implode(',', $emailBuilder->cc); } - if ($emailBuilder->bcc !== null) { + if ($emailBuilder->bcc !== null && count($emailBuilder->bcc) > 0) { $mailgunArray['bcc'] = implode(',', $emailBuilder->bcc); } @@ -55,7 +55,9 @@ class MailgunEmailSender extends \BaseClass implements EmailSender { } private function sendMessage($mailgunArray, $attachments, $modsForHeskSettings) { - $messageClient = new Mailgun($modsForHeskSettings['mailgun_api_key']); + $ssl = !defined('NO_MAILGUN_SSL'); + + $messageClient = new Mailgun($modsForHeskSettings['mailgun_api_key'], 'api.mailgun.net', 'v2', $ssl); $mailgunAttachments = array(); if (count($attachments) > 0) { diff --git a/api/BusinessLogic/Helpers.php b/api/BusinessLogic/Helpers.php index b841fd27..ea92bc5c 100644 --- a/api/BusinessLogic/Helpers.php +++ b/api/BusinessLogic/Helpers.php @@ -34,4 +34,154 @@ class Helpers extends \BaseClass { static function heskHtmlSpecialCharsDecode($in) { return str_replace(array('&', '<', '>', '"'), array('&', '<', '>', '"'), $in); } + + static function heskMakeUrl($text, $class = '', $shortenLinks = true) { + if (!defined('MAGIC_URL_EMAIL')) { + define('MAGIC_URL_EMAIL', 1); + define('MAGIC_URL_FULL', 2); + define('MAGIC_URL_LOCAL', 3); + define('MAGIC_URL_WWW', 4); + } + + $class = ($class) ? ' class="' . $class . '"' : ''; + + // matches a xxxx://aaaaa.bbb.cccc. ... + $text = preg_replace_callback( + '#(^|[\n\t (>.])(' . "[a-z][a-z\d+]*:/{2}(?:(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@|]+|%[\dA-F]{2})+|[0-9.]+|\[[a-z0-9.]+:[a-z0-9.]+:[a-z0-9.:]+\])(?::\d*)?(?:/(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@|]+|%[\dA-F]{2})*)*(?:\?(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@/?|]+|%[\dA-F]{2})*)?(?:\#(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@/?|]+|%[\dA-F]{2})*)?" . ')#iu', + function($matches) use ($class, $shortenLinks) { + return self::makeClickableCallback(MAGIC_URL_FULL, $matches[1], $matches[2], '', $class, $shortenLinks); + }, + $text + ); + + // matches a "www.xxxx.yyyy[/zzzz]" kinda lazy URL thing + $text = preg_replace_callback( + '#(^|[\n\t (>])(' . "www\.(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@|]+|%[\dA-F]{2})+(?::\d*)?(?:/(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@|]+|%[\dA-F]{2})*)*(?:\?(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@/?|]+|%[\dA-F]{2})*)?(?:\#(?:[^\p{C}\p{Z}\p{S}\p{P}\p{Nl}\p{No}\p{Me}\x{1100}-\x{115F}\x{A960}-\x{A97C}\x{1160}-\x{11A7}\x{D7B0}-\x{D7C6}\x{20D0}-\x{20FF}\x{1D100}-\x{1D1FF}\x{1D200}-\x{1D24F}\x{0640}\x{07FA}\x{302E}\x{302F}\x{3031}-\x{3035}\x{303B}]*[\x{00B7}\x{0375}\x{05F3}\x{05F4}\x{30FB}\x{002D}\x{06FD}\x{06FE}\x{0F0B}\x{3007}\x{00DF}\x{03C2}\x{200C}\x{200D}\pL0-9\-._~!$&'(*+,;=:@/?|]+|%[\dA-F]{2})*)?" . ')#iu', + function($matches) use ($class, $shortenLinks) { + return self::makeClickableCallback(MAGIC_URL_WWW, $matches[1], $matches[2], '', $class, $shortenLinks); + }, + $text + ); + + // matches an email address + $text = preg_replace_callback( + '/(^|[\n\t (>])(' . '((?:[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*(?:[\w\!\#$\%\'\*\+\-\/\=\?\^\`{\|\}\~]|&)+)@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,63})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)' . ')/iu', + function($matches) use ($class, $shortenLinks) { + return self::makeClickableCallback(MAGIC_URL_EMAIL, $matches[1], $matches[2], '', $class, $shortenLinks); + }, + $text + ); + + return $text; + } + + static function makeClickableCallback($type, $whitespace, $url, $relative_url, $class, $shortenLinks) + { + global $hesk_settings; + + $orig_url = $url; + $orig_relative = $relative_url; + $append = ''; + $url = htmlspecialchars_decode($url); + $relative_url = htmlspecialchars_decode($relative_url); + + // make sure no HTML entities were matched + $chars = array('<', '>', '"'); + $split = false; + + foreach ($chars as $char) { + $next_split = strpos($url, $char); + if ($next_split !== false) { + $split = ($split !== false) ? min($split, $next_split) : $next_split; + } + } + + if ($split !== false) { + // an HTML entity was found, so the URL has to end before it + $append = substr($url, $split) . $relative_url; + $url = substr($url, 0, $split); + $relative_url = ''; + } else if ($relative_url) { + // same for $relative_url + $split = false; + foreach ($chars as $char) { + $next_split = strpos($relative_url, $char); + if ($next_split !== false) { + $split = ($split !== false) ? min($split, $next_split) : $next_split; + } + } + + if ($split !== false) { + $append = substr($relative_url, $split); + $relative_url = substr($relative_url, 0, $split); + } + } + + // if the last character of the url is a punctuation mark, exclude it from the url + $last_char = ($relative_url) ? $relative_url[strlen($relative_url) - 1] : $url[strlen($url) - 1]; + + switch ($last_char) { + case '.': + case '?': + case '!': + case ':': + case ',': + $append = $last_char; + if ($relative_url) { + $relative_url = substr($relative_url, 0, -1); + } else { + $url = substr($url, 0, -1); + } + break; + + // set last_char to empty here, so the variable can be used later to + // check whether a character was removed + default: + $last_char = ''; + break; + } + + $short_url = ($hesk_settings['short_link'] && strlen($url) > 70 && $shortenLinks) ? substr($url, 0, 54) . ' ... ' . substr($url, -10) : $url; + + switch ($type) { + case MAGIC_URL_LOCAL: + $tag = 'l'; + $relative_url = preg_replace('/[&?]sid=[0-9a-f]{32}$/', '', preg_replace('/([&?])sid=[0-9a-f]{32}&/', '$1', $relative_url)); + $url = $url . '/' . $relative_url; + $text = $relative_url; + + // this url goes to http://domain.tld/path/to/board/ which + // would result in an empty link if treated as local so + // don't touch it and let MAGIC_URL_FULL take care of it. + if (!$relative_url) { + return $whitespace . $orig_url . '/' . $orig_relative; // slash is taken away by relative url pattern + } + break; + + case MAGIC_URL_FULL: + $tag = 'm'; + $text = $short_url; + break; + + case MAGIC_URL_WWW: + $tag = 'w'; + $url = 'http://' . $url; + $text = $short_url; + break; + + case MAGIC_URL_EMAIL: + $tag = 'e'; + $text = $short_url; + $url = 'mailto:' . $url; + break; + } + + $url = htmlspecialchars($url); + $text = htmlspecialchars($text); + $append = htmlspecialchars($append); + + $html = "$whitespace$text$append"; + + return $html; + } // END make_clickable_callback() } \ No newline at end of file diff --git a/api/BusinessLogic/Tickets/CreateReplyRequest.php b/api/BusinessLogic/Tickets/CreateReplyRequest.php new file mode 100644 index 00000000..b66febce --- /dev/null +++ b/api/BusinessLogic/Tickets/CreateReplyRequest.php @@ -0,0 +1,13 @@ +statusGateway = $statusGateway; + $this->ticketGateway = $ticketGateway; + $this->emailSenderHelper = $emailSenderHelper; + $this->userGateway = $userGateway; + $this->auditTrailGateway = $auditTrailGateway; + $this->loginGateway = $loginGateway; + $this->replyGateway = $replyGateway; + } + + /** + * @param $replyRequest CreateReplyRequest + * @param $heskSettings array + * @param $modsForHeskSettings array + * @throws ApiFriendlyException + * @throws \Exception + */ + function createReplyByCustomer($replyRequest, $heskSettings, $modsForHeskSettings) { + $ticket = $this->ticketGateway->getTicketByTrackingId($replyRequest->trackingId, $heskSettings); + + if ($ticket === null) { + throw new ApiFriendlyException("Ticket with tracking ID {$replyRequest->trackingId} not found.", + "Ticket not found", 404); + } + + $validationModel = new ValidationModel(); + if ($replyRequest->replyMessage === null || trim($replyRequest->replyMessage) === '') { + $validationModel->errorKeys[] = 'MESSAGE_REQUIRED'; + } + + if ($heskSettings['email_view_ticket']) { + if ($replyRequest->emailAddress === null || trim($replyRequest->emailAddress) === '') { + $validationModel->errorKeys[] = 'EMAIL_REQUIRED'; + } elseif (!in_array($replyRequest->emailAddress, $ticket->email)) { + $validationModel->errorKeys[] = 'EMAIL_NOT_FOUND_ON_TICKET'; + } + } + + if (count($validationModel->errorKeys) > 0) { + throw new ValidationException($validationModel); + } + + if ($modsForHeskSettings['rich_text_for_tickets_for_customers']) { + $replyRequest->replyMessage = Helpers::heskMakeUrl($replyRequest->replyMessage); + $replyRequest->replyMessage = nl2br($replyRequest->replyMessage); + } + + if ($this->loginGateway->isIpLockedOut($replyRequest->ipAddress, $heskSettings)) { + throw new ApiFriendlyException("The IP address entered has been locked out of the system for {$heskSettings['attempt_banmin']} minutes because of too many login failures", + "Locked Out", + 403); + } + + if ($this->ticketGateway->areRepliesBeingFlooded($replyRequest->ticketId, $replyRequest->ipAddress, $heskSettings)) { + throw new ApiFriendlyException("You have been locked out of the system for {$heskSettings['attempt_banmin']} minutes because of too many replies to a ticket.", + "Locked Out", + 403); + } + + // If staff hasn't replied yet, don't change the status; otherwise set it to the status for customer replies + $currentStatus = $this->statusGateway->getStatusById($ticket->statusId, $heskSettings); + if ($currentStatus->closable === Closable::YES || $currentStatus->closable === Closable::CUSTOMERS_ONLY) { + $customerReplyStatus = $this->statusGateway->getStatusForDefaultAction(DefaultStatusForAction::CUSTOMER_REPLY, $heskSettings); + $defaultNewTicketStatus = $this->statusGateway->getStatusForDefaultAction(DefaultStatusForAction::NEW_TICKET, $heskSettings); + + $ticket->statusId = $ticket->statusId === $defaultNewTicketStatus->id ? + $defaultNewTicketStatus->id : + $customerReplyStatus->id; + } + + $this->ticketGateway->updateMetadataForReply($ticket->id, $ticket->statusId, $heskSettings); + $createdReply = $this->replyGateway->insertReply($ticket->id, $ticket->name, $replyRequest->replyMessage, $replyRequest->hasHtml, $heskSettings); + + //-- Changing the ticket message to the reply's + $ticket->message = $replyRequest->replyMessage; + + $addressees = new Addressees(); + if ($ticket->ownerId !== null && $ticket->ownerId !== 0) { + $owner = $this->userGateway->getUserById($ticket->ownerId, $heskSettings); + + if ($owner->notificationSettings->replyToMe) { + $addressees->to[] = $owner->email; + $language = $owner->language === null ? $heskSettings['language'] : $owner->language; + $this->emailSenderHelper->sendEmailForTicket(EmailTemplateRetriever::NEW_REPLY_BY_CUSTOMER, + $language, + $addressees, + $ticket, + $heskSettings, + $modsForHeskSettings); + } + } else { + $users = $this->userGateway->getUsersForUnassignedReplyNotification($heskSettings); + foreach ($users as $user) { + $addressees->to[] = $user->email; + $language = $user->language === null ? $heskSettings['language'] : $user->language; + + $this->emailSenderHelper->sendEmailForTicket(EmailTemplateRetriever::NEW_REPLY_BY_CUSTOMER, + $language, + $addressees, + $ticket, + $heskSettings, + $modsForHeskSettings); + } + } + + return $createdReply; + } +} \ No newline at end of file diff --git a/api/Controllers/Tickets/CustomerReplyController.php b/api/Controllers/Tickets/CustomerReplyController.php new file mode 100644 index 00000000..a52d1866 --- /dev/null +++ b/api/Controllers/Tickets/CustomerReplyController.php @@ -0,0 +1,40 @@ +id = $ticketId; + $createReplyByCustomerModel->emailAddress = Helpers::safeArrayGet($jsonRequest, 'email'); + $createReplyByCustomerModel->trackingId = Helpers::safeArrayGet($jsonRequest, 'trackingId'); + $createReplyByCustomerModel->replyMessage = Helpers::safeArrayGet($jsonRequest, 'message'); + $createReplyByCustomerModel->hasHtml = Helpers::safeArrayGet($jsonRequest, 'html'); + $createReplyByCustomerModel->ipAddress = Helpers::safeArrayGet($jsonRequest, 'ip'); + + if ($createReplyByCustomerModel->ipAddress === null) { + $createReplyByCustomerModel->ipAddress = hesk_getClientIP(); + } + + /* @var $modsForHeskSettingsGateway ModsForHeskSettingsGateway */ + $modsForHeskSettingsGateway = $applicationContext->get(ModsForHeskSettingsGateway::clazz()); + $modsForHesk_settings = $modsForHeskSettingsGateway->getAllSettings($hesk_settings); + + /* @var $replyCreator ReplyCreator */ + $replyCreator = $applicationContext->get(ReplyCreator::clazz()); + $createdReply = $replyCreator->createReplyByCustomer($createReplyByCustomerModel, $hesk_settings, $modsForHesk_settings); + + return output($createdReply, 201); + } +} diff --git a/api/DataAccess/Security/LoginGateway.php b/api/DataAccess/Security/LoginGateway.php new file mode 100644 index 00000000..461eb62d --- /dev/null +++ b/api/DataAccess/Security/LoginGateway.php @@ -0,0 +1,24 @@ +init(); + + $rs = hesk_dbQuery("SELECT `number` FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "logins` + WHERE `ip` = '" . hesk_dbEscape($ipAddress) . "' + AND `last_attempt` IS NOT NULL + AND DATE_ADD(`last_attempt`, INTERVAL ".intval($heskSettings['attempt_banmin'])." MINUTE ) > NOW() LIMIT 1"); + + $result = hesk_dbNumRows($rs) == 1 && + hesk_dbResult($rs) >= $heskSettings['attempt_limit']; + + $this->close(); + + return $result; + } +} \ No newline at end of file diff --git a/api/DataAccess/Security/UserGateway.php b/api/DataAccess/Security/UserGateway.php index e836f18a..7a08cc39 100644 --- a/api/DataAccess/Security/UserGateway.php +++ b/api/DataAccess/Security/UserGateway.php @@ -100,6 +100,21 @@ class UserGateway extends CommonDao { return $users; } + function getUsersForUnassignedReplyNotification($heskSettings) { + $this->init(); + + $rs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "users` WHERE `notify_reply_unassigned` = '1' AND `active` = '1'"); + + $users = array(); + while ($row = hesk_dbFetchAssoc($rs)) { + $users[] = UserContext::fromDataRow($row); + } + + $this->close(); + + return $users; + } + function getManagerForCategory($categoryId, $heskSettings) { $this->init(); diff --git a/api/DataAccess/Statuses/StatusGateway.php b/api/DataAccess/Statuses/StatusGateway.php index c18e9b19..a44d8d6b 100644 --- a/api/DataAccess/Statuses/StatusGateway.php +++ b/api/DataAccess/Statuses/StatusGateway.php @@ -53,4 +53,22 @@ class StatusGateway extends CommonDao { return $statuses; } + + function getStatusById($id, $heskSettings) { + $this->init(); + + $metaRs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "statuses` WHERE `ID` = " . $id); + + $status = null; + if ($row = hesk_dbFetchAssoc($metaRs)) { + $languageRs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "text_to_status_xref` + WHERE `status_id` = " . intval($row['ID'])); + + $status = Status::fromDatabase($row, $languageRs); + } + + $this->close(); + + return $status; + } } \ No newline at end of file diff --git a/api/DataAccess/Tickets/ReplyGateway.php b/api/DataAccess/Tickets/ReplyGateway.php new file mode 100644 index 00000000..ab74c459 --- /dev/null +++ b/api/DataAccess/Tickets/ReplyGateway.php @@ -0,0 +1,33 @@ +init(); + + hesk_dbQuery("INSERT INTO `" . hesk_dbEscape($heskSettings['db_pfix']) . "replies` (`replyto`,`name`,`message`,`dt`,`attachments`, `html`) + VALUES ({$ticketId},'" . hesk_dbEscape($name) . "','" . hesk_dbEscape($message) . "',NOW(),'','" . $html . "')"); + + $customerCreatedReplyModel = new CustomerCreatedReplyModel(); + $id = hesk_dbInsertID(); + + $rs = hesk_dbQuery("SELECT * FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "replies` WHERE `id` = " . intval($id)); + $row = hesk_dbFetchAssoc($rs); + + $customerCreatedReplyModel->id = $row['id']; + $customerCreatedReplyModel->message = $row['message']; + $customerCreatedReplyModel->ticketId = $row['replyto']; + $customerCreatedReplyModel->dateCreated = hesk_date($row['dt'], true); + $customerCreatedReplyModel->html = $row['html'] === '1'; + $customerCreatedReplyModel->replierName = $row['name']; + + $this->close(); + + return $customerCreatedReplyModel; + } +} \ No newline at end of file diff --git a/api/DataAccess/Tickets/TicketGateway.php b/api/DataAccess/Tickets/TicketGateway.php index 2ea5a84d..892afebc 100644 --- a/api/DataAccess/Tickets/TicketGateway.php +++ b/api/DataAccess/Tickets/TicketGateway.php @@ -312,8 +312,8 @@ class TicketGateway extends CommonDao { $generatedFields = new TicketGatewayGeneratedFields(); $generatedFields->id = $id; - $generatedFields->dateCreated = $row['dt']; - $generatedFields->dateModified = $row['lastchange']; + $generatedFields->dateCreated = hesk_date($row['dt'], true); + $generatedFields->dateModified = hesk_date($row['lastchange'], true); $this->close(); @@ -454,4 +454,34 @@ class TicketGateway extends CommonDao { $this->close(); } + + function areRepliesBeingFlooded($id, $ip, $heskSettings) { + $this->init(); + + $result = false; + $res = hesk_dbQuery("SELECT `staffid` FROM `" . hesk_dbEscape($heskSettings['db_pfix']) . "replies` WHERE `replyto`='{$id}' AND `dt` > DATE_SUB(NOW(), INTERVAL 10 MINUTE) ORDER BY `id` ASC"); + if (hesk_dbNumRows($res) > 0) { + $sequential_customer_replies = 0; + while ($tmp = hesk_dbFetchAssoc($res)) { + $sequential_customer_replies = $tmp['staffid'] ? 0 : $sequential_customer_replies + 1; + } + + if ($sequential_customer_replies > 10) { + hesk_dbQuery("INSERT INTO `".hesk_dbEscape($heskSettings['db_pfix'])."logins` (`ip`, `number`) VALUES ('".hesk_dbEscape($ip)."', ".intval($heskSettings['attempt_limit'] + 1).")"); + $result = true; + } + } + + $this->close(); + + return $result; + } + + function updateMetadataForReply($id, $status, $heskSettings) { + $this->init(); + + hesk_dbQuery("UPDATE `" . hesk_dbEscape($heskSettings['db_pfix']) . "tickets` SET `lastchange`=NOW(), `status`='{$status}', `replies`=`replies`+1, `lastreplier`='0' WHERE `id`='{$id}'"); + + $this->close(); + } } \ No newline at end of file diff --git a/api/index.php b/api/index.php index ec493347..b991e91c 100644 --- a/api/index.php +++ b/api/index.php @@ -199,6 +199,7 @@ Link::all(array( '/v1-internal/categories/{i}/sort/{s}' => action(\Controllers\Categories\CategoryController::clazz() . '::sort', array(RequestMethod::POST), SecurityHandler::INTERNAL), // Tickets '/v1/tickets' => action(\Controllers\Tickets\CustomerTicketController::clazz(), RequestMethod::all(), SecurityHandler::OPEN), + '/v1/tickets/{i}/replies' => action(\Controllers\Tickets\CustomerReplyController::clazz(), array(RequestMethod::POST), SecurityHandler::OPEN), // Tickets - Staff '/v1/staff/tickets/{i}' => action(\Controllers\Tickets\StaffTicketController::clazz(), RequestMethod::all()), '/v1/staff/tickets/{i}/due-date' => action(\Controllers\Tickets\StaffTicketController::clazz() . '::updateDueDate', array(RequestMethod::PATCH), SecurityHandler::INTERNAL_OR_AUTH_TOKEN),