diff --git a/api/BusinessLogic/Tickets/CreateReplyRequest.php b/api/BusinessLogic/Tickets/CreateReplyRequest.php index e0d5bcd6..b66febce 100644 --- a/api/BusinessLogic/Tickets/CreateReplyRequest.php +++ b/api/BusinessLogic/Tickets/CreateReplyRequest.php @@ -4,6 +4,7 @@ namespace BusinessLogic\Tickets; class CreateReplyRequest { + public $ticketId; public $trackingId; public $emailAddress; public $replyMessage; diff --git a/api/BusinessLogic/Tickets/ReplyCreator.php b/api/BusinessLogic/Tickets/ReplyCreator.php index cfbf74a0..d43eeb5f 100644 --- a/api/BusinessLogic/Tickets/ReplyCreator.php +++ b/api/BusinessLogic/Tickets/ReplyCreator.php @@ -4,13 +4,19 @@ namespace BusinessLogic\Tickets; use BusinessLogic\Emails\EmailSenderHelper; +use BusinessLogic\Emails\EmailTemplateRetriever; use BusinessLogic\Exceptions\ApiFriendlyException; +use BusinessLogic\Exceptions\ValidationException; use BusinessLogic\Helpers; use BusinessLogic\Security\UserContext; +use BusinessLogic\Statuses\Closable; +use BusinessLogic\Statuses\DefaultStatusForAction; use BusinessLogic\ValidationModel; use DataAccess\AuditTrail\AuditTrailGateway; +use DataAccess\Security\LoginGateway; use DataAccess\Security\UserGateway; use DataAccess\Statuses\StatusGateway; +use DataAccess\Tickets\ReplyGateway; use DataAccess\Tickets\TicketGateway; class ReplyCreator { @@ -19,17 +25,23 @@ class ReplyCreator { private $emailSenderHelper; private $userGateway; private $auditTrailGateway; + private $loginGateway; + private $replyGateway; public function __construct(StatusGateway $statusGateway, TicketGateway $ticketGateway, EmailSenderHelper $emailSenderHelper, UserGateway $userGateway, - AuditTrailGateway $auditTrailGateway) { + AuditTrailGateway $auditTrailGateway, + LoginGateway $loginGateway, + ReplyGateway $replyGateway) { $this->statusGateway = $statusGateway; $this->ticketGateway = $ticketGateway; $this->emailSenderHelper = $emailSenderHelper; $this->userGateway = $userGateway; $this->auditTrailGateway = $auditTrailGateway; + $this->loginGateway = $loginGateway; + $this->replyGateway = $replyGateway; } /** @@ -38,6 +50,7 @@ class ReplyCreator { * @param $modsForHeskSettings array * @param $userContext UserContext * @throws ApiFriendlyException + * @throws \Exception */ function createReplyByCustomer($replyRequest, $heskSettings, $modsForHeskSettings, $userContext) { $ticket = $this->ticketGateway->getTicketByTrackingId($replyRequest->trackingId, $heskSettings); @@ -50,11 +63,44 @@ class ReplyCreator { $validationModel = new ValidationModel(); if (!strlen($replyRequest->replyMessage)) { $validationModel->errorKeys[] = 'MESSAGE_REQUIRED'; + + 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); + $this->replyGateway->insertReply($ticket->id, $ticket->name, $replyRequest->replyMessage, $replyRequest->hasHtml, $heskSettings); + + //-- Changing the ticket message to the reply's + $ticket->message = $replyRequest->replyMessage; + + // TODO Send the email. } } \ 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..d5ffe113 --- /dev/null +++ b/api/Controllers/Tickets/CustomerReplyController.php @@ -0,0 +1,24 @@ +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'); + } +} 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/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..399586f5 --- /dev/null +++ b/api/DataAccess/Tickets/ReplyGateway.php @@ -0,0 +1,17 @@ +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 . "')"); + + $this->close(); + } +} \ No newline at end of file diff --git a/api/DataAccess/Tickets/TicketGateway.php b/api/DataAccess/Tickets/TicketGateway.php index 2ea5a84d..62a40558 100644 --- a/api/DataAccess/Tickets/TicketGateway.php +++ b/api/DataAccess/Tickets/TicketGateway.php @@ -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),