From 6fa6c7b6863e847e41f05bf6203bb58f9857e905 Mon Sep 17 00:00:00 2001 From: Mike Koch Date: Tue, 10 Apr 2018 13:11:48 -0400 Subject: [PATCH] Add ability to reply via API --- api/BusinessLogic/Helpers.php | 131 ++++++++++++++++-- .../Tickets/CustomerCreatedReplyModel.php | 13 ++ api/BusinessLogic/Tickets/ReplyCreator.php | 51 ++++++- .../Tickets/CustomerReplyController.php | 12 ++ api/DataAccess/Security/UserGateway.php | 15 ++ api/DataAccess/Tickets/ReplyGateway.php | 16 +++ api/DataAccess/Tickets/TicketGateway.php | 4 +- 7 files changed, 222 insertions(+), 20 deletions(-) create mode 100644 api/BusinessLogic/Tickets/CustomerCreatedReplyModel.php diff --git a/api/BusinessLogic/Helpers.php b/api/BusinessLogic/Helpers.php index f356bfbb..ea92bc5c 100644 --- a/api/BusinessLogic/Helpers.php +++ b/api/BusinessLogic/Helpers.php @@ -48,33 +48,140 @@ class Helpers extends \BaseClass { // 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', - create_function( - "\$matches", - "return make_clickable_callback(MAGIC_URL_FULL, \$matches[1], \$matches[2], '', '$class', '$shortenLinks');" - ), + 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', - create_function( - "\$matches", - "return make_clickable_callback(MAGIC_URL_WWW, \$matches[1], \$matches[2], '', '$class', '$shortenLinks');" - ), + 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', - create_function( - "\$matches", - "return make_clickable_callback(MAGIC_URL_EMAIL, \$matches[1], \$matches[2], '', '$class', '$shortenLinks');" - ), + 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/CustomerCreatedReplyModel.php b/api/BusinessLogic/Tickets/CustomerCreatedReplyModel.php new file mode 100644 index 00000000..4b5bec2f --- /dev/null +++ b/api/BusinessLogic/Tickets/CustomerCreatedReplyModel.php @@ -0,0 +1,13 @@ +ticketGateway->getTicketByTrackingId($replyRequest->trackingId, $heskSettings); if ($ticket === null) { @@ -61,9 +61,19 @@ class ReplyCreator { } $validationModel = new ValidationModel(); - if (!strlen($replyRequest->replyMessage)) { + 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); } @@ -96,11 +106,40 @@ class ReplyCreator { } $this->ticketGateway->updateMetadataForReply($ticket->id, $ticket->statusId, $heskSettings); - $this->replyGateway->insertReply($ticket->id, $ticket->name, $replyRequest->replyMessage, $replyRequest->hasHtml, $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; - // TODO Send the email. + $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 index d5ffe113..1ed31771 100644 --- a/api/Controllers/Tickets/CustomerReplyController.php +++ b/api/Controllers/Tickets/CustomerReplyController.php @@ -5,7 +5,9 @@ namespace Controllers\Tickets; use BusinessLogic\Helpers; use BusinessLogic\Tickets\CreateReplyRequest; +use BusinessLogic\Tickets\ReplyCreator; use Controllers\JsonRetriever; +use DataAccess\Settings\ModsForHeskSettingsGateway; class CustomerReplyController extends \BaseClass { function post($ticketId) { @@ -20,5 +22,15 @@ class CustomerReplyController extends \BaseClass { $createReplyByCustomerModel->replyMessage = Helpers::safeArrayGet($jsonRequest, 'message'); $createReplyByCustomerModel->hasHtml = Helpers::safeArrayGet($jsonRequest, 'html'); $createReplyByCustomerModel->ipAddress = Helpers::safeArrayGet($jsonRequest, 'ip'); + + /* @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/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/Tickets/ReplyGateway.php b/api/DataAccess/Tickets/ReplyGateway.php index 399586f5..ab74c459 100644 --- a/api/DataAccess/Tickets/ReplyGateway.php +++ b/api/DataAccess/Tickets/ReplyGateway.php @@ -3,6 +3,7 @@ namespace DataAccess\Tickets; +use BusinessLogic\Tickets\CustomerCreatedReplyModel; use DataAccess\CommonDao; class ReplyGateway extends CommonDao { @@ -12,6 +13,21 @@ class ReplyGateway extends CommonDao { 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 62a40558..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();