Merge branch '340-post-customer-reply' into '3-4-0'
Add Customer Reply POST API Endpoint See merge request mike-koch/Mods-for-HESK!97
This commit is contained in:
commit
4a73524c32
@ -18,11 +18,11 @@ class MailgunEmailSender extends \BaseClass implements EmailSender {
|
|||||||
|
|
||||||
$mailgunArray['to'] = implode(',', $emailBuilder->to);
|
$mailgunArray['to'] = implode(',', $emailBuilder->to);
|
||||||
|
|
||||||
if ($emailBuilder->cc !== null) {
|
if ($emailBuilder->cc !== null && count($emailBuilder->cc) > 0) {
|
||||||
$mailgunArray['cc'] = implode(',', $emailBuilder->cc);
|
$mailgunArray['cc'] = implode(',', $emailBuilder->cc);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($emailBuilder->bcc !== null) {
|
if ($emailBuilder->bcc !== null && count($emailBuilder->bcc) > 0) {
|
||||||
$mailgunArray['bcc'] = implode(',', $emailBuilder->bcc);
|
$mailgunArray['bcc'] = implode(',', $emailBuilder->bcc);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,7 +55,9 @@ class MailgunEmailSender extends \BaseClass implements EmailSender {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private function sendMessage($mailgunArray, $attachments, $modsForHeskSettings) {
|
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();
|
$mailgunAttachments = array();
|
||||||
if (count($attachments) > 0) {
|
if (count($attachments) > 0) {
|
||||||
|
@ -34,4 +34,154 @@ class Helpers extends \BaseClass {
|
|||||||
static function heskHtmlSpecialCharsDecode($in) {
|
static function heskHtmlSpecialCharsDecode($in) {
|
||||||
return str_replace(array('&', '<', '>', '"'), array('&', '<', '>', '"'), $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<a href=\"$url\" target=\"blank\" $class>$text</a>$append";
|
||||||
|
|
||||||
|
return $html;
|
||||||
|
} // END make_clickable_callback()
|
||||||
}
|
}
|
13
api/BusinessLogic/Tickets/CreateReplyRequest.php
Normal file
13
api/BusinessLogic/Tickets/CreateReplyRequest.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BusinessLogic\Tickets;
|
||||||
|
|
||||||
|
|
||||||
|
class CreateReplyRequest {
|
||||||
|
public $ticketId;
|
||||||
|
public $trackingId;
|
||||||
|
public $emailAddress;
|
||||||
|
public $replyMessage;
|
||||||
|
public $hasHtml;
|
||||||
|
public $ipAddress;
|
||||||
|
}
|
13
api/BusinessLogic/Tickets/CustomerCreatedReplyModel.php
Normal file
13
api/BusinessLogic/Tickets/CustomerCreatedReplyModel.php
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BusinessLogic\Tickets;
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerCreatedReplyModel {
|
||||||
|
public $id;
|
||||||
|
public $ticketId;
|
||||||
|
public $replierName;
|
||||||
|
public $message;
|
||||||
|
public $dateCreated;
|
||||||
|
public $html;
|
||||||
|
}
|
145
api/BusinessLogic/Tickets/ReplyCreator.php
Normal file
145
api/BusinessLogic/Tickets/ReplyCreator.php
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BusinessLogic\Tickets;
|
||||||
|
|
||||||
|
|
||||||
|
use BusinessLogic\Emails\Addressees;
|
||||||
|
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 extends \BaseClass {
|
||||||
|
private $statusGateway;
|
||||||
|
private $ticketGateway;
|
||||||
|
private $emailSenderHelper;
|
||||||
|
private $userGateway;
|
||||||
|
private $auditTrailGateway;
|
||||||
|
private $loginGateway;
|
||||||
|
private $replyGateway;
|
||||||
|
|
||||||
|
public function __construct(StatusGateway $statusGateway,
|
||||||
|
TicketGateway $ticketGateway,
|
||||||
|
EmailSenderHelper $emailSenderHelper,
|
||||||
|
UserGateway $userGateway,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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;
|
||||||
|
}
|
||||||
|
}
|
40
api/Controllers/Tickets/CustomerReplyController.php
Normal file
40
api/Controllers/Tickets/CustomerReplyController.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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) {
|
||||||
|
global $applicationContext, $hesk_settings;
|
||||||
|
|
||||||
|
$jsonRequest = JsonRetriever::getJsonData();
|
||||||
|
|
||||||
|
$createReplyByCustomerModel = new CreateReplyRequest();
|
||||||
|
$createReplyByCustomerModel->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);
|
||||||
|
}
|
||||||
|
}
|
24
api/DataAccess/Security/LoginGateway.php
Normal file
24
api/DataAccess/Security/LoginGateway.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DataAccess\Security;
|
||||||
|
|
||||||
|
|
||||||
|
use DataAccess\CommonDao;
|
||||||
|
|
||||||
|
class LoginGateway extends CommonDao {
|
||||||
|
function isIpLockedOut($ipAddress, $heskSettings) {
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
@ -100,6 +100,21 @@ class UserGateway extends CommonDao {
|
|||||||
return $users;
|
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) {
|
function getManagerForCategory($categoryId, $heskSettings) {
|
||||||
$this->init();
|
$this->init();
|
||||||
|
|
||||||
|
@ -53,4 +53,22 @@ class StatusGateway extends CommonDao {
|
|||||||
|
|
||||||
return $statuses;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
33
api/DataAccess/Tickets/ReplyGateway.php
Normal file
33
api/DataAccess/Tickets/ReplyGateway.php
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace DataAccess\Tickets;
|
||||||
|
|
||||||
|
|
||||||
|
use BusinessLogic\Tickets\CustomerCreatedReplyModel;
|
||||||
|
use DataAccess\CommonDao;
|
||||||
|
|
||||||
|
class ReplyGateway extends CommonDao {
|
||||||
|
function insertReply($ticketId, $name, $message, $html, $heskSettings) {
|
||||||
|
$this->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;
|
||||||
|
}
|
||||||
|
}
|
@ -312,8 +312,8 @@ class TicketGateway extends CommonDao {
|
|||||||
|
|
||||||
$generatedFields = new TicketGatewayGeneratedFields();
|
$generatedFields = new TicketGatewayGeneratedFields();
|
||||||
$generatedFields->id = $id;
|
$generatedFields->id = $id;
|
||||||
$generatedFields->dateCreated = $row['dt'];
|
$generatedFields->dateCreated = hesk_date($row['dt'], true);
|
||||||
$generatedFields->dateModified = $row['lastchange'];
|
$generatedFields->dateModified = hesk_date($row['lastchange'], true);
|
||||||
|
|
||||||
$this->close();
|
$this->close();
|
||||||
|
|
||||||
@ -454,4 +454,34 @@ class TicketGateway extends CommonDao {
|
|||||||
|
|
||||||
$this->close();
|
$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();
|
||||||
|
}
|
||||||
}
|
}
|
@ -199,6 +199,7 @@ Link::all(array(
|
|||||||
'/v1-internal/categories/{i}/sort/{s}' => action(\Controllers\Categories\CategoryController::clazz() . '::sort', array(RequestMethod::POST), SecurityHandler::INTERNAL),
|
'/v1-internal/categories/{i}/sort/{s}' => action(\Controllers\Categories\CategoryController::clazz() . '::sort', array(RequestMethod::POST), SecurityHandler::INTERNAL),
|
||||||
// Tickets
|
// Tickets
|
||||||
'/v1/tickets' => action(\Controllers\Tickets\CustomerTicketController::clazz(), RequestMethod::all(), SecurityHandler::OPEN),
|
'/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
|
// Tickets - Staff
|
||||||
'/v1/staff/tickets/{i}' => action(\Controllers\Tickets\StaffTicketController::clazz(), RequestMethod::all()),
|
'/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),
|
'/v1/staff/tickets/{i}/due-date' => action(\Controllers\Tickets\StaffTicketController::clazz() . '::updateDueDate', array(RequestMethod::PATCH), SecurityHandler::INTERNAL_OR_AUTH_TOKEN),
|
||||||
|
Loading…
x
Reference in New Issue
Block a user